mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
Merge branch 'p5e/ra' into feat/disaggregated (P5e ra)
This commit is contained in:
+90
-21
@@ -1187,11 +1187,24 @@ jobs:
|
||||
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
|
||||
# ---- P5e (ra), MG_Remote/CONTRACT-P5E.md §7: ONE STEP, TWO ARMS, AND THE TREE PICKS ----
|
||||
#
|
||||
# This lane was written as an EXPECTED-RED control: under strict every integration-split
|
||||
# entry aborts on its first BARRIER-PULLED read, so the step gated on the run failing and
|
||||
# on the strict marker being the reason. P5e turns it into a HARD GREEN GATE - once the
|
||||
# client runs ahead, an unbarriered record's pull is a Fatal whatever this knob says
|
||||
# (§3.3), so what strict adds is only the BARRIERED rows' pulls, and those are a short,
|
||||
# named list. A revert of any handle arm in vi/sb/pg/tx2/fb then goes red HERE, by field
|
||||
# and verb, instead of in a picture nobody diffs.
|
||||
#
|
||||
# THE ARM IS CHOSEN BY THE TREE, NOT BY A SECOND EDIT. kMGPipeP5eRunAheadReady in
|
||||
# MG_Backend/Init.cpp is the one constant the integration commit flips, and it is exactly
|
||||
# the fact this lane's expectation depends on: with it false no server publishes
|
||||
# kCapRunAheadApply, every record is barriered, every scenario still aborts on its first
|
||||
# pull and the expected-red control is the honest gate. Reading it here means the lane
|
||||
# changes meaning in the same commit the runtime does - a lane whose flip is a separate
|
||||
# human step is a lane that is wrong for as long as that step is outstanding.
|
||||
- name: Integration-split under strict - expected-red before the P5e flip, hard green after
|
||||
working-directory: build-split
|
||||
env:
|
||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||
@@ -1199,27 +1212,83 @@ jobs:
|
||||
MOBILEGL_TRANSPORT: "inproc"
|
||||
run: |
|
||||
ulimit -c unlimited
|
||||
ready=$(grep -oE 'kMGPipeP5eRunAheadReady = (true|false)' "${GITHUB_WORKSPACE}/MobileGL/MG_Backend/Init.cpp" | head -1 | awk '{print $3}')
|
||||
echo "kMGPipeP5eRunAheadReady = ${ready}"
|
||||
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)
|
||||
tail -5 "${RUNNER_TEMP}/strict-split.txt"
|
||||
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"
|
||||
|
||||
if [ "${ready}" != "true" ]; then
|
||||
# ---- the pre-flip arm, unchanged from P5c ----
|
||||
if [ "${rc}" -eq 0 ]; then
|
||||
echo "::error::integration-split under MOBILEGL_IPC_STRICT_ERRORS=1 went GREEN while kMGPipeP5eRunAheadReady is false - the object-class residual pulls (P3b/P4b/P7) are still pinned, so strict must abort every drawing scenario. If P5e has just landed, flip that constant in the same commit."
|
||||
exit 1
|
||||
fi
|
||||
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"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---- the post-flip arm: HARD GREEN, with §7's allowlist ----
|
||||
#
|
||||
# EVERY ENTRY MUST COMPLETE. An abort here is either an unbarriered pull (§3.3's
|
||||
# unconditional Fatal, which is a missed migration) or a barriered row that is not on
|
||||
# the list below, and both are the lane's job to catch.
|
||||
if [ "${rc}" -ne 0 ]; then
|
||||
echo "::error::integration-split under MOBILEGL_IPC_STRICT_ERRORS=1 is RED on a run-ahead server. CONTRACT-P5E §7 makes this lane a hard green gate: an unbarriered record's pull aborts whatever the knob says, so a red here names a field and a verb in the failing entry's private log."
|
||||
grep -hoE 'Fatal\{UnmigratedPipeInput, "[^"]*"\}' MobileGL/MG_IntegrationTest/split-logs/*.log 2>/dev/null | sort | uniq -c | sort -rn | head -20
|
||||
exit 1
|
||||
fi
|
||||
echo "integration-split under strict is red by design; ${hits} private log(s) carry the strict marker"
|
||||
|
||||
# THE ADMITTED MARKERS, `<field>@<verb>`, AND NOTHING ELSE. These are the barriered
|
||||
# rows of §2.2 - the ones whose apply still pulls a row, and whose legality is the
|
||||
# client's own wait (ruling 4). Each line names the package that retires it; a row
|
||||
# that leaves this list leaves it by deletion, never by widening.
|
||||
# readbacks / the pack PBO ......... P8/P9 (read_pixels, get_texture_image)
|
||||
# CopyTex endpoint ................. tx2's trailing item
|
||||
# GenerateMipmap ................... tx2, until the VerbMipRes arm lands
|
||||
# set_storage_block_binding ........ pg's trailing item (by MGPStorageBlockBinding::ShaderCso)
|
||||
# XFB ............................... P3b/P4b/P9 (escalation (i) keeps the span lockstep)
|
||||
# resource_copy_region's names ..... the declared P5b debt
|
||||
printf '%s\n' \
|
||||
'GetTextureUnitObject@ReadPixels' \
|
||||
'GetTextureUnitObject@GetTextureImage' \
|
||||
'GetTextureUnitObject@CopyTexImage2D' \
|
||||
'GetTextureUnitObject@GenerateMipmap' \
|
||||
'GetBufferBindingSlot@ReadPixels' \
|
||||
'GetBufferBindingSlot@GetTextureImage' \
|
||||
'GetProgramObject@ShaderStorageBlockBinding' \
|
||||
'ValidateProgramName@ShaderStorageBlockBinding' \
|
||||
'GetTextureObject@CopyImageSubData' \
|
||||
'GetTransformFeedbackProgram@BeginTransformFeedback' \
|
||||
'GetTransformFeedbackProgram@EndTransformFeedback' \
|
||||
'GetBufferBindingPoint@BeginTransformFeedback' \
|
||||
'GetBufferBindingPoint@EndTransformFeedback' \
|
||||
> "${RUNNER_TEMP}/allowed.txt"
|
||||
|
||||
grep -rhoE '\[BARRIER-PULLED[^]]*\]' MobileGL/MG_IntegrationTest/split-logs/ 2>/dev/null > "${RUNNER_TEMP}/markers-raw.txt" || true
|
||||
grep -rhoE 'Fatal\{UnmigratedPipeInput, "[^"]*"\}' MobileGL/MG_IntegrationTest/split-logs/ 2>/dev/null \
|
||||
| sed -E 's/.*"([^"]*)".*/\1/' | sort -u > "${RUNNER_TEMP}/markers.txt" || true
|
||||
bad=$(comm -23 "${RUNNER_TEMP}/markers.txt" <(sort -u "${RUNNER_TEMP}/allowed.txt") || true)
|
||||
if [ -n "${bad}" ]; then
|
||||
echo "::error::a BARRIER-PULLED marker outside CONTRACT-P5E §7's allowlist appeared on a run-ahead server. Each one is a field an apply still reads out of client memory:"
|
||||
echo "${bad}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# `rsp` = 0 ON UNBARRIERED RECORDS, in every summary that printed it. A non-zero count
|
||||
# would be an abort that did not fire, which is the one failure §3.3's unconditional
|
||||
# arm cannot report by itself.
|
||||
if grep -rhoE 'rsp=[0-9]+' MobileGL/MG_IntegrationTest/split-logs/ 2>/dev/null | grep -qvE 'rsp=0$'; then
|
||||
echo "::warning::a scenario summary reports a non-zero rsp; under P5e every remaining pull is a BARRIERED row on the allowlist above, and the count belongs in the trailing inventory rather than in this lane"
|
||||
fi
|
||||
echo "integration-split under strict is GREEN with only the §7 allowlist admitted ($(wc -l < "${RUNNER_TEMP}/markers.txt") distinct marker(s))"
|
||||
|
||||
- name: Upload strict lane logs
|
||||
if: always()
|
||||
|
||||
@@ -58,22 +58,50 @@ namespace MobileGL::MG_Pipe {
|
||||
// existing filter on Fatal{UnmigratedPipeInput still matches, plus the class and the
|
||||
// phase that retires it - a strict abort that did not say which phase owes the answer
|
||||
// would leave the reader exactly where the gate found them.
|
||||
[[noreturn]] void StrictBarrierPullFatal(MGPipeInputField field, MGPipeVerb verb) {
|
||||
// P5e (ra): `why` is the second half of the marker, because the lane's allowlist step
|
||||
// reads this line and the two reasons mean different things to it. "MOBILEGL_IPC_
|
||||
// STRICT_ERRORS=1" is the operator asking for a real, ordered value to be loud;
|
||||
// "UNBARRIERED" is the value not existing (§3.3). The `<field>@<verb>` prefix and the
|
||||
// Fatal{UnmigratedPipeInput tag are unchanged, so every filter written since P5c still
|
||||
// matches.
|
||||
[[noreturn]] void StrictBarrierPullFatal(MGPipeInputField field, MGPipeVerb verb,
|
||||
const char* why) {
|
||||
const SizeT index = static_cast<SizeT>(field);
|
||||
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \"%s@%s\"} [BARRIER-PULLED, "
|
||||
"MOBILEGL_IPC_STRICT_ERRORS=1, retires in %s]",
|
||||
kMGPipeInputFieldNames[index], MGPipeVerbName(verb), kMGPipeFieldRetiringPhase[index]);
|
||||
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \"%s@%s\"} [BARRIER-PULLED, %s, "
|
||||
"retires in %s]",
|
||||
kMGPipeInputFieldNames[index], MGPipeVerbName(verb), why,
|
||||
kMGPipeFieldRetiringPhase[index]);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// One place decides what a BARRIER-PULLED read does, so the field accessors and the
|
||||
// seven sticky forwards cannot drift apart on it.
|
||||
//
|
||||
// ---- P5e (ra), CONTRACT-P5E §3.3: THE DETECTOR IS UNCONDITIONAL UNDER AN UNBARRIERED
|
||||
// RECORD, AND THAT IS WHAT MAKES THE STRICT LANE A GATE -------------------------------
|
||||
//
|
||||
// The knob exists because under LOCKSTEP a pulled row is a real, ordered, fresh value:
|
||||
// the client filled it and then parked, so "count it and carry on" is an honest
|
||||
// measurement of remaining debt and MOBILEGL_IPC_STRICT_ERRORS is the operator asking
|
||||
// for the debt to be loud instead. None of that survives run-ahead. With the client
|
||||
// running ahead of this apply, the row was either never filled for this verb (§3.1
|
||||
// skips the fill) or is being overwritten by a verb two frames later - so the value is
|
||||
// torn or stale BY CONSTRUCTION and there is nothing for a counter to count. A
|
||||
// "count it" arm here would be a wrong picture with a number beside it.
|
||||
//
|
||||
// rsp is therefore 0 on unbarriered records in every scenario summary (§7's pin): a
|
||||
// non-zero count would be an abort that did not fire.
|
||||
void CountBarrierPull(MGPipeInputField field, MGPipeVerb verb) {
|
||||
if (!MGPipeApplierCurrentRecordIsBarriered()) {
|
||||
StrictBarrierPullFatal(field, verb, "UNBARRIERED, the client did not fill it");
|
||||
}
|
||||
++g_residualPulls;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ResidualPulls, 1);
|
||||
}
|
||||
if (MG_Config::Ipc.StrictErrors) StrictBarrierPullFatal(field, verb);
|
||||
if (MG_Config::Ipc.StrictErrors) {
|
||||
StrictBarrierPullFatal(field, verb, "MOBILEGL_IPC_STRICT_ERRORS=1");
|
||||
}
|
||||
}
|
||||
|
||||
// The verb's OWN may-read table (FillPoints.def, kMGPipeClassFieldMask). The stamp
|
||||
|
||||
@@ -108,7 +108,18 @@ DECLARE_GL_FUNCTION_HEAD(void, DrawArrays, GLenum mode, GLint first, GLsizei cou
|
||||
DECLARE_GL_FUNCTION_HEAD(void, DrawElements, GLenum mode, GLsizei count, GLenum type, const void* indices) DECLARE_GL_FUNCTION_END_NO_RETURN(void, DrawElements, mode, count, type, indices)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, Enable, GLenum cap) DECLARE_GL_FUNCTION_END_NO_RETURN(void, Enable, cap)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, EnableVertexAttribArray, GLuint index) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EnableVertexAttribArray, index)
|
||||
MOBILEGL_GL_API void glFinish() { MGLOG_D("Implementing function: %s(...)", __FUNCTION__); }
|
||||
// P5e (ra), CONTRACT-P5E §2.5. STILL NOTHING ON THE WIRE - Flush and Finish have no record
|
||||
// (ARCHITECTURE §11) and this stays a no-op on every monolith build. What it is not any more
|
||||
// is a no-op on a RUN-AHEAD client: "the commands issued so far have completed" is a promise a
|
||||
// queue the client never waits on can break, and this is the only place an application can ask
|
||||
// for it. Under lockstep the client had already waited out every command it issued, so the
|
||||
// call really did have nothing to do; MGPipeClientFinish answers false-cheap there too.
|
||||
MOBILEGL_GL_API void glFinish() {
|
||||
MGLOG_D("Implementing function: %s(...)", __FUNCTION__);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MobileGL::MG_Pipe::MGPipeClientFinish();
|
||||
#endif
|
||||
}
|
||||
MOBILEGL_GL_API void glFlush() { MGLOG_D("Implementing function: %s(...)", __FUNCTION__); }
|
||||
DECLARE_GL_FUNCTION_HEAD(void, FramebufferRenderbuffer, GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferRenderbuffer, target, attachment, renderbuffertarget, renderbuffer)
|
||||
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture2D, GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture2D, target, attachment, textarget, texture, level)
|
||||
|
||||
@@ -86,6 +86,20 @@ namespace MobileGL::MG_Pipe {
|
||||
return inputs.m_currentVertexAttribute;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5e (ra, CONTRACT-P5E §2.3). THE FOUR O-CLASS ROWS, AND ONLY THEY: these are the only
|
||||
// members of this block that own a frontend object rather than point into one, so they
|
||||
// are the only ones through which the apply thread can become a last owner. The raw
|
||||
// pointer rows (the binding-slot bases) are borrowed from a live GLContext and own
|
||||
// nothing, so releasing them would buy no lifetime and lose the monolith's arms.
|
||||
static void ReleaseObjectPins(PipeInputs& inputs) {
|
||||
inputs.m_boundVertexArray.reset();
|
||||
inputs.m_programForDispatch.reset();
|
||||
inputs.m_programForDraw.reset();
|
||||
inputs.m_transformFeedbackProgram.reset();
|
||||
}
|
||||
#endif
|
||||
|
||||
static void CopyField(PipeInputs& dst, GLContext& ctx, MGPipeInputField field) {
|
||||
using F = MGPipeInputField;
|
||||
using MG_State::GLState::BufferBindPointTargets;
|
||||
@@ -331,6 +345,72 @@ namespace MobileGL::MG_Pipe {
|
||||
namespace {
|
||||
GLContext* LiveContext() { return MG_State::pGLContext.get(); }
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ---- P5e (ra): the fill decision (CONTRACT-P5E §3.1) -----------------------------
|
||||
//
|
||||
// Is this process a run-ahead CLIENT right now? Three questions, cheapest first, and
|
||||
// the last one is the latch ClientSession took at its first caps adoption - so this is
|
||||
// one pointer test and one bool load on the verb path once the first two are constant.
|
||||
Bool ClientRunsAhead() {
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return false;
|
||||
if (MG_Remote::Client::RunsAsTheServerRole()) return false;
|
||||
const MG_Remote::Client::ClientSession* session = MG_Remote::Client::ClientSession::Active();
|
||||
return session != nullptr && session->RunAheadArmed();
|
||||
}
|
||||
|
||||
// THE WIRE OP A VERB BOUNDARY BECOMES, which is the join MGPipeVerbForWireOp draws in
|
||||
// the other direction. Built by walking the op space once at compile time rather than
|
||||
// written out, because a second hand-written table is a second thing to forget a row
|
||||
// in - and the generator already refuses a verb-shaped op with no row.
|
||||
constexpr MGPWireOp WireOpForVerb(MGPipeVerb verb) {
|
||||
for (SizeT i = 0; i < static_cast<SizeT>(MGPWireOp::kOpCount); ++i) {
|
||||
const auto op = static_cast<MGPWireOp>(i);
|
||||
if (MGPipeVerbForWireOp(op) == verb) return op;
|
||||
}
|
||||
return MGPWireOp::kOpCount;
|
||||
}
|
||||
|
||||
// CONTRACT-P5E §2.1's predicate, ASKED AT THE VALIDATE POINT - which is before the
|
||||
// record exists, so it is asked of the verb and the live context rather than of the
|
||||
// payload. The two halves must agree with MGPipeBarriered(op, payload, applierState),
|
||||
// which is what the server computes, so each clause is the same clause:
|
||||
//
|
||||
// 1. the static WaitClass column - the same generated table, same op;
|
||||
// 2. kCtxVerb inside an open XFB span - ctx.IsTransformFeedbackActive() here,
|
||||
// the applied MGPContextValues mirror there, and set_context_values precedes the
|
||||
// verb on the ring, so the two read the same value (§2.1);
|
||||
// 3. a draw carrying kDrawClientArrays - NOT asked here, because under run-ahead
|
||||
// such a draw never reaches a record at all: vi refuses it on this very thread in
|
||||
// EmitDrawRecord's array arm (§5.1, ruling 2). Asking it here would be a second
|
||||
// spelling of vi's "does any enabled attribute lack a buffer" walk, and two
|
||||
// spellings of an escalation is precisely what ruling 3 replaced.
|
||||
//
|
||||
// AN OP WITH NO VERB ROW ANSWERS BARRIERED. A verb the join does not know is one this
|
||||
// file cannot reason about, and the safe answer - fill it, wait for it - is also the
|
||||
// pre-P5e answer.
|
||||
// `ctx` MAY BE NULL, and that is not a convenience: the validate point asks this
|
||||
// question BEFORE it has decided whether there is anything to fill, so that the answer
|
||||
// is about the RECORD and not about the state of the block. A null context has no open
|
||||
// transform-feedback span, so clause 2 is false for it - and the verb is a no-op below
|
||||
// either way.
|
||||
Bool ClientVerbIsBarriered(MGPipeVerb verb, GLContext* ctx) {
|
||||
const MGPWireOp op = WireOpForVerb(verb);
|
||||
if (op == MGPWireOp::kOpCount) return true;
|
||||
if (MGPipeWaitClassFor(op) != kWaitNone) return true;
|
||||
if (MGPipeCallClassFor(op) == kCtxVerb && ctx != nullptr &&
|
||||
ctx->IsTransformFeedbackActive()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether the LAST fill actually happened, i.e. whether the rows in the block describe
|
||||
// the verb in flight. Read by MGPipeNoteFrontendMutation, which refreshes one field of
|
||||
// that fill: with no fill behind it there is nothing to refresh and the write would be
|
||||
// the role violation §3.5 names.
|
||||
Bool g_lastFillWasBarriered = true;
|
||||
#endif
|
||||
|
||||
template <class T>
|
||||
const SharedPtr<T>& NullShared() {
|
||||
static const SharedPtr<T> null;
|
||||
@@ -547,10 +627,18 @@ namespace MobileGL::MG_Pipe {
|
||||
void MGPipeNoteFrontendMutation(MGPipeInputField field) {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5e (ra, CONTRACT-P5E §3.1): THIS REFRESHES ONE FIELD OF THE LAST FILL, so with no
|
||||
// fill behind it there is nothing to refresh. Under run-ahead the last verb may have
|
||||
// been unbarriered, in which case the block describes an older verb the server is no
|
||||
// longer being asked about and a write here would be a GL-thread touch of server-role
|
||||
// memory. It returns instead - the same answer, one line earlier, as the class-mask
|
||||
// test below gives for a field the verb never pushed.
|
||||
if (!g_lastFillWasBarriered) return;
|
||||
// P5c (gt, layer 2): the single-field refresh is a client write into gPipeInputs too -
|
||||
// same gate as the fill.
|
||||
// same gate as the fill, and it makes the same claim the fill made: the rows it is
|
||||
// touching belong to a record this thread is parked behind (or will park behind).
|
||||
MG_Remote::Client::ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt(
|
||||
"MGPipeNoteFrontendMutation");
|
||||
"MGPipeNoteFrontendMutation", /*isBarrieredFill=*/true);
|
||||
#endif
|
||||
auto* ctx = LiveContext();
|
||||
if (ctx == nullptr) return;
|
||||
@@ -1000,6 +1088,29 @@ namespace MobileGL::MG_Pipe {
|
||||
Bool MGPipeDeferDestroyAndFreeIfOnApplyThread(MGPipeKind kind, Uint64 lifetimeId) {
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return false;
|
||||
if (!MG_Remote::Client::RunsAsTheServerRole()) return false;
|
||||
// P5e (ra), CONTRACT-P5E §2.7 / ruling 13. The queue stays - a BARRIERED record's apply
|
||||
// may still pin a frontend object (its fill's O-class rows, XFB's targets), so the
|
||||
// apply thread can still be a last owner and this is still the belt that keeps that
|
||||
// death off the client's allocator. But an enqueue from an UNBARRIERED record is a
|
||||
// FINDING, not a service: rule F says such an apply names no client memory at all, so
|
||||
// a SharedPtr it could be the last owner of means some site is still pinning a
|
||||
// frontend object across a record and the migration this phase believes it finished is
|
||||
// not finished. Named once with the kind so the site is findable, and Fatal under
|
||||
// strict so the lane owns the red rather than a log nobody reads.
|
||||
if (!MG_Pipe::MGPipeApplierCurrentRecordIsBarriered()) {
|
||||
if (MG_Config::Ipc.StrictErrors) {
|
||||
MGLOG_F("MGPipe: Fatal{RoleViolation, \"deferred-destroy\"} - an UNBARRIERED "
|
||||
"apply was the last owner of a frontend object of kind %u (lifetime "
|
||||
"%llu). CONTRACT-P5E rule F says an unbarriered apply reads no client "
|
||||
"memory, so nothing it touched should have been a SharedPtr at all",
|
||||
static_cast<unsigned>(kind), static_cast<unsigned long long>(lifetimeId));
|
||||
std::abort();
|
||||
}
|
||||
MGLOG_E_ONCE("MGPipe: an UNBARRIERED apply deferred the destruction of a frontend "
|
||||
"object of kind %u - CONTRACT-P5E §2.7's finding: some apply-thread "
|
||||
"site still holds a frontend SharedPtr across a record",
|
||||
static_cast<unsigned>(kind));
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_deferredDestroyMutex);
|
||||
g_deferredDestroys.push_back(MGPipeDeferredDestroy{kind, lifetimeId});
|
||||
@@ -1008,6 +1119,30 @@ namespace MobileGL::MG_Pipe {
|
||||
return true;
|
||||
}
|
||||
|
||||
// P5e (ra), CONTRACT-P5E §2.3. Declared in PipeMutation.h, where its WHY is argued.
|
||||
//
|
||||
// NO GUARD ON THE CALLER'S BEHALF: ClientSession calls this only after a barriered apply
|
||||
// has returned on a run-ahead session, and adding a second "is run-ahead armed" test here
|
||||
// would be a copy of a decision that belongs on the other side. What this side owns is
|
||||
// WHICH rows go, and that answer is ReleaseObjectPins' four.
|
||||
//
|
||||
// THE STAMPS ARE LEFT ALONE, deliberately. A released row reads back as null, and a
|
||||
// BARRIERED verb's fill re-copies it before that verb's apply can pull it (§3.1 skips the
|
||||
// fill only for UNBARRIERED records); an unbarriered apply may not read it at all, and
|
||||
// §3.3's detector is what says so by name. Clearing the stamps as well would trade that
|
||||
// named abort for the poison Fatal, which names the field but not the rule it broke.
|
||||
void MGPipeReleaseResidualFillPins() { MGPipeFillAccess::ReleaseObjectPins(gPipeInputs); }
|
||||
|
||||
// P5e (ra), CONTRACT-P5E §2.5. Declared in PipeMutation.h; see there for why it is not a
|
||||
// ClientSession call at the GL entry point.
|
||||
void MGPipeClientFinish() {
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
|
||||
if (MG_Remote::Client::RunsAsTheServerRole()) return;
|
||||
if (MG_Remote::Client::ClientSession* session = MG_Remote::Client::ClientSession::Active()) {
|
||||
session->Finish();
|
||||
}
|
||||
}
|
||||
|
||||
void MGPipeDrainDeferredDestroys() {
|
||||
if (g_deferredDestroyCount.load(std::memory_order_acquire) == 0) return;
|
||||
Vector<MGPipeDeferredDestroy> drained;
|
||||
@@ -1986,9 +2121,20 @@ 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");
|
||||
// P5e (ra, §3.1): a verb whose fill was skipped has no stamps of this thread's to
|
||||
// retire, and the bump below would move a serial the SERVER's stamp owns. Leave
|
||||
// returns, and the tracker's base-instance clear - which is frontend state, not block
|
||||
// state - still runs at the bottom.
|
||||
if (g_lastFillWasBarriered) {
|
||||
// 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", /*isBarrieredFill=*/true);
|
||||
}
|
||||
if (!g_lastFillWasBarriered) {
|
||||
MGPipeTrackerInstance().ClearPendingBaseInstance();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
// Same bump the next fill would make, without a verb to fill from: no field is
|
||||
@@ -2727,6 +2873,18 @@ namespace MobileGL::MG_Pipe {
|
||||
// Did the applier reproduce it? Byte for byte, over the attributes this call
|
||||
// named - anything less would be a mirror that disagrees with the frontend in a
|
||||
// window no gate looks at.
|
||||
//
|
||||
// P5e (ra, CONTRACT-P5E §3.4): NOT UNDER RUN-AHEAD. The mirror is the APPLIER's
|
||||
// copy of this record, and set_vertex_attrib_defaults is a kWaitNone row - so
|
||||
// under run-ahead this thread has not waited for the apply and the read races it,
|
||||
// and the repair below (a CopyField straight into the block) is precisely the
|
||||
// GL-thread write §3.5 refuses. The client's own authority is `resolved` and
|
||||
// `g_attribDefaultLastHeader`, which is what it just published; there is nothing
|
||||
// the mirror could add that the wire does not already carry. A build that ever
|
||||
// needs the repair arm again has to earn it with a barriered row.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (ClientRunsAhead()) return sizeof(MGPVertexAttribDefaults) + header.Count * sizeof(MGPAttribValue);
|
||||
#endif
|
||||
const auto* mirror = MGPipeFillAccess::VertexAttribDefaultsOf(gPipeInputs);
|
||||
Bool reproduced = true;
|
||||
for (SizeT i = 0; i < kAttribs && reproduced; ++i) {
|
||||
@@ -3061,13 +3219,20 @@ namespace MobileGL::MG_Pipe {
|
||||
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");
|
||||
// ---- P5e (ra), CONTRACT-P5E §3: WHO OWNS gPipeInputs FOR THIS VERB ----------------
|
||||
//
|
||||
// Under run-ahead the block is SERVER-ROLE MEMORY for an unbarriered record: the
|
||||
// client does not fill it, does not stamp it and does not withdraw the server's stamp,
|
||||
// because it will not be parked while the apply runs and every one of those writes
|
||||
// would race the applier's own reads. What still runs, unchanged, is the tracker walk
|
||||
// and the emitters (steps 2 and 3): those PRODUCE RECORDS, which is the whole of what
|
||||
// an unbarriered verb is allowed to hand the server.
|
||||
//
|
||||
// `fillOwed` is deliberately a separate name from `barriered`, and that is what makes
|
||||
// the red-once one line: setting it to `true` restores the old behaviour (a fill for
|
||||
// every verb) and the guard below then fires Fatal{RoleViolation, "gPipeInputs"} on
|
||||
// the first unbarriered verb, by name.
|
||||
const Bool runAhead = ClientRunsAhead();
|
||||
#endif
|
||||
ParsePoisonOmissionKnob();
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
@@ -3080,24 +3245,63 @@ namespace MobileGL::MG_Pipe {
|
||||
"(configure with -DMOBILEGL_PIPE_VERIFY=ON)");
|
||||
}
|
||||
#endif
|
||||
auto* ctx = LiveContext();
|
||||
// THE IDENTITY / LIVENESS PAIR IS WRITTEN ON EVERY VERB, run-ahead or not, and that is
|
||||
// a NAMED DEVIATION from CONTRACT-P5E §3.1's list (see the report): `m_live` and
|
||||
// `m_contextIdentity` are not FIELDS of the residual model - no FieldOwnership.def row
|
||||
// owns them, no applier record carries them, and no server stamp can answer them -
|
||||
// they are "does this process still have a GL context", which every null-context guard
|
||||
// in the backend reads and which only this thread can know. Two stores, no freshness
|
||||
// and no stamp, so an unbarriered apply reading them reads a fact about the client's
|
||||
// process rather than a value the wire owes it.
|
||||
MGPipeFillAccess::SetIdentity(inputs, ctx);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// §2.1's predicate, client-side (see ClientVerbIsBarriered).
|
||||
const Bool barriered = !runAhead || ClientVerbIsBarriered(verb, ctx);
|
||||
// THE ONE LINE THE RED-ONCE FLIPS: `= true` here is "fill for every verb", the
|
||||
// pre-P5e behaviour, and it turns the guard below into the abort §3.5 names.
|
||||
const Bool fillOwed = barriered;
|
||||
g_lastFillWasBarriered = fillOwed;
|
||||
if (fillOwed) {
|
||||
// P5c (gt, CONTRACT-P5C §6 layer 2) / P5e §3.5: the residual fill is THE
|
||||
// client-side write into gPipeInputs. Under lockstep it is legal because it runs
|
||||
// before the record is published - the apply thread's in-applier flag is provably
|
||||
// down here. Under run-ahead it is legal because this record is BARRIERED: this
|
||||
// thread is about to park behind it. The second argument is which of the two
|
||||
// claims the caller is making, and a fill that made the second one falsely is the
|
||||
// named abort.
|
||||
MG_Remote::Client::ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt(
|
||||
"MGPipeValidateForVerb", /*isBarrieredFill=*/barriered);
|
||||
}
|
||||
#else
|
||||
constexpr Bool fillOwed = true;
|
||||
#endif
|
||||
if (fillOwed) {
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
MGPipeFilledState& filled = MGPipeFillAccess::Filled(inputs);
|
||||
// Starts at 1: FilledGen == 0 is "never filled", and MGPipeInputFieldIsFresh refuses
|
||||
// it on both branches, so a read before this first bump is
|
||||
// Fatal{UnmigratedPipeInput, "<Field>@<none>"} rather than default storage.
|
||||
++filled.CurrentVerbSerial;
|
||||
// Starts at 1: FilledGen == 0 is "never filled", and MGPipeInputFieldIsFresh
|
||||
// refuses it on both branches, so a read before this first bump is
|
||||
// Fatal{UnmigratedPipeInput, "<Field>@<none>"} rather than default storage.
|
||||
++MGPipeFillAccess::Filled(inputs).CurrentVerbSerial;
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The client is filling, so whatever the server stamped at its last verb boundary is
|
||||
// withdrawn: the stamps below are the CLIENT's again and a stale read is a defect, not
|
||||
// a residual pull. Disarming here rather than at the end of the applier's work is what
|
||||
// makes the arming flag say "the current stamps are the server's" no matter which of
|
||||
// the two roles ran last.
|
||||
MGPipeServerClearVerbBoundary();
|
||||
// The client is filling, so whatever the server stamped at its last verb boundary
|
||||
// is withdrawn: the stamps below are the CLIENT's again and a stale read is a
|
||||
// defect, not a residual pull. Disarming here rather than at the end of the
|
||||
// applier's work is what makes the arming flag say "the current stamps are the
|
||||
// server's" no matter which of the two roles ran last.
|
||||
//
|
||||
// P5e (§3.2): under run-ahead this runs only inside a barriered fill, which is the
|
||||
// whole of the E note's hazard (b.4) - a GL thread withdrawing the SERVER's stamp
|
||||
// while the apply thread is inside a record that depends on it. For an unbarriered
|
||||
// verb the stamp is not touched at all, and the applier's own LeaveApplier is then
|
||||
// its only writer.
|
||||
MGPipeServerClearVerbBoundary();
|
||||
#endif
|
||||
MGPipeFillAccess::SetVerb(inputs, verb);
|
||||
}
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
MGPipeFilledState& filled = MGPipeFillAccess::Filled(inputs);
|
||||
#endif
|
||||
MGPipeFillAccess::SetVerb(inputs, verb);
|
||||
auto* ctx = LiveContext();
|
||||
MGPipeFillAccess::SetIdentity(inputs, ctx);
|
||||
if (ctx == nullptr) {
|
||||
// The pending base instance belongs to THIS verb, and this exit skips step 3's
|
||||
// clear, so it has to make the same promise here: a base-instanced draw with no
|
||||
@@ -3373,12 +3577,20 @@ namespace MobileGL::MG_Pipe {
|
||||
// does read. The two halves of P5c rv's gate still read the ONE `contextValuesWireLive`
|
||||
// computed at the top of this function - it is the memo's key AND its argument - so they
|
||||
// cannot disagree about who supplies the eight value-class fields.
|
||||
//
|
||||
// P5e (ra, §3.1): AND IT DOES NOT RUN AT ALL FOR AN UNBARRIERED RECORD. Every write
|
||||
// below - the CopyField and the FilledGen stamp alike - is a GL-thread write into a
|
||||
// block the apply thread is about to read without this thread being parked, which is
|
||||
// exactly what rule F forbids. The server does not go without an answer: it stamps its
|
||||
// own boundary and §3.3's detector turns any field it still needs from here into
|
||||
// Fatal{UnmigratedPipeInput, "<field>@<verb>"} by name, which is what makes the strict
|
||||
// lane a gate rather than a count.
|
||||
const Bool applierDerives = ApplierDerivesRenderStateFields();
|
||||
// BY VALUE, NOT BY REFERENCE: the memo's storage is rebuilt in place when the key moves,
|
||||
// and the walk below holds this across 63 iterations.
|
||||
const MGPipeFieldMask supplied =
|
||||
SuppliedFieldMask(pushMask, applierDerives, contextValuesWireLive);
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
for (SizeT i = 0; fillOwed && i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
if (!MGPipeFieldMaskHas(mask, field)) continue;
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
|
||||
@@ -953,9 +953,19 @@ namespace MobileGL::MG_Pipe {
|
||||
// EmitAndWaitTails the client still blocks after every record, so the answer the server
|
||||
// must stamp is `true` whatever the wire says - the two named exemption scopes (§4.4) are
|
||||
// legal exactly while the client is parked, and it is parked behind all of them today.
|
||||
// Package ra flips this to true in the same commit that lands the wait rule; the sink's
|
||||
// stamp then follows the predicate. Computing the predicate anyway (PipeApplier::ApplyOne)
|
||||
// is deliberate: it keeps the function exercised on every record for the whole phase.
|
||||
// IT FLIPS WITH kMGPipeP5eRunAheadReady, IN THE INTEGRATION COMMIT, AND NOT WHEN ra LANDS
|
||||
// (ra's amendment to ID-103's wording, which said "the same commit that lands the wait
|
||||
// rule"). The argument ID-103 makes is about whether the CLIENT is parked, not about
|
||||
// whether ra's code exists: RunAheadArmed() is a conjunction whose third term is the
|
||||
// server's caps bit, so between ra landing and the caps bit being published the client
|
||||
// still blocks after every record - and stamping `false` there would withdraw §4.4's
|
||||
// exemptions from probes that client's own wait still makes safe, which is exactly the
|
||||
// "refusal with no defect behind it" ID-103 refused. The two constants are therefore ONE
|
||||
// switch with two spellings, and the integration commit throws both.
|
||||
//
|
||||
// Computing the predicate anyway (PipeApplier::ApplyOne) is deliberate: it keeps the
|
||||
// function exercised on every record for the whole phase rather than first run on the day
|
||||
// it starts deciding.
|
||||
inline constexpr Bool kMGPipeP5eClientWaitRuleLanded = false;
|
||||
|
||||
// P5c (rv): the two serials the PipeInputs texture-shutter accessors answer with under a
|
||||
|
||||
@@ -231,8 +231,38 @@ namespace MobileGL::MG_Pipe {
|
||||
// BeforeReadOnlyVerb) through MGPipeDrainDeferredDestroys. A delayed free is safe:
|
||||
// slots are plentiful, reuse is delayed rather than corrupted, and the replay runs the
|
||||
// helper itself, so every helper's own three-step order is untouched.
|
||||
// P5e (ra), CONTRACT-P5E §2.7 / ruling 13 (ID-91): THE QUEUE STAYS LIVE, AND AN
|
||||
// UNBARRIERED ENQUEUE IS A FINDING. After the family packages land, an unbarriered apply
|
||||
// holds no frontend SharedPtr at all - every object is resolved from a handle - so the
|
||||
// only apply thread that can still be a last owner is one inside a BARRIERED record (a
|
||||
// fill's O-class rows, XFB's pinned targets). An enqueue from an unbarriered record
|
||||
// therefore means some site still pins a frontend object across a record, which is a
|
||||
// migration this phase believes it finished: it logs once with the kind, and is Fatal
|
||||
// under MOBILEGL_IPC_STRICT_ERRORS so the strict lane owns the red. The queue itself is
|
||||
// NOT removed - it is the belt that keeps the barriered case from corrupting the
|
||||
// allocator while the finding is being read.
|
||||
Bool MGPipeDeferDestroyAndFreeIfOnApplyThread(MGPipeKind kind, Uint64 lifetimeId);
|
||||
void MGPipeDrainDeferredDestroys();
|
||||
|
||||
// P5e (ra), CONTRACT-P5E §2.3: drop the four O-class SharedPtr rows the residual fill left
|
||||
// in gPipeInputs (the bound VAO and the three programs, PipeInputs.h's O block). Called by
|
||||
// ClientSession on the GL thread once a BARRIERED apply has returned - the one instant
|
||||
// under run-ahead at which the applier is provably idle and this block has no other
|
||||
// reader. Under lockstep nothing calls it: the next verb's fill overwrites those rows
|
||||
// within a verb, and dropping them early would only cost a re-pin.
|
||||
//
|
||||
// It is HERE rather than in ClientSession because the rows are PipeInputs' private storage
|
||||
// and MGPipeFillAccess - the client-side door to it - lives in MG_Impl (PipeFill.cpp).
|
||||
// MG_Remote says when; MG_Impl says what.
|
||||
void MGPipeReleaseResidualFillPins();
|
||||
|
||||
// P5e (ra), CONTRACT-P5E §2.5: glFinish's whole body under a transport. Declared here
|
||||
// rather than on ClientSession so that MG_Impl's GL entry points - which are BELOW
|
||||
// MG_Remote - can reach it without learning that a session exists; the implementation in
|
||||
// PipeFill.cpp is one forward to ClientSession::Finish. A no-op with no live session, on a
|
||||
// monolith transport, and on a lockstep one (there the client waited out every command it
|
||||
// issued before it could reach the call).
|
||||
void MGPipeClientFinish();
|
||||
#endif
|
||||
|
||||
void MGPipeEmitResourceCreate(MG_State::GLState::BufferObject& buffer);
|
||||
|
||||
@@ -287,12 +287,41 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
|
||||
// ---- resources ---------------------------------------------------------------------
|
||||
|
||||
// P5e (ra), CONTRACT-P5E §2.5 / ruling 15 (ID-93): DOES THIS SUB-DATA RECORD WANT ITS
|
||||
// ANSWER? One predicate, two callers - this route and the wire emit table - because the
|
||||
// route cannot pass a flag through the thunk (the call table's signature is the
|
||||
// catalogue's) and two hand-written copies of "is this the buffer half" is exactly the
|
||||
// drift that would make the client wait for an answer the emitter told the server not to
|
||||
// bother with, or read a slot that was never posted.
|
||||
//
|
||||
// THE BUFFER HALF DOES NOT: its Bool is discarded at the only call site there is
|
||||
// (MGPipeEmitResourceSubData), and because the row carries kReplySlot the client used to
|
||||
// wait for it anyway - one full round trip per 64 KB persistent-map block, whose answer
|
||||
// nobody looked at. THE TEXTURE HALF DOES: DrainTextureSubData clears the level's dirty
|
||||
// flag on an ACCEPTED reply, so its answer is load-bearing (D-D5; making that half
|
||||
// fire-and-forget is a trailing item, not this package's).
|
||||
//
|
||||
// The test is MGPSubData::Target == kMGPipeResourceTargetBuffer, whole field, which is the
|
||||
// invariant MGPipeTypes.h asserts beside the packer and which the applier's own
|
||||
// SubDataNamesABuffer already reads.
|
||||
inline Bool MGPipeSubDataWantsItsReply(const MGPSubData& record) {
|
||||
return record.Target != kMGPipeResourceTargetBuffer;
|
||||
}
|
||||
|
||||
inline Bool MGPipeRouteResourceSubData(const MGPSubData& record, const void* bytes,
|
||||
Uint64 byteCount,
|
||||
const MGPSubRegion* regions = nullptr) {
|
||||
// THE SLOT IS STILL MINTED AND STILL TAKEN, even for the buffer half. The row keeps
|
||||
// kReplySlot on the wire, the server still posts, and the mailbox's "every minted slot
|
||||
// is taken" rule is what the next row's ReplyOverrun Fatal is looking for - so the
|
||||
// half that does not WAIT still has to collect. What changes is that under run-ahead
|
||||
// the emit table publishes and returns, and the answer it collects is the emitter's
|
||||
// accept-by-construction rather than the server's.
|
||||
MGPReplySlot reply = MGPipeMintReplySlot();
|
||||
MGP_ResourceSubData(&record, bytes, byteCount, regions, record.RegionCount, &reply);
|
||||
return MGPipeTakeReplyBool(reply, "resource_subdata");
|
||||
const Bool accepted = MGPipeTakeReplyBool(reply, "resource_subdata");
|
||||
return MGPipeSubDataWantsItsReply(record) ? accepted : true;
|
||||
}
|
||||
inline void MGPipeRouteBufferSubDataResident(const MGPSubData& record, const void* bytes,
|
||||
Uint64 byteCount) {
|
||||
|
||||
@@ -172,6 +172,27 @@ namespace MobileGL::MG_Remote::Client {
|
||||
return CapsMirrorInstance().Backend();
|
||||
}
|
||||
|
||||
namespace {
|
||||
// P5e (ra), CONTRACT-P5E §2.5: THE WAIT BEFORE EVERY `Server*` EGL FORWARDER.
|
||||
//
|
||||
// These calls do not travel on SEG_CMD. They go through ServerLoop's control mailbox,
|
||||
// which is pumped BETWEEN drain batches (ServerLoop.cpp's PumpControlRequest, above
|
||||
// DrainRing) - so a make-current, a surface creation or a resize can land between two
|
||||
// records the client published and never waited for. Under lockstep that was
|
||||
// impossible: the client had waited out every record it issued before it could reach
|
||||
// this line. Under run-ahead it is one queued frame wide, and a context switch applied
|
||||
// in the middle of another context's draws is not a wrong pixel, it is a wrong
|
||||
// everything.
|
||||
//
|
||||
// ServerSwapEGLBuffers is deliberately NOT on this list, and its own virtual says why:
|
||||
// present IS the swap and it travels as a record, in order, with its own credit.
|
||||
void WaitForApplyBeforeEglForwarder(const char* forwarder) {
|
||||
if (ClientSession* session = ClientSession::Active()) {
|
||||
session->WaitForApplyToCatchUp(forwarder);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ---- the nine EGL lifecycle virtuals ---------------------------------------------------
|
||||
//
|
||||
// FORWARD FIRST, THEN RUN THE BASE. The server has to own the context before the client's
|
||||
@@ -180,6 +201,7 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// InitCapabilities has run.
|
||||
|
||||
Bool BackendObject_Remote::InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) {
|
||||
WaitForApplyBeforeEglForwarder("InitializeEGLDisplay");
|
||||
if (!Server::ServerInitializeEGLDisplay(dpy, major, minor)) return false;
|
||||
return MG_Backend::BackendObject::InitializeEGLDisplay(dpy, major, minor);
|
||||
}
|
||||
@@ -188,6 +210,7 @@ namespace MobileGL::MG_Remote::Client {
|
||||
const MG_Backend::WindowHandle& handle) {
|
||||
// The handle first: the server's backend has to know which window it is about to make
|
||||
// a surface for, and ServerSetWindowHandle is the only way to tell it.
|
||||
WaitForApplyBeforeEglForwarder("CreateEGLWindowSurface");
|
||||
Server::ServerSetWindowHandle(handle);
|
||||
if (!Server::ServerCreateEGLWindowSurface(surface, handle)) return false;
|
||||
// The server's surface init published the default framebuffer's shape as a
|
||||
@@ -202,6 +225,7 @@ namespace MobileGL::MG_Remote::Client {
|
||||
}
|
||||
|
||||
Bool BackendObject_Remote::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
WaitForApplyBeforeEglForwarder("ResizeEGLWindowSurface");
|
||||
if (!Server::ServerResizeEGLWindowSurface(surface, width, height)) return false;
|
||||
// A resize re-creates the server's swapchain, which re-posts the surface-changed
|
||||
// event - same drain, same reason as CreateEGLWindowSurface.
|
||||
@@ -210,6 +234,7 @@ namespace MobileGL::MG_Remote::Client {
|
||||
}
|
||||
|
||||
Bool BackendObject_Remote::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
|
||||
WaitForApplyBeforeEglForwarder("CreateEGLPbufferSurface");
|
||||
if (!Server::ServerCreateEGLPbufferSurface(surface, width, height)) return false;
|
||||
// Same drain as the window surface: InitPbufferSurface publishes the default
|
||||
// framebuffer's depth/stencil format on SEG_EVENT from inside this very RPC.
|
||||
@@ -223,6 +248,7 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// class latches "the surface is initialised" and calls InitCapabilities, because
|
||||
// InitCapabilities' answer comes from a snapshot the server can only publish once its
|
||||
// own InitCapabilities has run - and ServerMakeEGLCurrent is what publishes it.
|
||||
WaitForApplyBeforeEglForwarder("MakeEGLCurrent");
|
||||
if (!Server::ServerMakeEGLCurrent(dpy, draw, read, ctx)) return false;
|
||||
if (!MG_Backend::BackendObject::MakeEGLCurrent(dpy, draw, read, ctx)) return false;
|
||||
|
||||
|
||||
@@ -139,7 +139,12 @@ namespace MobileGL::MG_Remote::Client {
|
||||
return timeoutNs / 1000000 + (timeoutNs % 1000000 != 0) + kBarrierTimeoutMs;
|
||||
}
|
||||
|
||||
// Defined below; declared here because the applied wait has to be able to drain
|
||||
// SEG_EVENT while it is parked (P5e §2.6).
|
||||
Uint32 DrainEventRing(Transport::EventRingConsumer& events);
|
||||
|
||||
Transport::SessionWait WaitForAppliedBudget(Transport::SessionProducer& producer,
|
||||
Transport::EventRingConsumer& events,
|
||||
Uint64 seq, Uint64 remainingMs) {
|
||||
// The transport takes Uint32 milliseconds with UINT32_MAX meaning forever.
|
||||
// Keep even the largest GL timeout finite, using bounded chunks and preserving
|
||||
@@ -148,7 +153,23 @@ namespace MobileGL::MG_Remote::Client {
|
||||
for (;;) {
|
||||
const Uint32 chunkMs = remainingMs > kMaxFiniteWaitMs
|
||||
? kMaxFiniteWaitMs : static_cast<Uint32>(remainingMs);
|
||||
const auto wait = producer.WaitForApplied(seq, chunkMs);
|
||||
// P5e (ra, CONTRACT-P5E §2.6): THE PARK BREAKS ON TWO THINGS. A server that
|
||||
// ran out of SEG_EVENT stops producing and therefore stops applying, so a
|
||||
// waiter that could only be woken by appliedSeq would wait for a watermark
|
||||
// nothing is going to move - and it is holding the only drain there is. The
|
||||
// second exit is that case: drain, clear the latch, ring the server, park
|
||||
// again. `remainingMs` is deliberately NOT charged for it; a wake that did
|
||||
// the queue's work is not the caller's budget being spent.
|
||||
const auto wait = producer.WaitForAppliedOrEventBacklog(seq, chunkMs);
|
||||
if (wait == Transport::SessionWait::Reached) {
|
||||
Transport::RingControl* control = producer.Control();
|
||||
if (control == nullptr ||
|
||||
Transport::Watermark::Reached(control->appliedSeq, seq)) {
|
||||
return wait;
|
||||
}
|
||||
DrainEventRing(events);
|
||||
continue;
|
||||
}
|
||||
if (wait != Transport::SessionWait::TimedOut || remainingMs <= chunkMs) return wait;
|
||||
remainingMs -= chunkMs;
|
||||
}
|
||||
@@ -592,6 +613,9 @@ namespace MobileGL::MG_Remote::Client {
|
||||
m_events = Transport::EventRingConsumer(m_shm.EventControl(), control,
|
||||
m_shm.EventRingBase(), m_shm.EventRingCapacity(),
|
||||
m_shm.EventSegmentBase());
|
||||
// P5e (ra, §2.6): the drain's other half. A server parked on a full SEG_EVENT is woken
|
||||
// by the drain that emptied it, not by the next thing this client happens to publish.
|
||||
m_events.SetServerDoorbell(&m_clientTransport->PeerDoorbell());
|
||||
if (!m_replies.Valid() || !m_events.Valid()) {
|
||||
Stop();
|
||||
return MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
@@ -629,6 +653,10 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// half-wired shape this session exists not to have.
|
||||
m_encoder = Wire::PipeWireEncoder(control, &m_cmd, nullptr, &m_segments);
|
||||
m_encoder.SetStageRetirementDoorbell(m_producer.SelfDoorbell());
|
||||
// P5e (ra, §2.6): the stage bell is a client wait and every client wait drains. The
|
||||
// encoder owns the park; the session owns SEG_EVENT, so the session lends it a drain.
|
||||
m_encoder.SetStageWaitHook(
|
||||
+[](void* self) { DrainEventRing(static_cast<ClientSession*>(self)->Events()); }, this);
|
||||
|
||||
// ---- 7. the first CapsSnapshot, if the server had a backend to publish one from.
|
||||
// ONE DRAIN, ONE ADOPTER (c1): PumpControlPlane below is the only thing in the client
|
||||
@@ -774,6 +802,14 @@ namespace MobileGL::MG_Remote::Client {
|
||||
m_serverTransport.reset();
|
||||
m_transport = nullptr;
|
||||
m_started = false;
|
||||
// P5e (ra, §1): the LATCH GOES DOWN WITH THE SESSION. It is "what the first caps
|
||||
// adoption of THIS session said", and a session that starts again - the same singleton,
|
||||
// a second Start - must take it again from that session's own first snapshot rather
|
||||
// than inherit a verdict about a server that is gone. The present serial goes with it
|
||||
// for the same reason: the credit's id space belongs to the session that paces on it.
|
||||
m_runAheadArmed = false;
|
||||
m_runAheadLatched = false;
|
||||
m_presentsSent = 0;
|
||||
if (g_active == this) {
|
||||
g_active = nullptr;
|
||||
}
|
||||
@@ -814,7 +850,8 @@ namespace MobileGL::MG_Remote::Client {
|
||||
Uint64 ClientSession::EmitAndWaitTails(MG_Pipe::MGPWireOp op, const void* payload,
|
||||
Uint64 payloadBytes, const Wire::WireTail* tails,
|
||||
Uint32 tailCount, void* replyOut, Uint64 replyBytes,
|
||||
Int32* statusOut, Uint64* replySizeOut) {
|
||||
Int32* statusOut, Uint64* replySizeOut,
|
||||
Bool wantReply) {
|
||||
Uint64 varTailBytes = 0;
|
||||
for (Uint32 i = 0; i < tailCount; ++i) varTailBytes += tails[i].Size;
|
||||
if (statusOut != nullptr) *statusOut = Wire::ReplySink::kStatusError;
|
||||
@@ -863,6 +900,12 @@ namespace MobileGL::MG_Remote::Client {
|
||||
wait == Transport::SessionWait::ShutDown ? "shutdown" : "timeout");
|
||||
std::abort();
|
||||
}
|
||||
// P5e (ra, §2.6): EVERY WAIT EXIT DRAINS. The cmd-space wait is one of the two
|
||||
// safety nets under run-ahead (the stage bell is the other), and a client parked
|
||||
// here without draining is the other half of the flow-control deadlock: the server
|
||||
// stops applying on a full event ring, so retiredSeq stops moving, so this wait
|
||||
// never completes. One drain on the way out closes it.
|
||||
DrainEventRing(m_events);
|
||||
seq = m_encoder.EncodeRecord(op, payload, payloadBytes, tails, tailCount);
|
||||
if (seq == Wire::kInvalidSeq) {
|
||||
MGLOG_F("MGPipe: Fatal{RingOverrun, \"SEG_CMD\"} - %s refused after sufficient "
|
||||
@@ -880,8 +923,63 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// (R-16 / ID-31) - fourteen rows now, because the four Bool acceptance entry points
|
||||
// gained the flag. Asking the catalogue rather than the caller is what stops a caller
|
||||
// that forgot to pass a buffer from silently turning an answer into a guess.
|
||||
const Bool ownsReplySlot =
|
||||
//
|
||||
// P5e (ra) ADDS ONE CALLER-SIDE CONJUNCT AND NO SECOND SOURCE OF TRUTH: `wantReply`.
|
||||
// The catalogue still says which rows CARRY a slot; a caller that passes false says it
|
||||
// will not read this one (resource_subdata's buffer half, §2.5 - the only such caller).
|
||||
// The server posts either way, so the wire is unchanged and the row keeps its flag.
|
||||
const Bool rowCarriesReplySlot =
|
||||
(MG_Pipe::MGPipeCallFlagsFor(op) & static_cast<Uint32>(MG_Pipe::kReplySlot)) != 0;
|
||||
const Bool ownsReplySlot = rowCarriesReplySlot && wantReply;
|
||||
|
||||
// ---- THE P5e WAIT RULE (CONTRACT-P5E §2.3) ---------------------------------------
|
||||
//
|
||||
// IT STANDS BEFORE THE BATCH SKIP AND REPLACES IT WHOLE, rather than adding a clause to
|
||||
// it. The batch skip is a rule about CALL CLASSES ("a set/CSO/object record carries its
|
||||
// whole value in the payload"); the wait class is a rule about WHAT AN APPLY READS, and
|
||||
// it is generated from the same PipeCalls.def row the class comes from
|
||||
// (MGPipeWaitClassFor, c0e). Two rules layered would mean a row could be skipped by the
|
||||
// class rule and waited by the column, and the column is the one the SERVER's
|
||||
// MGPipeBarriered reads - so the two halves of the wire would disagree about whether
|
||||
// the client is parked behind this record, which is the single fact §4.4's exemptions
|
||||
// and §3.3's detector both rest on.
|
||||
//
|
||||
// WITH RUN-AHEAD DISARMED NOTHING BELOW RUNS and the file's original shape continues -
|
||||
// Magma, a monolith transport, MOBILEGL_IPC_RUN_AHEAD=0 and every build before the
|
||||
// integration commit take the old path byte for byte, which is what makes RUN_AHEAD=0
|
||||
// a pure wait-rule A/B on identical server code (§5.8).
|
||||
if (RunAheadArmed() && !ownsReplySlot) {
|
||||
MG_Pipe::MGPipeWaitClass waitClass = MG_Pipe::MGPipeWaitClassFor(op);
|
||||
// §2.5's named relaxation, and the ONLY place a static column answer is overridden:
|
||||
// a kWaitReply row whose caller does not want the answer has nothing to wait FOR.
|
||||
// It is spelled here rather than in the column because the column is per-op and
|
||||
// this is per-call - the texture half of the same row still waits.
|
||||
if (!wantReply && waitClass == MG_Pipe::kWaitReply) waitClass = MG_Pipe::kWaitNone;
|
||||
switch (waitClass) {
|
||||
case MG_Pipe::kWaitNone:
|
||||
// Published and gone. Rule F is what makes this legal: the apply of an
|
||||
// unbarriered record names no client memory at all (§3, §4, §5).
|
||||
return seq;
|
||||
case MG_Pipe::kWaitPresent:
|
||||
// The credit wait already ran, in EmitPresent, BEFORE the encode (§2.4). There
|
||||
// is nothing left to do here and waiting would turn the credit into a barrier.
|
||||
return seq;
|
||||
case MG_Pipe::kWaitApplied:
|
||||
// §2.2's barriered rows: applier_reset, generate_mipmap (until tx2 lands its
|
||||
// VerbMipRes arm), set_storage_block_binding, the five XFB span verbs and the
|
||||
// CopyTex endpoint. Each still pulls a row or probes a registry, and the
|
||||
// client's wait is exactly what keeps P5C's semantics legal for it (ruling 4).
|
||||
// Falls through to the wait below.
|
||||
break;
|
||||
case MG_Pipe::kWaitReply:
|
||||
case MG_Pipe::kWaitClassCount:
|
||||
// Unreachable: `ownsReplySlot` is false here, and gen_pipe refuses a kWaitReply
|
||||
// row without a slot and a kReplySlot row that does not wait (c0e's three
|
||||
// negative controls). Treated as a wait rather than as a skip, because the
|
||||
// wrong answer to "does this hang" is the recoverable one.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// MOBILEGL_IPC_BATCH_WAITS (default 1): a value-class record is published WITHOUT
|
||||
// waiting for its own apply. The barrier's one load-bearing reader is the backend
|
||||
@@ -912,7 +1010,13 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// (set_texture_params, resource_subdata, resource_readback and get_texture_image
|
||||
// own reply slots and wait anyway; the resource_* transfers and object_death resolve
|
||||
// by handle).
|
||||
if (!ownsReplySlot && MG_Config::Ipc.BatchWaits != 0 &&
|
||||
//
|
||||
// AND IT IS DISARMED WHOLE UNDER RUN-AHEAD (P5e §2.3): the wait class above is the
|
||||
// rule there, and a second skip rule reading a different column is how the two halves
|
||||
// of the wire end up disagreeing about whether the client is parked. Written as a
|
||||
// conjunct here rather than left to "the classes happen not to overlap", because the
|
||||
// classes overlapping is one PipeCalls.def row away at any time.
|
||||
if (!ownsReplySlot && !RunAheadArmed() && MG_Config::Ipc.BatchWaits != 0 &&
|
||||
op != MG_Pipe::MGPWireOp::GenerateMipmap) {
|
||||
const MG_Pipe::MGPipeCallClass callClass = MG_Pipe::MGPipeCallClassFor(op);
|
||||
if (callClass == MG_Pipe::kCtxState || callClass == MG_Pipe::kCtxCso ||
|
||||
@@ -931,7 +1035,8 @@ namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
const BarrierWaitScope waiting;
|
||||
const Uint64 waitBudgetMs = AppliedWaitBudgetMs(op, payload);
|
||||
const Transport::SessionWait wait = WaitForAppliedBudget(m_producer, seq, waitBudgetMs);
|
||||
const Transport::SessionWait wait =
|
||||
WaitForAppliedBudget(m_producer, m_events, seq, waitBudgetMs);
|
||||
if (wait == Transport::SessionWait::ShutDown) {
|
||||
// The doorbell died: the server went away. The only thing that returns from a
|
||||
// kWaitForever park, and therefore the only way a client blocked in the barrier
|
||||
@@ -965,7 +1070,17 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// clears when the writeback lands, and the writeback lands on this ring.
|
||||
DrainEventRing(m_events);
|
||||
|
||||
if (!ownsReplySlot) return seq;
|
||||
if (!ownsReplySlot) {
|
||||
// P5e (ra, §2.3). A barriered record's apply has returned and this thread is the
|
||||
// only runnable one, so the four O-class SharedPtr rows the last fill left in
|
||||
// gPipeInputs can go. Under lockstep they were harmless - the next verb's fill
|
||||
// overwrote them - but under run-ahead the next fill may be a frame away, and a
|
||||
// frontend VAO or program whose LAST owner is that block is one the apply thread
|
||||
// can end up destroying (§2.7's deferred-destroy hazard). Released here rather
|
||||
// than at the fill, because here is where the applier is provably idle.
|
||||
if (RunAheadArmed()) ReleaseFillPins();
|
||||
return seq;
|
||||
}
|
||||
|
||||
// THE SAME WAIT, NOT A SECOND ONE. appliedSeq >= seq already means the server wrote
|
||||
// this record's answer, because it writes the slot before it advances the watermark.
|
||||
@@ -1000,6 +1115,166 @@ namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
Bool ClientSession::BarrierArmed() const { return m_barrierArmed; }
|
||||
|
||||
// ---- P5e (ra): the run-ahead latch and the waits it leaves behind ---------------------
|
||||
|
||||
Bool ClientSession::RunAheadArmed() const { return m_runAheadArmed; }
|
||||
// THE PRODUCER'S NUMBER, NOT A SECOND ONE. SessionProducer records it inside
|
||||
// PublishAndNotify, which is the only place a record becomes published at all; a copy kept
|
||||
// here would be a second thing to forget to write on a path that publishes without waiting
|
||||
// - which, after P5e, is most of them.
|
||||
Uint64 ClientSession::LastPublishedSeq() const { return m_producer.LastPublishedSeq(); }
|
||||
Uint64 ClientSession::PresentCreditWaits() const { return m_presentCreditWaits; }
|
||||
|
||||
void ClientSession::LatchRunAheadFromCaps() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// THE CONJUNCTION, IN THE CONTRACT'S OWN ORDER (§1). The caps read is last because it
|
||||
// is the only one that costs a mirror load, and because it is the only one whose answer
|
||||
// can change: the other two are process configuration.
|
||||
const Bool armed = m_barrierArmed && MG_Config::Ipc.RunAhead != 0 &&
|
||||
Caps().HasCap(MG_Pipe::kCapRunAheadApply);
|
||||
if (!m_runAheadLatched) {
|
||||
m_runAheadLatched = true;
|
||||
m_runAheadArmed = armed;
|
||||
if (armed) {
|
||||
MGLOG_I("MG_Remote client: run-ahead ARMED - the server publishes "
|
||||
"kCapRunAheadApply, so an unbarriered record is published and not "
|
||||
"waited for (CONTRACT-P5E §2.3), paced by a present credit of %u",
|
||||
MG_Config::Ipc.PresentCredit);
|
||||
} else if (MG_Config::Ipc.RunAhead != 0 && m_barrierArmed) {
|
||||
// THE KNOB IS NOT A SWITCH ON ITS OWN, and an operator who set it has to be
|
||||
// told that, or an A/B arm that measured nothing looks like an A/B arm that
|
||||
// measured no difference (Config.h's own argument for parsing it everywhere).
|
||||
MGLOG_W_ONCE("MG_Remote client: run-ahead requested, server does not publish "
|
||||
"kCapRunAheadApply - running lockstep. This is Magma always, and "
|
||||
"Espryt until the P5e integration commit flips "
|
||||
"kMGPipeP5eRunAheadReady");
|
||||
}
|
||||
return;
|
||||
}
|
||||
// A LATER SNAPSHOT MAY ONLY TURN IT OFF. Promotion would mean the records already
|
||||
// published without a wait were published against a rule this server never agreed to,
|
||||
// and nothing can un-publish them; demotion only adds waits, so it is safe and is
|
||||
// exactly what a server that lost its run-ahead backend needs.
|
||||
if (!armed && m_runAheadArmed) {
|
||||
m_runAheadArmed = false;
|
||||
MGLOG_W("MG_Remote client: run-ahead DISARMED by a later caps snapshot - every "
|
||||
"record from here waits as it did before P5e");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ClientSession::ReleaseFillPins() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The block's own door: PipeInputs' O-class rows are private and MG_Remote has no
|
||||
// business knowing which four they are. MG_Impl owns the fill, so MG_Impl owns the
|
||||
// un-fill (PipeFill.cpp), and this side only says WHEN.
|
||||
MG_Pipe::MGPipeReleaseResidualFillPins();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ClientSession::WaitForApplyToCatchUp(const char* why) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A NO-OP WITHOUT RUN-AHEAD, and that is not an optimisation: under lockstep the
|
||||
// client already waited for every record it published, so appliedSeq is at
|
||||
// LastPublishedSeq by construction and a second wait would only add a park on a
|
||||
// watermark that is already reached.
|
||||
if (!m_runAheadArmed || !m_started) return;
|
||||
const Uint64 target = m_producer.LastPublishedSeq();
|
||||
if (target == 0) return;
|
||||
const BarrierWaitScope waiting;
|
||||
const Transport::SessionWait wait =
|
||||
WaitForAppliedBudget(m_producer, m_events, target, kBarrierTimeoutMs);
|
||||
if (wait == Transport::SessionWait::ShutDown) {
|
||||
// The doorbell died: teardown, not a fault - the same answer EmitAndWaitTails
|
||||
// gives, and for the same reason.
|
||||
MGLOG_E("MG_Remote client: the forced wait for %s woke on a dead doorbell; the "
|
||||
"server is gone", why);
|
||||
return;
|
||||
}
|
||||
if (wait != Transport::SessionWait::Reached) {
|
||||
MGLOG_F("MGPipe: Fatal{BarrierTimeout, \"%s\"} - a forced run-ahead wait did not "
|
||||
"reach appliedSeq %llu within %u ms. The forced waits of CONTRACT-P5E §2.5 "
|
||||
"are the points at which the client must be in step again, so a wedge here "
|
||||
"is a wedge in the queue and not a slow frame",
|
||||
why, static_cast<unsigned long long>(target), kBarrierTimeoutMs);
|
||||
std::abort();
|
||||
}
|
||||
// EVERY WAIT EXIT DRAINS (§2.6). It is what the flow-control deadlock argument rests
|
||||
// on: the server blocks only on a full event ring, the client blocks only on
|
||||
// watermarks the server advances, and every client wait drains - so at most one side
|
||||
// is parked at any instant.
|
||||
DrainEventRing(m_events);
|
||||
ReleaseFillPins();
|
||||
#else
|
||||
(void)why;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ClientSession::Finish() {
|
||||
// glFinish. Nothing goes on the wire - Flush and Finish have no record (ARCHITECTURE
|
||||
// §11) - and under lockstep there was nothing to do, because the client had already
|
||||
// waited out every command it issued. Under run-ahead "the commands issued so far have
|
||||
// completed" is a promise the queue can break, so this is where it is kept.
|
||||
WaitForApplyToCatchUp("glFinish");
|
||||
}
|
||||
|
||||
Uint64 ClientSession::AcquirePresentCredit() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// MINTED CLIENT-SIDE AND 1-BASED (§1). Before P5e the client sent 0 and the server
|
||||
// stamped its own frame count, which was honest while nothing paced on it; a credit
|
||||
// needs an id space only the payer can advance, and that is this one.
|
||||
const Uint64 serial = m_presentsSent + 1;
|
||||
if (!m_runAheadArmed) {
|
||||
// Lockstep: the present record's own barrier IS the pacing, one frame deep by
|
||||
// construction. The serial is still minted so that both arms carry the same field
|
||||
// and the server's ReturnPresentCredit is fed on both - an A/B whose two arms
|
||||
// disagree about a wire field is not an A/B (Config.h's rule for the knobs).
|
||||
m_presentsSent = serial;
|
||||
return serial;
|
||||
}
|
||||
const Uint32 credit = MG_Config::Ipc.PresentCredit;
|
||||
if (m_presentsSent >= credit) {
|
||||
// Wait for swap number (serial - credit) to have come back. With credit 1 that is
|
||||
// the previous frame: the client publishes frame N+1's records while the server
|
||||
// applies and swaps frame N, and blocks here until swap N returned - one frame of
|
||||
// overlap and at most one frame of added latency (ruling 4 / ID-92).
|
||||
const Uint64 awaited = serial - credit;
|
||||
++m_presentCreditWaits;
|
||||
const BarrierWaitScope waiting;
|
||||
Transport::SessionWait wait = Transport::SessionWait::TimedOut;
|
||||
for (;;) {
|
||||
// §2.6 again: the credit park breaks on a full SEG_EVENT too, for the same
|
||||
// reason the applied park does - a server that stopped producing stopped
|
||||
// swapping, and this waiter holds the only drain.
|
||||
wait = m_producer.WaitForPresentAckOrEventBacklog(awaited, kBarrierTimeoutMs);
|
||||
if (wait != Transport::SessionWait::Reached) break;
|
||||
Transport::RingControl* control = m_producer.Control();
|
||||
if (control == nullptr ||
|
||||
Transport::Watermark::Reached(control->presentAckSerial, awaited)) {
|
||||
break;
|
||||
}
|
||||
DrainEventRing(m_events);
|
||||
}
|
||||
if (wait == Transport::SessionWait::TimedOut) {
|
||||
MGLOG_F("MGPipe: Fatal{PresentCreditTimeout} - present %llu waited %u ms for "
|
||||
"swap %llu to come back with a credit of %u. The credit, never the "
|
||||
"ring's bytes, is what paces a run-ahead client (CONTRACT-P5E §2.4), "
|
||||
"so a credit that never returns is a server that stopped swapping",
|
||||
static_cast<unsigned long long>(serial), kBarrierTimeoutMs,
|
||||
static_cast<unsigned long long>(awaited), credit);
|
||||
std::abort();
|
||||
}
|
||||
// ShutDown is teardown and returns: the present did not happen, the same answer
|
||||
// the barrier gives on a dead doorbell.
|
||||
if (wait == Transport::SessionWait::Reached) DrainEventRing(m_events);
|
||||
}
|
||||
m_presentsSent = serial;
|
||||
return serial;
|
||||
#else
|
||||
return ++m_presentsSent;
|
||||
#endif
|
||||
}
|
||||
|
||||
// R-1's mutual-exclusion invariant, as two probes that answer honestly.
|
||||
//
|
||||
// THE CLIENT'S FLAG IS THREAD-LOCAL AND THE SERVER'S IS NOT, and the asymmetry is the
|
||||
@@ -1012,7 +1287,8 @@ namespace MobileGL::MG_Remote::Client {
|
||||
return g_applyThreadInsideApplier.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt(const char* surface) {
|
||||
void ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt(const char* surface,
|
||||
Bool isBarrieredFill) {
|
||||
// THE CHAIN IS A CONJUNCTION, SO ITS ORDER IS FREE - AND IT IS ORDERED CHEAPEST-FIRST
|
||||
// (P5d round 3, package D). Every line below is a pure predicate whose only effect is
|
||||
// "no violation, go home"; the Fatal fires on the AND of all of them, so reordering
|
||||
@@ -1028,6 +1304,28 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// both miss do we pay for a role probe, two singleton reads, a shared atomic and the
|
||||
// thread_local at the end.
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
|
||||
// ---- P5e (ra), CONTRACT-P5E §3.5: THE RUN-AHEAD RULE IS A DIFFERENT RULE -----------
|
||||
//
|
||||
// It is first, before the BatchWaits return, because BatchWaits' argument does not
|
||||
// survive run-ahead: "the pull-verbs' own wait keeps their pulled reads fenced" is a
|
||||
// statement about a client that waits at every verb, and this one does not. Under
|
||||
// run-ahead the block is SERVER-ROLE memory (§3) and the single legal GL-thread write
|
||||
// is the residual fill of a BARRIERED record - the one the client is about to park
|
||||
// behind. Every other touch races the applier by construction, so there is no
|
||||
// in-applier flag to consult and no "count it" arm: the value would be torn.
|
||||
//
|
||||
// This is where a fill left standing for an unbarriered record lands, by name.
|
||||
if (ClientSessionInstance().RunAheadArmed()) {
|
||||
if (isBarrieredFill) return;
|
||||
if (Server::ServerLoop::OnApplyThread()) return; // the applier owns the block
|
||||
MGLOG_F("MGPipe: Fatal{RoleViolation, \"gPipeInputs\"} - the GL thread touched "
|
||||
"gPipeInputs (%s) on a RUN-AHEAD session outside a barriered fill. With "
|
||||
"the client running ahead the block is the server's to read for as long "
|
||||
"as an unbarriered record is in flight (CONTRACT-P5E §3), and this thread "
|
||||
"has no wait that would make the touch quiescent",
|
||||
surface);
|
||||
std::abort();
|
||||
}
|
||||
// MOBILEGL_IPC_BATCH_WAITS=1 makes "the fill runs while the apply thread applies an
|
||||
// earlier value record" the INTENDED shape: the fill writes only fields no record
|
||||
// supplies (the wire-live skips are the same answer the emitters use), the applier
|
||||
@@ -1090,6 +1388,14 @@ namespace MobileGL::MG_Remote::Client {
|
||||
}
|
||||
if (AdoptCapsSnapshot(envelope->msg_as_CapsSnapshot())) ++adopted;
|
||||
}
|
||||
// P5e (ra, §1): the run-ahead latch rides R-12's invalidation edge, because "the first
|
||||
// caps adoption after Start" and "the one place a CapsSnapshot becomes a mirror
|
||||
// generation" are the same instant, and a latch taken anywhere else would be reading a
|
||||
// mirror whose generation nobody had adopted yet. Called even when `adopted` is 0: the
|
||||
// first call in Start() is what takes the latch, and a pump that adopted nothing still
|
||||
// has to leave it taken (a handshake with no snapshot latches "not armed", which is
|
||||
// the honest answer for a server that never said what it consumes).
|
||||
if (adopted != 0 || !m_runAheadLatched) LatchRunAheadFromCaps();
|
||||
return adopted;
|
||||
}
|
||||
|
||||
@@ -1167,9 +1473,9 @@ namespace MobileGL::MG_Remote::Client {
|
||||
MGLOG_I("MG_Remote client: wire ledger: maxrec=%llu maxrecop=%s cap=%llu (%llu.%llu%% of "
|
||||
"RingProducer::MaxRecordBytes, half of a %llu byte SEG_CMD) cmdbytes=%llu "
|
||||
"ringwraps=%llu ringpads=%llu "
|
||||
"ringwaits=%llu emitseq=%llu cliwait=%llu clipark=%llu - R-10's proof obligation, "
|
||||
"R-9's producer readings and P5d round 3's client wait pair, published from the "
|
||||
"session that produced them",
|
||||
"ringwaits=%llu emitseq=%llu cliwait=%llu clipark=%llu credit-waits=%llu - "
|
||||
"R-10's proof obligation, R-9's producer readings, P5d round 3's client wait "
|
||||
"pair and P5e's present credit, published from the session that produced them",
|
||||
static_cast<unsigned long long>(maxRecord), m_encoder.MaxRecordOpName(),
|
||||
static_cast<unsigned long long>(cap),
|
||||
static_cast<unsigned long long>(permille / 10),
|
||||
@@ -1185,7 +1491,13 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// profile runs did not set MOBILEGL_PIPE_STATS either, and "did the barrier spin
|
||||
// or did it park" is the question their logs have to be able to answer.
|
||||
static_cast<unsigned long long>(m_producer.Waits()),
|
||||
static_cast<unsigned long long>(m_producer.Parks()));
|
||||
static_cast<unsigned long long>(m_producer.Parks()),
|
||||
// P5e (ra, §2.4). The number the device exit reads to say whether the present
|
||||
// credit is what paces this workload: 0 with credit 1 means the server is
|
||||
// never the frame's critical path, and a count near the frame count means it
|
||||
// always is. It is on this line rather than only on the stats line for
|
||||
// LogWireLedger's own reason - a trace replay never reaches a stats summary.
|
||||
static_cast<unsigned long long>(m_presentCreditWaits));
|
||||
}
|
||||
|
||||
#undef MGP5_C0_STUB
|
||||
|
||||
@@ -112,15 +112,79 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// or its MGPDrawIndirect block behind the range array). The one-tail form above is this
|
||||
// one with a single WireTail; the barrier policy lives in exactly one body. A distinct
|
||||
// name rather than an overload, because `nullptr, 0` would match both.
|
||||
// P5e (ra, CONTRACT-P5E §2.5): `wantReply` is the ONE caller-side relaxation of the
|
||||
// catalogue's reply rule, and it exists for exactly one row - resource_subdata's
|
||||
// BUFFER half, whose Bool the only caller discards (PipeFill.cpp's
|
||||
// MGPipeEmitResourceSubData). The row keeps kReplySlot on the wire (the server still
|
||||
// posts; an unread slot is harmless, ReplySlot.h:16, 105), so nothing about the
|
||||
// protocol moves - what moves is that under run-ahead this client neither waits for
|
||||
// the answer nor reads it, which is what makes the 64 KB persistent-map push
|
||||
// fire-and-forget instead of a hidden round trip per block. It is NOT a licence to
|
||||
// skip an answer a caller uses: R-5 still forbids re-deriving one locally, and every
|
||||
// other kReplySlot row passes true (the default) and waits.
|
||||
Uint64 EmitAndWaitTails(MG_Pipe::MGPWireOp op, const void* payload, Uint64 payloadBytes,
|
||||
const Wire::WireTail* tails, Uint32 tailCount, void* replyOut,
|
||||
Uint64 replyBytes, Int32* statusOut,
|
||||
Uint64* replySizeOut = nullptr);
|
||||
Uint64* replySizeOut = nullptr, Bool wantReply = true);
|
||||
|
||||
// MOBILEGL_IPC_VERB_BARRIER. False is the R-1 negative control and is EXPECTED to be
|
||||
// red; it must be run once and the way it goes red recorded.
|
||||
Bool BarrierArmed() const;
|
||||
|
||||
// ---- P5e (ra): the wait rule (CONTRACT-P5E §1, §2.3, §2.4, §2.5) -------------------
|
||||
|
||||
// A CONJUNCTION OF THREE, LATCHED ONCE: `m_barrierArmed && Ipc.RunAhead &&
|
||||
// Caps().HasCap(kCapRunAheadApply)`. The third conjunct is the server's own statement
|
||||
// that it applies an unbarriered record without reading client memory, so run-ahead is
|
||||
// never something the client can turn on by itself - on Magma, and on Espryt until the
|
||||
// integration commit publishes bit 10, this answers false and EmitAndWaitTails runs
|
||||
// today's lockstep path byte for byte.
|
||||
//
|
||||
// LATCHED AT THE FIRST CAPS ADOPTION AFTER Start, AND A LATER SNAPSHOT MAY ONLY TURN IT
|
||||
// OFF. A caps re-run cannot promote a lockstep server to a run-ahead one mid-frame:
|
||||
// records already published without a wait would have been published against the wrong
|
||||
// rule, and there is no way to un-publish them. Demotion is safe in the other
|
||||
// direction - it only adds waits - so it is allowed.
|
||||
Bool RunAheadArmed() const;
|
||||
|
||||
// §2.3: the four O-class SharedPtr rows of gPipeInputs (PipeInputs.h:801-809 - the
|
||||
// bound VAO and the three programs), dropped on the GL thread once a barriered apply
|
||||
// has returned. Under lockstep the next fill overwrites them within a verb; under
|
||||
// run-ahead the next fill may be a frame away, and a pinned VAO whose last owner ends
|
||||
// up being the apply thread is exactly the deferred-destroy hazard §2.7 keeps a queue
|
||||
// for. Releasing them at the one instant the applier is provably idle costs four
|
||||
// refcount drops per barriered record and removes the pin entirely.
|
||||
void ReleaseFillPins();
|
||||
|
||||
// §2.5's forced wait, as one named entry point: publish nothing, wait for the apply
|
||||
// thread to reach LastPublishedSeq, drain the reverse channel. `why` names the caller
|
||||
// in the timeout Fatal, because "which forced wait wedged" is the whole diagnostic.
|
||||
// A no-op when run-ahead is not armed (the client is already in step by construction).
|
||||
void WaitForApplyToCatchUp(const char* why);
|
||||
|
||||
// glFinish. Flush and Finish stay no-ops ON THE WIRE (ARCHITECTURE §11, :415) - there
|
||||
// is no record to emit - but "the GL commands issued so far have completed" cannot be
|
||||
// true while a run-ahead queue still holds them, so under run-ahead glFinish becomes
|
||||
// exactly WaitForApplyToCatchUp + the drain that comes with it.
|
||||
void Finish();
|
||||
|
||||
// The last seq this session published, whether or not it waited for it. The forced
|
||||
// waits above are defined against it.
|
||||
Uint64 LastPublishedSeq() const;
|
||||
|
||||
// §2.4's counter, published on the stats line as `credit-waits`: how often the client
|
||||
// actually blocked for a swap to come back. Zero with a credit of 1 means the server
|
||||
// is never the frame's critical path; a number close to the frame count means the
|
||||
// credit is what paces this workload, which is what the device exit measures.
|
||||
Uint64 PresentCreditWaits() const;
|
||||
|
||||
// §2.4, called by EmitPresent immediately before it encodes: pay the credit (parking
|
||||
// if this client already has `MOBILEGL_IPC_PRESENT_CREDIT` presents in flight), then
|
||||
// mint and return this frame's 1-based MGPPresent::FrameSerial. The credit wait is
|
||||
// BEFORE the encode on purpose: a client that encoded first would hold a SEG_CMD
|
||||
// reservation across the park.
|
||||
Uint64 AcquirePresentCredit();
|
||||
|
||||
// R-1's invariant made checkable rather than only written down: true while this
|
||||
// thread is inside a barrier wait. The apply thread sets its own flag on entry to the
|
||||
// applier; a debug/verify build asserts the two are never both true, and that the
|
||||
@@ -140,7 +204,17 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// 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);
|
||||
//
|
||||
// P5e (ra, CONTRACT-P5E §3.5) RE-DERIVES IT UNDER RUN-AHEAD, and `isBarrieredFill` is
|
||||
// what the caller says about ITSELF: "this touch is the residual fill of a record this
|
||||
// thread is about to park behind". With run-ahead armed the block is server-role
|
||||
// memory for every other touch, so the rule stops being "not while the applier is
|
||||
// inside" - which is unanswerable when the client never waits - and becomes "not at
|
||||
// all, unless you are that fill". The MOBILEGL_IPC_BATCH_WAITS early return goes with
|
||||
// it: batching is about how many round trips a lockstep client pays, and it says
|
||||
// nothing about a client that pays none.
|
||||
static void RefusePipeInputsTouchWhileApplierOwnsIt(const char* surface,
|
||||
Bool isBarrieredFill = false);
|
||||
|
||||
// ---- c1's additions ---------------------------------------------------------------
|
||||
|
||||
@@ -250,9 +324,20 @@ namespace MobileGL::MG_Remote::Client {
|
||||
void LogWireLedger() const;
|
||||
|
||||
private:
|
||||
// P5e (ra): the latch, taken from PumpControlPlane after every adoption. Private
|
||||
// because "when may run-ahead start" is this class's own rule and not a caller's.
|
||||
void LatchRunAheadFromCaps();
|
||||
|
||||
Wire::PipeWireEncoder m_encoder;
|
||||
Wire::SegmentTable m_segments;
|
||||
Bool m_barrierArmed = true;
|
||||
// P5e (ra): the latch of RunAheadArmed() and the fact that it has been taken once.
|
||||
Bool m_runAheadArmed = false;
|
||||
Bool m_runAheadLatched = false;
|
||||
// §2.4: presents published (the 1-based FrameSerial space) and how often the credit
|
||||
// wait really blocked.
|
||||
Uint64 m_presentsSent = 0;
|
||||
Uint64 m_presentCreditWaits = 0;
|
||||
|
||||
std::unique_ptr<Transport::InProcessTransport> m_clientTransport;
|
||||
std::unique_ptr<Transport::InProcessTransport> m_serverTransport;
|
||||
|
||||
@@ -1120,10 +1120,20 @@ namespace MobileGL::MG_Remote::Client {
|
||||
}
|
||||
|
||||
MG_Pipe::MGPPresent record{};
|
||||
// FrameSerial 0 = "the server stamps its own". P5 has no client-side present credit
|
||||
// (MOBILEGL_IPC_PRESENT_CREDIT is P6's), so a client-minted serial would be a second
|
||||
// id space with no consumer.
|
||||
record.FrameSerial = 0;
|
||||
// P5e (ra, CONTRACT-P5E §1, §2.4). FrameSerial USED TO BE 0 - "the server stamps
|
||||
// its own" - and that was honest while nothing paced on it. It is now minted here,
|
||||
// 1-based, by AcquirePresentCredit, which also PAYS the credit: if this client
|
||||
// already has MOBILEGL_IPC_PRESENT_CREDIT presents in flight it parks until the
|
||||
// server's OnPresent returns one, and only then does it mint.
|
||||
//
|
||||
// THE CREDIT WAIT IS BEFORE THE ENCODE, WHICH IS NOT A DETAIL: EmitAndWait
|
||||
// reserves SEG_CMD bytes as its first act, so a client that encoded and then
|
||||
// parked would hold a ring reservation across a whole frame of server time. It
|
||||
// also drains SEG_EVENT on its way out, like every other wait (§2.6).
|
||||
//
|
||||
// With run-ahead disarmed this is a counter and nothing else, and the record below
|
||||
// travels exactly as it did - the present row's own barrier is the pacing there.
|
||||
record.FrameSerial = session.AcquirePresentCredit();
|
||||
session.EmitAndWait(MG_Pipe::MGPWireOp::Present, &record, sizeof(record), nullptr, 0,
|
||||
nullptr, 0, nullptr);
|
||||
|
||||
|
||||
@@ -348,15 +348,33 @@ namespace MobileGL::MG_Remote::Client {
|
||||
ClientSession& session = RequireSession("ResourceSubData");
|
||||
MG_Pipe::MGPSubData record = *payload;
|
||||
record.Blob = StageOptional(session, blobBytes, blobByteCount);
|
||||
// P5e (ra, CONTRACT-P5E §2.5 / ruling 15): the BUFFER half does not want its
|
||||
// answer, and MGPipeSubDataWantsItsReply is the one place that decides - the same
|
||||
// function the route reads, so the two halves of this call cannot disagree about
|
||||
// whether a wait is owed. Under run-ahead the record is published and this thread
|
||||
// returns; the persistent-map push (PersistentMapTracker's 64 KB blocks) stops
|
||||
// being a hidden round trip per block, which is the single biggest wait left on
|
||||
// the steady path that is not a draw.
|
||||
//
|
||||
// THE ANSWER IS ACCEPT-BY-CONSTRUCTION, AND THAT IS HONEST HERE AND NOWHERE ELSE:
|
||||
// the only caller discards it (PipeFill.cpp's MGPipeEmitResourceSubData), so
|
||||
// "accepted" is not a re-derivation of a server decision - it is the absence of a
|
||||
// question. The server still posts the real answer into the slot; nothing reads
|
||||
// it, which ReplySlot.h:16, 105 makes legal. A row whose acceptance a caller USES
|
||||
// may never take this path - R-5 has not moved.
|
||||
const Bool wantReply = MG_Pipe::MGPipeSubDataWantsItsReply(record);
|
||||
Int32 status = 0;
|
||||
const Uint64 seq = session.EmitAndWait(
|
||||
MGPWireOp::ResourceSubData, &record, sizeof(record), varTail,
|
||||
static_cast<Uint64>(varTailCount) * sizeof(MG_Pipe::MGPSubRegion), nullptr, 0,
|
||||
&status);
|
||||
const Wire::WireTail tail{varTail, static_cast<Uint64>(varTailCount) *
|
||||
sizeof(MG_Pipe::MGPSubRegion)};
|
||||
const Uint64 seq = session.EmitAndWaitTails(
|
||||
MGPWireOp::ResourceSubData, &record, sizeof(record),
|
||||
varTail != nullptr ? &tail : nullptr, varTail != nullptr ? 1u : 0u, nullptr, 0,
|
||||
&status, nullptr, wantReply);
|
||||
reply->Id = seq;
|
||||
MG_Pipe::MGPipePostReply(*reply, status, status == 0 ? 1u : 0u);
|
||||
MG_Pipe::MGPipePostReply(*reply, wantReply ? status : 0,
|
||||
(wantReply ? status == 0 : true) ? 1u : 0u);
|
||||
++g_emitted;
|
||||
if (status == 1) ++g_declined;
|
||||
if (wantReply && status == 1) ++g_declined;
|
||||
}
|
||||
|
||||
void Wire_BufferSubDataResident(const MG_Pipe::MGPSubData* payload, const void* blobBytes,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "PipeApplier.h"
|
||||
|
||||
#include "ServerSession.h"
|
||||
#include "../Transport/ReplySlot.h"
|
||||
|
||||
#include <Config.h>
|
||||
@@ -29,6 +30,20 @@
|
||||
|
||||
namespace MobileGL::MG_Remote::Server {
|
||||
|
||||
namespace {
|
||||
// P5e (ra, CONTRACT-P5E §1 / §6): DOES THIS SERVER PUBLISH kCapRunAheadApply? The
|
||||
// question is asked of the server's OWN CallMask and not of a build constant, because
|
||||
// "the client may run ahead" is exactly what that bit says and Magma never sets it.
|
||||
// A session with no CallMask yet (the bring-up window, a fixture that never called
|
||||
// SetCapabilityBits) answers false: no client can have latched run-ahead against a
|
||||
// snapshot that was never published.
|
||||
Bool ServerPublishesRunAhead() {
|
||||
const ServerSession* session = ServerSession::Active();
|
||||
if (session == nullptr || !session->CallMaskIsSet()) return false;
|
||||
return (session->CallMask() & static_cast<Uint64>(MG_Pipe::kCapRunAheadApply)) != 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ReplyPool::ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes)
|
||||
: m_base(static_cast<Uint8*>(base)), m_size(sizeBytes), m_slots(slotCount), m_slotBytes(slotBytes) {}
|
||||
|
||||
@@ -288,7 +303,30 @@ namespace MobileGL::MG_Remote::Server {
|
||||
++m_presents;
|
||||
// FrameSerial 0 means "the server stamps its own" (c1-v1 8.3): P5 has no client-side
|
||||
// present credit, so the client sends 0 and the frame count on this side IS the serial.
|
||||
//
|
||||
// P5e (ra, CONTRACT-P5E §1, §2.4): IT IS THE CLIENT'S NOW, 1-BASED AND MINTED BY THE
|
||||
// PAYER. A credit can only be paced in an id space the waiter advances, and the waiter
|
||||
// is the client (`WaitForPresentAck(m_presentsSent + 1 - credit)`), so a server-stamped
|
||||
// serial would be the server acknowledging its own count. The 0 arm survives for a
|
||||
// peer that has not been re-built - and on a server that PUBLISHES the run-ahead cap it
|
||||
// is a protocol fault, because such a client is pacing on this answer and a 0 would
|
||||
// acknowledge a frame nobody asked about.
|
||||
if (present.FrameSerial == 0 && ServerPublishesRunAhead()) {
|
||||
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"Present.FrameSerial\"} - a run-ahead "
|
||||
"server was handed present serial 0. The client mints this 1-based and "
|
||||
"waits on it for its credit (CONTRACT-P5E §2.4); returning a credit for "
|
||||
"serial 0 would release a wait that is asking about frame N");
|
||||
std::abort();
|
||||
}
|
||||
m_lastPresentSerial = present.FrameSerial != 0 ? present.FrameSerial : m_presents;
|
||||
// §2.4's other half, and the reason ServerSession::ReturnPresentCredit has had no
|
||||
// production caller since v1 wrote it: ONE CREDIT PER SWAP, returned after Present()
|
||||
// has returned rather than before it, because what the client is waiting for is the
|
||||
// swap and not the record's apply. It advances presentAckSerial AND rings the client's
|
||||
// bell - a client parked in WaitForPresentAck(kWaitForever) needs the pair.
|
||||
if (ServerSession* session = ServerSession::Active()) {
|
||||
session->ReturnPresentCredit(m_lastPresentSerial);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -377,15 +377,27 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// CondVarDoorbell::Kill() breaks that, and Kill is the CLIENT's teardown call, not
|
||||
// something an EGL make-current request may perform. So the predicate carries all
|
||||
// three arming conditions and the doorbell wakes the thread for any of them.
|
||||
// P5e (ra, CONTRACT-P5E §2.6) ADDS A FOURTH, AND IT IS THE ONE THAT SAYS "NOT NOW"
|
||||
// RATHER THAN "WAKE UP". A full SEG_EVENT means the apply thread has nowhere to put
|
||||
// the events the next record's apply would produce, so a waiting record is NOT work
|
||||
// yet: applying it would only park the producer half way through. Stop and Control
|
||||
// still win - a teardown may not be held up by a client that stopped draining - which
|
||||
// is why they are tested first and the ring test sits between them and the queue.
|
||||
//
|
||||
// The wake comes from EventRingConsumer::Drained(), which clears the latch AND rings
|
||||
// this bell (that pairing is the whole of the flow-control protocol; a cleared flag
|
||||
// with no bell is the lost wakeup the forward direction's publish-then-ring order
|
||||
// exists to prevent).
|
||||
const auto ready = [this, &control, &ring] {
|
||||
return control.cmdHead.load(std::memory_order_acquire) != ring.LocalTail() ||
|
||||
m_stopRequested.load(std::memory_order_acquire) || ControlIsPending();
|
||||
if (m_stopRequested.load(std::memory_order_acquire) || ControlIsPending()) return true;
|
||||
if (control.eventRingFull.load(std::memory_order_acquire) != 0) return false;
|
||||
return control.cmdHead.load(std::memory_order_acquire) != ring.LocalTail();
|
||||
};
|
||||
|
||||
for (;;) {
|
||||
PumpControlRequest();
|
||||
if (m_stopRequested.load(std::memory_order_acquire)) break;
|
||||
DrainRing();
|
||||
if (control.eventRingFull.load(std::memory_order_acquire) == 0) DrainRing();
|
||||
if (m_stopRequested.load(std::memory_order_acquire)) break;
|
||||
if (ready()) continue;
|
||||
|
||||
|
||||
@@ -180,6 +180,87 @@ namespace MobileGL::MG_Remote::Server {
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// ---- P5e (ra), CONTRACT-P5E §2.6: the ring stops being a Fatal and becomes a queue --
|
||||
//
|
||||
// THE FATAL'S PREMISE DIES WITH THE LOCKSTEP. "A full ring is a producer burst no
|
||||
// workload has" was true because the client drained at EVERY verb: the ring was empty
|
||||
// whenever the server started a record, so filling 256 KiB inside one apply meant a
|
||||
// defect. A run-ahead client drains at its waits, which on the steady path are one
|
||||
// present apart, so a full ring is now an ordinary backlog and aborting on it would
|
||||
// turn the reverse channel's capacity into a workload limit.
|
||||
//
|
||||
// SO THE PRODUCER BLOCKS. It publishes what is already in the ring and rings the
|
||||
// client (a client that has not yet noticed cannot drain), then parks on its own bell
|
||||
// until the client's Drained() clears the latch and rings back. The deadlock argument
|
||||
// is the contract's, and it needs BOTH halves: the server blocks only here, the client
|
||||
// blocks only on watermarks the server advances, and every client park breaks on
|
||||
// `eventRingFull` as well (SessionProducer::WaitFor*OrEventBacklog) - so a parked
|
||||
// client is always wakeable by the server that is waiting for it.
|
||||
//
|
||||
// TWO NAMED REFUSALS SURVIVE, and they are the impossible cases rather than the busy
|
||||
// ones: a single event larger than the ring can EVER hold (no amount of draining
|
||||
// helps), and a wait that ran out of patience (the peer is gone or wedged). A server
|
||||
// that does not publish kCapRunAheadApply keeps P5C's Fatal unchanged.
|
||||
Bool ServerPublishesRunAhead(const ServerSession& session) {
|
||||
if (!session.CallMaskIsSet()) return false;
|
||||
return (session.CallMask() & static_cast<Uint64>(MG_Pipe::kCapRunAheadApply)) != 0;
|
||||
}
|
||||
|
||||
constexpr Uint32 kEventBacklogWaitMs = 30000;
|
||||
|
||||
void* ReserveEventOrBlock(ServerSession& session, Transport::EventKind kind,
|
||||
const char* eventName, Uint64 payloadBytes) {
|
||||
void* slot = session.Events().Reserve(kind, payloadBytes);
|
||||
if (slot != nullptr) return slot;
|
||||
if (!ServerPublishesRunAhead(session)) FatalEventRingOverflow(eventName, payloadBytes);
|
||||
|
||||
// Impossible case one: the record does not fit an EMPTY ring. Reserve caps one
|
||||
// record at capacity/2 (Ring.h), so no drain can ever make room and blocking would
|
||||
// be a hang with a comment on it.
|
||||
const Uint64 cap = session.Events().Ring().MaxRecordBytes();
|
||||
if (payloadBytes + sizeof(Transport::RingRecordHeader) > cap) {
|
||||
MGLOG_F("MGPipe: Fatal{EventRingOverflow} - %s needs %llu bytes and SEG_EVENT "
|
||||
"caps ONE record at %llu (half its capacity). Flow control cannot help: "
|
||||
"no amount of draining makes a record fit a ring that is too small for "
|
||||
"it. Raise MOBILEGL_IPC_EVENT_KB or slice the event at its producer, as "
|
||||
"the writeback path already does",
|
||||
eventName, static_cast<unsigned long long>(payloadBytes),
|
||||
static_cast<unsigned long long>(cap));
|
||||
std::abort();
|
||||
}
|
||||
|
||||
Transport::RingControl& control = session.Control();
|
||||
Transport::Doorbell& bell = session.ConsumerDoorbell();
|
||||
const auto drained = [&control] {
|
||||
return control.eventRingFull.load(std::memory_order_acquire) == 0;
|
||||
};
|
||||
for (Uint32 round = 0; round < 2; ++round) {
|
||||
// PUBLISH AND RING FIRST. The client cannot drain a ring whose head it has not
|
||||
// been shown, and the latch Reserve just set is only useful to a client that
|
||||
// gets a bell with it.
|
||||
session.PublishEvents();
|
||||
if (!drained() && !bell.Wait(control.consumerParked, drained, /*spinUs=*/0,
|
||||
kEventBacklogWaitMs)) {
|
||||
MGLOG_F("MGPipe: Fatal{EventRingOverflow} - %s waited %u ms for the client "
|
||||
"to drain SEG_EVENT and it never did. Under run-ahead the apply "
|
||||
"thread parks on this ring (CONTRACT-P5E §2.6); a client that does "
|
||||
"not drain is one that is not running",
|
||||
eventName, kEventBacklogWaitMs);
|
||||
std::abort();
|
||||
}
|
||||
slot = session.Events().Reserve(kind, payloadBytes);
|
||||
if (slot != nullptr) return slot;
|
||||
}
|
||||
// Two full rounds against a ring the record provably fits: something else is
|
||||
// producing into it, which on this side is impossible (one producer, the apply
|
||||
// thread, by construction).
|
||||
MGLOG_F("MGPipe: Fatal{EventRingOverflow} - %s could not reserve %llu bytes on an "
|
||||
"emptied SEG_EVENT. The apply thread is SEG_EVENT's only producer, so a "
|
||||
"drained ring that still refuses a record it fits is a corrupt cursor set",
|
||||
eventName, static_cast<unsigned long long>(payloadBytes));
|
||||
std::abort();
|
||||
}
|
||||
|
||||
void ServerOnBufferWriteback(MG_Pipe::MGPipeHandle res, Uint64 offset,
|
||||
MG_Pipe::MGPBlobRef bytes) {
|
||||
if (bytes.Seg != MG_Pipe::kMGHostSpanSegNone) {
|
||||
@@ -195,10 +276,8 @@ namespace MobileGL::MG_Remote::Server {
|
||||
}
|
||||
ServerSession& session = ServerSessionInstance();
|
||||
const Uint64 payloadBytes = sizeof(Transport::EventBufferWritebackHead) + bytes.Size;
|
||||
void* slot = session.Events().Reserve(Transport::kEventBufferWriteback, payloadBytes);
|
||||
if (slot == nullptr) {
|
||||
FatalEventRingOverflow("kEventBufferWriteback", payloadBytes);
|
||||
}
|
||||
void* slot = ReserveEventOrBlock(session, Transport::kEventBufferWriteback,
|
||||
"kEventBufferWriteback", payloadBytes);
|
||||
Transport::EventBufferWritebackHead head{};
|
||||
head.Resource = Transport::EventHandle{res.Slot, res.Gen};
|
||||
head.Offset = offset;
|
||||
@@ -217,10 +296,8 @@ namespace MobileGL::MG_Remote::Server {
|
||||
ServerSession& session = ServerSessionInstance();
|
||||
const Uint64 tailBytes = static_cast<Uint64>(rangeCount) * sizeof(Transport::EventRange);
|
||||
const Uint64 payloadBytes = sizeof(Transport::EventGpuWrittenHead) + tailBytes;
|
||||
void* slot = session.Events().Reserve(Transport::kEventGpuWritten, payloadBytes);
|
||||
if (slot == nullptr) {
|
||||
FatalEventRingOverflow("kEventGpuWritten", payloadBytes);
|
||||
}
|
||||
void* slot = ReserveEventOrBlock(session, Transport::kEventGpuWritten,
|
||||
"kEventGpuWritten", payloadBytes);
|
||||
Transport::EventGpuWrittenHead head{};
|
||||
head.Resource = Transport::EventHandle{res.Slot, res.Gen};
|
||||
head.RangeCount = static_cast<std::uint32_t>(rangeCount);
|
||||
@@ -237,10 +314,8 @@ namespace MobileGL::MG_Remote::Server {
|
||||
void ServerOnSurfaceChanged(const MG_Pipe::MGPSurfaceInfo* info) {
|
||||
ServerSession& session = ServerSessionInstance();
|
||||
constexpr Uint64 payloadBytes = sizeof(Transport::EventSurfaceChangedHead);
|
||||
void* slot = session.Events().Reserve(Transport::kEventSurfaceChanged, payloadBytes);
|
||||
if (slot == nullptr) {
|
||||
FatalEventRingOverflow("kEventSurfaceChanged", payloadBytes);
|
||||
}
|
||||
void* slot = ReserveEventOrBlock(session, Transport::kEventSurfaceChanged,
|
||||
"kEventSurfaceChanged", payloadBytes);
|
||||
Transport::EventSurfaceChangedHead head{};
|
||||
if (info != nullptr) {
|
||||
head.Width = info->Width;
|
||||
@@ -649,10 +724,8 @@ namespace MobileGL::MG_Remote::Server {
|
||||
}
|
||||
const Uint32 messageBytes = static_cast<Uint32>(length) + 1;
|
||||
const Uint64 payloadBytes = sizeof(Transport::EventGlErrorHead) + messageBytes;
|
||||
void* slot = m_events.Reserve(Transport::kEventGlError, payloadBytes);
|
||||
if (slot == nullptr) {
|
||||
FatalEventRingOverflow("kEventGlError", payloadBytes);
|
||||
}
|
||||
void* slot = ReserveEventOrBlock(*this, Transport::kEventGlError, "kEventGlError",
|
||||
payloadBytes);
|
||||
Transport::EventGlErrorHead head{};
|
||||
head.Code = code;
|
||||
head.MessageBytes = messageBytes;
|
||||
|
||||
@@ -189,10 +189,26 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
void Drained() {
|
||||
m_consumer.PublishRetired();
|
||||
if (m_cmdControl != nullptr) {
|
||||
m_cmdControl->eventRingFull.store(0, std::memory_order_release);
|
||||
// P5e (ra, CONTRACT-P5E §2.6): CLEARING THE LATCH IS ONLY HALF OF IT. Under
|
||||
// flow control the server is PARKED on this flag - either inside a producer
|
||||
// that could not Reserve, or at a record boundary refusing to start the next
|
||||
// record - and a cleared flag with no bell is a wakeup lost exactly the way
|
||||
// the forward direction's publish-then-ring order exists to prevent. The
|
||||
// exchange is what makes the ring one-shot: only the drain that actually found
|
||||
// the flag up rings, so a steady stream of drains costs one relaxed RMW.
|
||||
const bool wasFull =
|
||||
m_cmdControl->eventRingFull.exchange(0, std::memory_order_acq_rel) != 0;
|
||||
if (wasFull && m_serverBell != nullptr) {
|
||||
NotifyIfParked(*m_serverBell, m_cmdControl->consumerParked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The bell the SERVER parks on, lent by the session so that Drained() can ring it.
|
||||
// Null in every fixture that consumes a ring without a server behind it, and the
|
||||
// ring's clear then behaves exactly as it did before P5e.
|
||||
void SetServerDoorbell(Doorbell* bell) { m_serverBell = bell; }
|
||||
|
||||
// Byte offset of `payload` inside SEG_EVENT, which is what an
|
||||
// OnBufferWriteback MGPBlobRef must carry (Seg = kSegEvent, Offset =
|
||||
// this, Size = the head's Size). Never a host address - R-2's rule B.
|
||||
@@ -217,6 +233,8 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
RingControl* m_cmdControl = nullptr;
|
||||
RingConsumer m_consumer;
|
||||
const std::uint8_t* m_segmentBase = nullptr;
|
||||
// P5e (ra): the server's bell, rung by Drained() when it cleared a full latch.
|
||||
Doorbell* m_serverBell = nullptr;
|
||||
};
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Transport
|
||||
|
||||
@@ -501,6 +501,40 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
timeoutMs);
|
||||
}
|
||||
|
||||
// P5e (ra), CONTRACT-P5E §2.6. Two predicates, one OR each, and the caller decides which
|
||||
// of the two happened by reading the watermark it actually cares about.
|
||||
bool SessionProducer::EventRingIsFull() const {
|
||||
return m_control != nullptr && m_control->eventRingFull.load(std::memory_order_acquire) != 0;
|
||||
}
|
||||
|
||||
SessionWait SessionProducer::WaitForAppliedOrEventBacklog(std::uint64_t seq,
|
||||
std::uint32_t timeoutMs) {
|
||||
if (!Valid()) {
|
||||
return SessionWait::TimedOut;
|
||||
}
|
||||
RingControl* control = m_control;
|
||||
return Park(
|
||||
[control, seq] {
|
||||
return Watermark::Reached(control->appliedSeq, seq) ||
|
||||
control->eventRingFull.load(std::memory_order_acquire) != 0;
|
||||
},
|
||||
timeoutMs);
|
||||
}
|
||||
|
||||
SessionWait SessionProducer::WaitForPresentAckOrEventBacklog(std::uint64_t serial,
|
||||
std::uint32_t timeoutMs) {
|
||||
if (!Valid()) {
|
||||
return SessionWait::TimedOut;
|
||||
}
|
||||
RingControl* control = m_control;
|
||||
return Park(
|
||||
[control, serial] {
|
||||
return Watermark::Reached(control->presentAckSerial, serial) ||
|
||||
control->eventRingFull.load(std::memory_order_acquire) != 0;
|
||||
},
|
||||
timeoutMs);
|
||||
}
|
||||
|
||||
SessionWait SessionProducer::WaitForCmdSpace(std::uint64_t bytes, std::uint32_t timeoutMs) {
|
||||
if (!Valid()) {
|
||||
return SessionWait::TimedOut;
|
||||
|
||||
@@ -325,6 +325,22 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
SessionWait WaitForApplied(std::uint64_t seq, std::uint32_t timeoutMs);
|
||||
// Present throttle.
|
||||
SessionWait WaitForPresentAck(std::uint64_t serial, std::uint32_t timeoutMs);
|
||||
|
||||
// ---- P5e (ra), CONTRACT-P5E §2.6: the same two waits, PLUS the event ring ---------
|
||||
//
|
||||
// A parked client must be wakeable by a server that ran out of SEG_EVENT, or the
|
||||
// flow-control deadlock is one burst wide: the server stops producing, so it stops
|
||||
// applying, so appliedSeq never reaches the seq this waiter is parked on, so the ring
|
||||
// is never drained. Both predicates therefore break on `eventRingFull` as well, and
|
||||
// the CALLER re-reads the watermark to find out which of the two woke it - a third
|
||||
// SessionWait value would have made every existing `== Reached` site a bug.
|
||||
//
|
||||
// The old entry points above are unchanged and keep their callers: teardown's
|
||||
// WaitForApplied has no ring to drain and must not learn about one.
|
||||
SessionWait WaitForAppliedOrEventBacklog(std::uint64_t seq, std::uint32_t timeoutMs);
|
||||
SessionWait WaitForPresentAckOrEventBacklog(std::uint64_t serial, std::uint32_t timeoutMs);
|
||||
// True when the peer latched "SEG_EVENT is full and I stopped producing".
|
||||
bool EventRingIsFull() const;
|
||||
// Back-pressure when Reserve returned nullptr. NEVER call this when
|
||||
// FreeBytes() is already >= the record: Ring.h:226-233 - a nullptr with
|
||||
// enough free bytes can only mean "too big, chunk", and waiting on it
|
||||
|
||||
@@ -896,11 +896,19 @@ namespace MobileGL::MG_Remote::Wire {
|
||||
// The allocation still cannot progress after reclamation. Count this
|
||||
// blocked allocation once, not each watermark poll or each reclaimed mark.
|
||||
if (!waited) { ++m_stageReclaimWaits; waited = true; }
|
||||
if (!m_stageRetirementBell->Wait(m_control->producerParked, ready, 0, 5000)) {
|
||||
// P5e (ra, CONTRACT-P5E §2.6): drain BEFORE parking and again after waking.
|
||||
// Before, because the ring may already be full and the server already stopped
|
||||
// - in which case retiredSeq will never move and there is nothing to wake us;
|
||||
// after, because the wake may have come from a server that is about to fill it
|
||||
// again. The hook is null in every standalone codec, which has no session and
|
||||
// therefore no reverse channel to drain.
|
||||
if (m_stageWaitHook != nullptr) m_stageWaitHook(m_stageWaitSelf);
|
||||
if (!ready() && !m_stageRetirementBell->Wait(m_control->producerParked, ready, 0, 5000)) {
|
||||
MGLOG_F("MGPipe: Fatal{RetirementWaitFailed, \"SEG_STAGE\"} producer wait "
|
||||
"ended before the pending allocation retired (shutdown or timeout)");
|
||||
std::abort();
|
||||
}
|
||||
if (m_stageWaitHook != nullptr) m_stageWaitHook(m_stageWaitSelf);
|
||||
}
|
||||
ReclaimStagedBytes();
|
||||
}
|
||||
|
||||
@@ -340,6 +340,20 @@ namespace MobileGL::MG_Remote::Wire {
|
||||
// without a consumer cannot wait for retirement and retain the named refusal.
|
||||
void SetStageRetirementDoorbell(Transport::Doorbell* bell) { m_stageRetirementBell = bell; }
|
||||
|
||||
// P5e (ra, CONTRACT-P5E §2.6): THE STAGE BELL IS A CLIENT WAIT, so it has to drain the
|
||||
// reverse channel like every other one. The encoder cannot drain it itself - SEG_EVENT
|
||||
// and its consumers belong to the session - so the session installs a hook and this
|
||||
// layer only says WHEN. Without it the flow-control deadlock is one buffer upload
|
||||
// wide: the server stops applying on a full event ring, retiredSeq stops moving, and a
|
||||
// client parked here for staged bytes never drains the ring that would release it.
|
||||
// A raw function pointer rather than std::function: this header sits below MG_Impl and
|
||||
// the call is on a path that is already about to park.
|
||||
using StageWaitHook = void (*)(void* self);
|
||||
void SetStageWaitHook(StageWaitHook hook, void* self) {
|
||||
m_stageWaitHook = hook;
|
||||
m_stageWaitSelf = self;
|
||||
}
|
||||
|
||||
// Bytes this encoder has ever written into SEG_CMD, pad fillers included: the
|
||||
// producer's monotonic head cursor. It is the DENOMINATOR the wrap count only means
|
||||
// anything against - "0 wraps" is a defect when the run pushed more bytes than the ring
|
||||
@@ -383,6 +397,9 @@ namespace MobileGL::MG_Remote::Wire {
|
||||
Uint64 m_cmdWrapPads = 0;
|
||||
Uint64 m_stageReclaimWaits = 0;
|
||||
Transport::Doorbell* m_stageRetirementBell = nullptr;
|
||||
// P5e (ra): the session's event drain, called around the stage-bell park (§2.6).
|
||||
StageWaitHook m_stageWaitHook = nullptr;
|
||||
void* m_stageWaitSelf = nullptr;
|
||||
Vector<StageMark> m_stageMarks;
|
||||
SizeT m_stageMarkFront = 0;
|
||||
Uint8* m_stageBase = nullptr;
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#include <Init.h>
|
||||
#include <MG_Impl/EGLImpl/EGLImpl.h>
|
||||
@@ -2097,8 +2098,379 @@ TEST(RemoteGuards, ClientPipeInputsFillWithTheApplierIdleIsAllowed) {
|
||||
});
|
||||
ExpectChildSuccess(child);
|
||||
}
|
||||
|
||||
// ===========================================================================================
|
||||
// P5e (ra) - the wait rule, the present credit and gPipeInputs' ownership
|
||||
// (MG_Remote/CONTRACT-P5E.md §1, §2.3, §2.4, §3.1, §3.3, §3.5)
|
||||
// ===========================================================================================
|
||||
//
|
||||
// EVERY CASE BELOW ARMS RUN-AHEAD BY PUBLISHING THE CAP BIT, because that is the only thing
|
||||
// that arms it: RunAheadArmed() is a conjunction whose third term is the SERVER's own
|
||||
// statement that it applies an unbarriered record without reading client memory, and in a
|
||||
// tree where kMGPipeP5eRunAheadReady is still false no production session sets it. Driving the
|
||||
// arm through the caps mirror rather than through a test-only setter is deliberate (ID-102's
|
||||
// lesson): what these cases exercise is then the production latch and not a branch beside it.
|
||||
//
|
||||
// NONE OF THEM DRAWS. The draw path is the family packages' to migrate; until they land, a
|
||||
// draw under run-ahead would abort inside the backend for a reason that has nothing to do with
|
||||
// what is being asserted here.
|
||||
void StartRunAheadSession() {
|
||||
::alarm(15);
|
||||
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||
MG_Config::Ipc.RunAhead = 1;
|
||||
Srv::ServerSessionInstance().SetCapabilityBits(
|
||||
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply));
|
||||
Srv::ServerSessionInstance().SetConsumedSubsystems(kMGPipeSubsystemsMigratedAtP4a);
|
||||
// THE MIRROR IS ADOPTED BEFORE Start, AND THAT IS THE LATCH'S RULE MADE VISIBLE. This
|
||||
// process has no server backend, so PublishCapsSnapshot is deferred and the handshake
|
||||
// carries no snapshot at all - and §1 says a later snapshot may only turn run-ahead OFF,
|
||||
// never on. So the arm has to be true at the FIRST adoption, which for a fixture means
|
||||
// before the session takes its latch. A test that adopted afterwards and found the client
|
||||
// still lockstep would be reading the contract's own rule as a failure.
|
||||
{
|
||||
MG_Pipe::MGPCaps caps{};
|
||||
caps.CallMask = static_cast<Uint64>(MG_Pipe::kCapRunAheadApply) |
|
||||
MG_Remote::MGCapsConsumerBits(kMGPipeSubsystemsMigratedAtP4a);
|
||||
CapsMirrorInstance().Adopt(caps, MG_Backend::FormatCapabilityCache{}, RendererInfo{},
|
||||
String{"4.6"}, BackendType::DirectGLES);
|
||||
}
|
||||
if (ClientSessionInstance().Start(MG_Config::TransportMode::InProcess, {}) != MOBILEGL_OK) {
|
||||
::_exit(81);
|
||||
}
|
||||
if (!ClientSessionInstance().RunAheadArmed()) ::_exit(83); // the latch never took
|
||||
}
|
||||
|
||||
// THE SERVER HALF OF §2.4, SUBSTITUTED - and it is substituted because this fixture has no
|
||||
// backend object at all, so ServerVerbSink::OnPresent DECLINES before it can reach
|
||||
// ServerSession::ReturnPresentCredit ("present arrived with no backend object"). What the
|
||||
// cases below assert is therefore the CLIENT's half of the credit: that it pays, when, and in
|
||||
// whose id space. That the server returns exactly one credit per swap, after Present() has
|
||||
// returned, is PipeApplier::OnPresent's own three lines and the device exit's `credit-waits`
|
||||
// reading; a peer here cannot pin it without a backend to swap on.
|
||||
struct PresentCreditPeer : Codec::WireVerbSink {
|
||||
std::atomic<Uint64> lastSerial{0};
|
||||
Bool OnPresent(const MGPPresent& present) override {
|
||||
lastSerial.store(present.FrameSerial);
|
||||
Srv::ServerSessionInstance().ReturnPresentCredit(present.FrameSerial);
|
||||
return true;
|
||||
}
|
||||
void Install() {
|
||||
if (Srv::ServerLoopInstance().RunOnApplyThread(
|
||||
[](void* self) {
|
||||
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
|
||||
decoder.SetVerbSink(static_cast<PresentCreditPeer*>(self));
|
||||
return MOBILEGL_OK;
|
||||
},
|
||||
this) != MOBILEGL_OK) {
|
||||
::_exit(82);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// §1: the conjunction, and the half of it that is not the client's to decide. The cap bit is
|
||||
// absent here, so the knob alone changes nothing - which is the whole reason the knob is
|
||||
// parsed on every arm rather than only where it means something (Config.h).
|
||||
TEST(RemoteRunAhead, TheKnobAloneDoesNotArmRunAheadWithoutTheServersCapBit) {
|
||||
const auto child = RunInChild([] {
|
||||
MG_Config::Ipc.RunAhead = 1;
|
||||
StartControlSession(); // SetCapabilityBits(0)
|
||||
if (ClientSessionInstance().RunAheadArmed()) ::_exit(101);
|
||||
ClientSessionInstance().Stop();
|
||||
});
|
||||
ExpectChildSuccess(child);
|
||||
}
|
||||
|
||||
// §1 again, from the other side: MOBILEGL_IPC_VERB_BARRIER=0 is the LOCKSTEP arm's negative
|
||||
// control and it disarms run-ahead outright, because the barrier is the first term of the
|
||||
// conjunction. Under run-ahead its documented red is the first barriered row's stale pull -
|
||||
// that one is a scenario, not a unit case; what is pinned here is that the two controls do not
|
||||
// silently compose into a third mode nobody designed.
|
||||
TEST(RemoteRunAhead, ClearingTheVerbBarrierDisarmsRunAheadEvenWithTheCapBit) {
|
||||
const auto child = RunInChild([] {
|
||||
MG_Config::Ipc.RunAhead = 1;
|
||||
MG_Config::Ipc.VerbBarrier = 0;
|
||||
::alarm(15);
|
||||
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||
Srv::ServerSessionInstance().SetCapabilityBits(
|
||||
static_cast<Uint64>(MG_Pipe::kCapRunAheadApply));
|
||||
Srv::ServerSessionInstance().SetConsumedSubsystems(kMGPipeSubsystemsMigratedAtP4a);
|
||||
MG_Pipe::MGPCaps caps{};
|
||||
caps.CallMask = static_cast<Uint64>(MG_Pipe::kCapRunAheadApply) |
|
||||
MG_Remote::MGCapsConsumerBits(kMGPipeSubsystemsMigratedAtP4a);
|
||||
CapsMirrorInstance().Adopt(caps, MG_Backend::FormatCapabilityCache{}, RendererInfo{},
|
||||
String{"4.6"}, BackendType::DirectGLES);
|
||||
if (ClientSessionInstance().Start(MG_Config::TransportMode::InProcess, {}) != MOBILEGL_OK) {
|
||||
::_exit(81);
|
||||
}
|
||||
if (ClientSessionInstance().RunAheadArmed()) ::_exit(101);
|
||||
ClientSessionInstance().Stop();
|
||||
});
|
||||
ExpectChildSuccess(child);
|
||||
}
|
||||
|
||||
// §2.4's RED-ONCE, BY COUNT. With a credit of 1 the client may have one present in flight, so
|
||||
// the second and third presents each pay the credit before they encode - `credit-waits` is 2
|
||||
// after three presents and the present-ack watermark has reached 2. Delete the
|
||||
// WaitForPresentAck arm from ClientSession::AcquirePresentCredit and the counter stays 0 while
|
||||
// all three presents publish: red by count, which is the only way a wait that is usually
|
||||
// already satisfied can be tested at all.
|
||||
//
|
||||
// The serial is asserted too, because the counter alone would survive a credit paid against
|
||||
// the wrong id space (the pre-P5e FrameSerial was 0 and the server stamped its own).
|
||||
TEST(RemoteRunAhead, ACreditOneClientPaysTheCreditAtEveryPresentAfterTheFirst) {
|
||||
const auto child = RunInChild([] {
|
||||
MG_Config::Ipc.PresentCredit = 1;
|
||||
StartRunAheadSession();
|
||||
PresentCreditPeer peer;
|
||||
peer.Install();
|
||||
ClientSession& session = ClientSessionInstance();
|
||||
if (session.PresentCreditWaits() != 0) ::_exit(101);
|
||||
RemoteEmitTable().Present();
|
||||
if (session.PresentCreditWaits() != 0) ::_exit(102); // the first one is free
|
||||
RemoteEmitTable().Present();
|
||||
RemoteEmitTable().Present();
|
||||
if (session.PresentCreditWaits() != 2) ::_exit(103);
|
||||
// ONE CREDIT PER SWAP, in the client's own 1-based space: the server returned the
|
||||
// second present's credit, so the watermark is at least 2.
|
||||
if (session.Control() == nullptr ||
|
||||
session.Control()->presentAckSerial.load() < 2u) {
|
||||
::_exit(104);
|
||||
}
|
||||
session.Stop();
|
||||
});
|
||||
ExpectChildSuccess(child);
|
||||
}
|
||||
|
||||
// §2.4's other half: with the credit raised the client stops paying until it is that far
|
||||
// ahead. It is the control that keeps the case above from passing because of an unconditional
|
||||
// counter rather than because of the credit.
|
||||
TEST(RemoteRunAhead, ACreditThreeClientPaysNothingForItsFirstThreePresents) {
|
||||
const auto child = RunInChild([] {
|
||||
MG_Config::Ipc.PresentCredit = 3;
|
||||
StartRunAheadSession();
|
||||
PresentCreditPeer peer;
|
||||
peer.Install();
|
||||
ClientSession& session = ClientSessionInstance();
|
||||
RemoteEmitTable().Present();
|
||||
RemoteEmitTable().Present();
|
||||
RemoteEmitTable().Present();
|
||||
if (session.PresentCreditWaits() != 0) ::_exit(101);
|
||||
RemoteEmitTable().Present();
|
||||
if (session.PresentCreditWaits() != 1) ::_exit(102);
|
||||
session.Stop();
|
||||
});
|
||||
ExpectChildSuccess(child);
|
||||
}
|
||||
|
||||
// §3.1 / §3.5's RED-ONCE. `Clear` is a kWaitNone row, so under run-ahead the client publishes
|
||||
// it and moves on - and therefore does not fill gPipeInputs for it. This case is the GREEN
|
||||
// half: the validate point runs the tracker walk and the emitters and touches the block not at
|
||||
// all, so nothing aborts.
|
||||
//
|
||||
// THE RED: in MGPipeValidateForVerb change `const Bool fillOwed = barriered;` to `= true` -
|
||||
// which is exactly "leave one CopyField in an unbarriered fill" - and the guard below it fires
|
||||
// Fatal{RoleViolation, "gPipeInputs"} on this very call.
|
||||
TEST(RemoteRunAhead, AnUnbarrieredVerbDoesNotFillPipeInputsAndIsAllowed) {
|
||||
const auto child = RunInChild([] {
|
||||
StartRunAheadSession();
|
||||
MGPipeValidateForVerb(MGPipeVerb::Clear);
|
||||
MGPipeLeaveVerb();
|
||||
ClientSessionInstance().Stop();
|
||||
});
|
||||
ExpectChildSuccess(child);
|
||||
}
|
||||
|
||||
// §3.5, the aborting arm, driven at the guard itself: a GL-thread touch that is NOT the
|
||||
// residual fill of a barriered record. Under lockstep this same call is a no-op (the apply
|
||||
// thread is not inside the applier and MOBILEGL_IPC_BATCH_WAITS is 1); under run-ahead there
|
||||
// is no such window to be outside of, because the client never parks for an unbarriered
|
||||
// record - so the answer stops depending on timing and becomes the rule.
|
||||
TEST(RemoteGuards, ClientPipeInputsTouchOutsideABarrieredFillUnderRunAheadIsFatalByName) {
|
||||
const auto child = RunInChild([] {
|
||||
StartRunAheadSession();
|
||||
ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt("ra-test-surface",
|
||||
/*isBarrieredFill=*/false);
|
||||
ClientSessionInstance().Stop();
|
||||
});
|
||||
ExpectNamedAbort(child, "Fatal{RoleViolation, \"gPipeInputs\"}");
|
||||
}
|
||||
|
||||
// ... and the control: the barriered fill says so and is let through. Without this the case
|
||||
// above would pass just as well against "abort unconditionally".
|
||||
TEST(RemoteGuards, ClientPipeInputsTouchInsideABarrieredFillUnderRunAheadIsAllowed) {
|
||||
const auto child = RunInChild([] {
|
||||
StartRunAheadSession();
|
||||
ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt("ra-test-surface",
|
||||
/*isBarrieredFill=*/true);
|
||||
ClientSessionInstance().Stop();
|
||||
});
|
||||
ExpectChildSuccess(child);
|
||||
}
|
||||
|
||||
// §3.3's RED-ONCE, and the one that turns the strict lane into a gate. The probe is the
|
||||
// sticky forward itself, run on the apply thread inside a server-stamped verb with the
|
||||
// current record marked UNBARRIERED - which is the state the sink puts that thread in for
|
||||
// every kWaitNone row once the wait rule is live. MOBILEGL_IPC_STRICT_ERRORS is deliberately
|
||||
// NOT set: the point is that the knob has stopped being the deciding input, because a row the
|
||||
// client never filled has no value to count.
|
||||
//
|
||||
// THE RED: put CountBarrierPull's first line back to `if (MG_Config::Ipc.StrictErrors)` only
|
||||
// and this case stops aborting - and, on the lane, every scenario that still pulls a row goes
|
||||
// from a named abort to a wrong picture with a number beside it.
|
||||
TEST(RemoteGuards, AResidualPullUnderAnUnbarrieredRecordIsFatalWithoutTheStrictKnob) {
|
||||
const auto child = RunInChild([] {
|
||||
MG_Config::Ipc.StrictErrors = false;
|
||||
StartRunAheadSession();
|
||||
Srv::ServerLoopInstance().RunOnApplyThread(
|
||||
+[](void*) -> MobileGLResult {
|
||||
MG_Pipe::MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
|
||||
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(false);
|
||||
MG_Pipe::MGPipeStickyForwardPull(MGPipeInputField::GetProgramObject);
|
||||
return MOBILEGL_OK;
|
||||
},
|
||||
nullptr);
|
||||
ClientSessionInstance().Stop();
|
||||
});
|
||||
ExpectNamedAbort(child, "Fatal{UnmigratedPipeInput, \"GetProgramObject@Clear\"}");
|
||||
}
|
||||
|
||||
// The control: the SAME probe inside a BARRIERED record is P5C's counted residual pull, and
|
||||
// with the strict knob off it counts and carries on. Ruling 4 is this line - a barriered
|
||||
// record keeps P5C's semantics exactly, because the client is parked in its own wait.
|
||||
TEST(RemoteGuards, AResidualPullUnderABarrieredRecordStillOnlyCounts) {
|
||||
const auto child = RunInChild([] {
|
||||
MG_Config::Ipc.StrictErrors = false;
|
||||
StartRunAheadSession();
|
||||
Srv::ServerLoopInstance().RunOnApplyThread(
|
||||
+[](void*) -> MobileGLResult {
|
||||
MG_Pipe::MGPipeServerStampVerbBoundary(MGPipeVerb::Clear);
|
||||
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true);
|
||||
const Uint64 before = MG_Pipe::MGPipeResidualPullCount();
|
||||
MG_Pipe::MGPipeStickyForwardPull(MGPipeInputField::GetProgramObject);
|
||||
if (MG_Pipe::MGPipeResidualPullCount() != before + 1) ::_exit(101);
|
||||
return MOBILEGL_OK;
|
||||
},
|
||||
nullptr);
|
||||
ClientSessionInstance().Stop();
|
||||
});
|
||||
ExpectChildSuccess(child);
|
||||
}
|
||||
|
||||
// §2.6's RED-ONCE, and the one the brief names: ~300 KiB of kEventGpuWritten published from
|
||||
// the apply thread behind a sequence nobody waited for. SEG_EVENT is 256 KiB, so this is
|
||||
// several ringfuls; under P5C's rule the FIRST Reserve that failed was Fatal{EventRingOverflow}
|
||||
// and this case aborts by that name. With flow control the producer publishes, rings, parks on
|
||||
// the latch and retries, and the burst completes.
|
||||
//
|
||||
// THE DRAIN RUNS ON A THREAD OF ITS OWN, and that is forced by the fixture rather than chosen:
|
||||
// RunOnApplyThread is synchronous, so the thread that posted the burst cannot also be the
|
||||
// thread that empties the ring. In production it is the GL thread draining at its own waits
|
||||
// (§2.6's deadlock argument); here it is a drainer beside the poster, which puts the producer
|
||||
// in exactly the state that argument describes.
|
||||
TEST(RemoteRunAhead, AnEventBurstBehindAnUnwaitedSequenceFlowControlsInsteadOfAborting) {
|
||||
const auto child = RunInChild([] {
|
||||
StartRunAheadSession();
|
||||
std::atomic<bool> stop{false};
|
||||
std::atomic<Uint64> delivered{0};
|
||||
std::thread drainer([&stop, &delivered] {
|
||||
// THE FIRST DRAIN IS DELAYED ON PURPOSE. A drainer that starts immediately can
|
||||
// keep a 256 KiB ring from ever being full, and a red-once that depends on the
|
||||
// scheduler losing a race is not a red-once. 50 ms is four orders of magnitude
|
||||
// more than the burst needs to fill the ring, so the producer is provably parked
|
||||
// on the latch before anything empties it.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
while (!stop.load(std::memory_order_acquire)) {
|
||||
delivered.fetch_add(ClientSessionInstance().DrainPublishedEvents());
|
||||
std::this_thread::yield();
|
||||
}
|
||||
delivered.fetch_add(ClientSessionInstance().DrainPublishedEvents());
|
||||
});
|
||||
// 3000 records of 64 ranges each is ~3 MB against a 256 KiB ring - twelve ringfuls,
|
||||
// with a drainer racing it - so the full latch is hit many times over rather than
|
||||
// maybe once. That margin is what makes the red-once (restore the Fatal) reliable:
|
||||
// a burst that merely might fill the ring would be a red-once that merely might be red.
|
||||
Srv::ServerLoopInstance().RunOnApplyThread(
|
||||
+[](void*) -> MobileGLResult {
|
||||
MG_Pipe::MGPRange ranges[64];
|
||||
for (Uint32 r = 0; r < 64; ++r) ranges[r] = MG_Pipe::MGPRange{r * 64ull, 64ull};
|
||||
for (Uint32 i = 0; i < 3000; ++i) {
|
||||
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(MG_Pipe::MGPipeHandle{7, 1}, 64, ranges);
|
||||
}
|
||||
return MOBILEGL_OK;
|
||||
},
|
||||
nullptr);
|
||||
stop.store(true, std::memory_order_release);
|
||||
drainer.join();
|
||||
// LOSSLESS, which is the property flow control had to preserve: every one of the 3000
|
||||
// records crossed. A drop policy would have been the other way to survive a full ring,
|
||||
// and P5C's §4.4 refuses it - a writeback or a GPU-write mark that did not arrive is a
|
||||
// stale buffer, not a missing statistic.
|
||||
if (delivered.load() < 3000u) ::_exit(101);
|
||||
ClientSessionInstance().Stop();
|
||||
});
|
||||
ExpectChildSuccess(child);
|
||||
}
|
||||
|
||||
// §2.7 / ruling 13: the deferred-destroy queue stays, and an enqueue from an unbarriered
|
||||
// apply is the finding. Under strict it is the abort; this is the arm the lane owns.
|
||||
TEST(RemoteGuards, ADeferredDestroyFromAnUnbarrieredApplyIsFatalUnderStrict) {
|
||||
const auto child = RunInChild([] {
|
||||
MG_Config::Ipc.StrictErrors = true;
|
||||
StartRunAheadSession();
|
||||
Srv::ServerLoopInstance().RunOnApplyThread(
|
||||
+[](void*) -> MobileGLResult {
|
||||
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(false);
|
||||
(void)MG_Pipe::MGPipeDeferDestroyAndFreeIfOnApplyThread(MG_Pipe::MGPipeKind::Texture,
|
||||
4242);
|
||||
return MOBILEGL_OK;
|
||||
},
|
||||
nullptr);
|
||||
ClientSessionInstance().Stop();
|
||||
});
|
||||
ExpectNamedAbort(child, "Fatal{RoleViolation, \"deferred-destroy\"}");
|
||||
}
|
||||
|
||||
// The control, and the whole of ruling 13: a BARRIERED apply may still be a last owner (its
|
||||
// fill's O-class rows, XFB's pinned targets), so the queue takes the death and the GL thread
|
||||
// replays it. Deleting the queue would have made this an allocator touch from the server role.
|
||||
TEST(RemoteGuards, ADeferredDestroyFromABarrieredApplyIsStillServed) {
|
||||
const auto child = RunInChild([] {
|
||||
MG_Config::Ipc.StrictErrors = true;
|
||||
StartRunAheadSession();
|
||||
Srv::ServerLoopInstance().RunOnApplyThread(
|
||||
+[](void*) -> MobileGLResult {
|
||||
MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true);
|
||||
if (!MG_Pipe::MGPipeDeferDestroyAndFreeIfOnApplyThread(MG_Pipe::MGPipeKind::Texture,
|
||||
4243)) {
|
||||
::_exit(101);
|
||||
}
|
||||
return MOBILEGL_OK;
|
||||
},
|
||||
nullptr);
|
||||
MG_Pipe::MGPipeDrainDeferredDestroys();
|
||||
ClientSessionInstance().Stop();
|
||||
});
|
||||
ExpectChildSuccess(child);
|
||||
}
|
||||
#endif
|
||||
|
||||
// §2.5 / ruling 15 (ID-93): which half of resource_subdata wants its answer. One predicate,
|
||||
// read by MGPipeRouteResourceSubData and by Wire_ResourceSubData - a second spelling of it is
|
||||
// how the client comes to wait for an answer the emitter told the server not to bother with.
|
||||
// The buffer target's whole packed field is 0 (MGPipeTypes.h asserts it beside the packer),
|
||||
// which is what makes the test a comparison and not a mask.
|
||||
TEST(RemoteRunAhead, OnlyTheTextureHalfOfResourceSubDataWantsItsReply) {
|
||||
MG_Pipe::MGPSubData buffer{};
|
||||
buffer.Target = MG_Pipe::MGPipePackSubDataTarget(MG_Pipe::kMGPipeResourceTargetBuffer, 0u);
|
||||
EXPECT_FALSE(MG_Pipe::MGPipeSubDataWantsItsReply(buffer));
|
||||
|
||||
MG_Pipe::MGPSubData texture{};
|
||||
texture.Target = MG_Pipe::MGPipePackSubDataTarget(
|
||||
static_cast<Uint32>(MG_Pipe::MGPipeResourceTarget::Tex2D),
|
||||
static_cast<Uint32>(TextureUploadTarget::Texture2D));
|
||||
EXPECT_TRUE(MG_Pipe::MGPipeSubDataWantsItsReply(texture));
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -550,6 +550,122 @@ TEST(SessionTest, PresentAckSerialIsWaitedOnWithGreaterOrEqualAndWakesThroughThe
|
||||
EXPECT_EQ(session->Control().producerParked.load(), 0u);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// P5e (ra) - SEG_EVENT flow control (MG_Remote/CONTRACT-P5E.md §2.6)
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// THE DEADLOCK THE CONTRACT ARGUES AWAY HAS TWO HALVES AND THIS IS THE FIRST. A run-ahead
|
||||
// server that cannot Reserve on SEG_EVENT stops producing and therefore stops applying, so
|
||||
// appliedSeq stops moving - and the client parked on appliedSeq is holding the only drain
|
||||
// there is. A waiter that could ONLY be woken by the watermark would wait for a number nobody
|
||||
// is going to publish.
|
||||
//
|
||||
// Red-once: point the case at WaitForApplied instead of WaitForAppliedOrEventBacklog and the
|
||||
// join below never returns - the 5 s deadline fires and the wait reads TimedOut.
|
||||
TEST(SessionTest, AFullEventRingWakesAClientParkedOnAppliedSeq) {
|
||||
auto session = std::make_shared<SessionFixture>();
|
||||
ASSERT_TRUE(session->Build(TestSizes()));
|
||||
|
||||
std::atomic<bool> released{false};
|
||||
std::atomic<SessionWait> result{SessionWait::TimedOut};
|
||||
std::thread parked([session, &released, &result] {
|
||||
result.store(session->producer.WaitForAppliedOrEventBacklog(4, 5000));
|
||||
released.store(true, std::memory_order_release);
|
||||
});
|
||||
|
||||
while (session->Control().producerParked.load() == 0 && !released.load()) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
// NOT the watermark. The server latches "SEG_EVENT is full and I stopped" and rings; the
|
||||
// client's business is to drain, not to conclude that its record was applied.
|
||||
session->Control().eventRingFull.store(1, std::memory_order_release);
|
||||
session->consumer.NotifyClient();
|
||||
|
||||
parked.join();
|
||||
EXPECT_TRUE(released.load());
|
||||
EXPECT_EQ(result.load(), SessionWait::Reached);
|
||||
// AND THE WATERMARK IS UNTOUCHED, which is what makes the caller's re-read the right way
|
||||
// to tell the two wakeups apart: a third SessionWait value would have turned every
|
||||
// existing `== Reached` site into a bug.
|
||||
EXPECT_EQ(session->Control().appliedSeq.load(), 0u);
|
||||
EXPECT_TRUE(session->producer.EventRingIsFull());
|
||||
}
|
||||
|
||||
// The same for the present credit, because a credit-paced client parks there for a whole frame
|
||||
// at a time and is the likeliest waiter to be holding the drain when the ring fills.
|
||||
TEST(SessionTest, AFullEventRingWakesAClientParkedOnThePresentCredit) {
|
||||
auto session = std::make_shared<SessionFixture>();
|
||||
ASSERT_TRUE(session->Build(TestSizes()));
|
||||
|
||||
std::atomic<bool> released{false};
|
||||
std::atomic<SessionWait> result{SessionWait::TimedOut};
|
||||
std::thread parked([session, &released, &result] {
|
||||
result.store(session->producer.WaitForPresentAckOrEventBacklog(2, 5000));
|
||||
released.store(true, std::memory_order_release);
|
||||
});
|
||||
|
||||
while (session->Control().producerParked.load() == 0 && !released.load()) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
session->Control().eventRingFull.store(1, std::memory_order_release);
|
||||
session->consumer.NotifyClient();
|
||||
|
||||
parked.join();
|
||||
EXPECT_TRUE(released.load());
|
||||
EXPECT_EQ(result.load(), SessionWait::Reached);
|
||||
EXPECT_EQ(session->Control().presentAckSerial.load(), 0u);
|
||||
}
|
||||
|
||||
// THE SECOND HALF: the drain that empties the ring must RING, not merely clear. A cleared flag
|
||||
// with no bell is the lost wakeup the forward direction's publish-then-ring order exists to
|
||||
// prevent, and the server parked on this flag is inside a producer that cannot return without
|
||||
// placing its event.
|
||||
//
|
||||
// THE BELL IS COUNTED RATHER THAN WAITED ON, and that is the difference between a test and a
|
||||
// coincidence: a real doorbell REMEMBERS a notify delivered while nobody was parked and can
|
||||
// return from a park for reasons of its own, so a thread that happened to wake proves nothing
|
||||
// about who rang. This one records every Notify().
|
||||
//
|
||||
// Red-once: drop the NotifyIfParked from EventRingConsumer::Drained() and `rings` stays 0.
|
||||
namespace {
|
||||
struct CountingBell : Doorbell {
|
||||
std::atomic<std::uint64_t> rings{0};
|
||||
void Notify() override { rings.fetch_add(1, std::memory_order_relaxed); }
|
||||
bool Park(std::uint32_t) override { return false; }
|
||||
void Reset() override {}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST(SessionTest, DrainingAFullEventRingClearsTheLatchAndRingsTheServer) {
|
||||
auto session = std::make_shared<SessionFixture>();
|
||||
ASSERT_TRUE(session->Build(TestSizes()));
|
||||
|
||||
CountingBell serverBell;
|
||||
// The bell the CLIENT rings is the one the SERVER parks on - which is which is the
|
||||
// session's knowledge and not the transport's (contract §3.9), so the fixture spells the
|
||||
// pair the same way ClientSession::Start does.
|
||||
session->eventIn.SetServerDoorbell(&serverBell);
|
||||
|
||||
// A drain with the latch DOWN is the steady state - every wait exit drains - and it must
|
||||
// not ring: a store to a shared cache line per drain is what the doorbell design exists to
|
||||
// avoid (Doorbell.h's bidirectional argument).
|
||||
session->eventIn.Drained();
|
||||
EXPECT_EQ(serverBell.rings.load(), 0u);
|
||||
|
||||
// Now the server is parked on the latch, which is what the flag means.
|
||||
session->Control().eventRingFull.store(1, std::memory_order_release);
|
||||
session->Control().consumerParked.store(1, std::memory_order_release);
|
||||
session->eventIn.Drained();
|
||||
|
||||
EXPECT_EQ(session->Control().eventRingFull.load(), 0u);
|
||||
EXPECT_EQ(serverBell.rings.load(), 1u);
|
||||
|
||||
// ONE-SHOT: the latch is down again, so the next drain is silent. Without the exchange -
|
||||
// a plain store plus a read - a steady stream of drains would ring every time.
|
||||
session->eventIn.Drained();
|
||||
EXPECT_EQ(serverBell.rings.load(), 1u);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// kRecPad (R-9's last sentence, and the one with no other detector)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -412,12 +412,18 @@ server 没有第二份 `BufferObject`,不存在"staging → server 侧 shadow"
|
||||
|
||||
- 不设"records ≥ 64 KiB"一类阈值。规则:每条记录(或每 8–16 条摊销)release-store `cmdHead`,仅当 `consumerParked` 时敲门铃。
|
||||
- 显式门铃点:`present`、任何 `kNeedsAck` 请求、`eglMakeCurrent`、`glFlush`、`SEG_STAGE` 余量 < 1/4、**轮询类入口**(`glClientWaitSync` 任意 timeout、`glGetSynciv(GL_SYNC_STATUS)`、`glGetQueryObject*(AVAILABLE|NO_WAIT)`);带 `GL_SYNC_FLUSH_COMMANDS_BIT` 无条件 publish。饥饿升级:同一 handle 连续 N 次(`MOBILEGL_IPC_POLL_ESCALATE`,默认 64)本地回答"未就绪"而 watermark 毫无移动 → 升级为一次阻塞 round trip(P10)。
|
||||
- `glFinish`/`glFlush` 保持纯 no-op。seq = 记录序数;两个互相独立的窗口:字节 credit 与 present credit(`presentsSent - presentAckSerial >= MOBILEGL_IPC_PRESENT_CREDIT` 时 `eglSwapBuffers` 阻塞)。server 不发 credit 消息,consumer 每 64 条记录更新一次 `appliedSeq`。
|
||||
- `glFlush` 保持纯 no-op。**`glFinish` 在 P5e 之后不再是**:wire 上仍然没有它的记录,但"至此发出的命令都已完成"是一条 run-ahead 队列能违背的承诺,所以它变成 `WaitForApplyToCatchUp`——等到 `appliedSeq` 追上最后一条发布的记录,再排空反向通道(`CONTRACT-P5E.md` §2.5)。lockstep 下它照旧什么都不做,因为那里 client 本来就把自己发出的每条命令都等完了。
|
||||
- seq = 记录序数;两个互相独立的窗口:字节 credit 与 present credit(`presentsSent - presentAckSerial >= MOBILEGL_IPC_PRESENT_CREDIT` 时 `eglSwapBuffers` 阻塞)。server 不发 credit 消息,consumer 每 64 条记录更新一次 `appliedSeq`。
|
||||
- **P5e:等待规则由生成的 `WaitClass` 列决定,不再由 call class 决定。**`MGPipeWaitClassFor(op)`(`PipeCalls.def` 的第五列)把每一行分成 `kWaitReply` / `kWaitPresent` / `kWaitApplied` / `kWaitNone`,`EmitAndWaitTails` 在 `RunAheadArmed()` 为真时照这一列走:`kWaitNone` 发布即返回,`kWaitApplied` 等一次 apply 再放掉 fill 的四个 O 类 pin,`kWaitPresent` 的等待已经在 `EmitPresent` 编码之前付过(下条)。它**替换**而不是叠加 `MOBILEGL_IPC_BATCH_WAITS` 的跳过规则:batch 读的是 call class,这一列读的是"这条记录的 apply 会读什么",而 server 侧的 `MGPipeBarriered` 读的是后者——两条规则并存就是线两端对"client 现在是否被 park 着"给出不同答案。
|
||||
- **强制等待点**(§2.5):`MapBuffer(READ)` / `GetBufferSubData` / `CopyBufferSubData` 源走 `SyncGpuWrites`(reply 行,不变);`glFinish` 如上;每一个 `Server*` EGL forwarder 之前等一次——control mailbox 是在两批 drain **之间**泵的,make-current 落在两条 run-ahead 记录中间不是画错一个像素而是全错。`ServerSwapEGLBuffers` 不在此列:present 就是那次 swap。`glGetError` 的放宽也在这里具名:run-ahead verb 的 `kEventGlError` 在下一个排空点被看到,即最多晚一个 present credit。
|
||||
|
||||
### 11.7 事件回传与溢出(P9)
|
||||
### 11.7 事件回传与溢出(P9 定策略,P5e 落地)
|
||||
|
||||
`SEG_EVENT` 承载十个回调加回读完成通知。client 排空点:`glGetError`、`glGetQueryObject*`、`glClientWaitSync`、`glGetSynciv`、`eglSwapBuffers`、`glMapBuffer*`/`glGetBufferSubData`/`glCopyBufferSubData`,以及每一次等待循环的每一轮。溢出策略(修一个双向死锁):`EvLogLine` ≤WARN 有损;语义承载事件无损——ring 满时 server 置 `eventRingFull=1`、**在记录边界停止 apply**、敲反向门铃,client 排空后清标志并敲正向门铃。故障注入:client 被 credit 阻塞时灌满 `SEG_EVENT`;日志洪泛下注入一次 link 失败,那行 ERROR 必须出现且两侧恢复。
|
||||
|
||||
**P5e 把上面这段从计划变成代码(`CONTRACT-P5E.md` §2.6)。**`Fatal{EventRingOverflow}` 的前提是"client 每个 verb 都排空,所以 256 KiB 满环是没有工作负载会产生的突发";run-ahead 的 client 在自己的等待点排空,稳态上相隔一个 present,满环因此是普通积压,为它中止等于把反向通道的容量变成工作负载上限。现在 producer 改为阻塞:发布已在环里的部分、敲 client 门铃、park 在自己的 bell 上等标志清掉、重试。两条具名拒绝留下,而且是**不可能**而非**繁忙**的情形——单条事件大于半个环(再怎么排空也放不下),以及 30 秒都等不到排空(对端不在跑)。不发布 `kCapRunAheadApply` 的 server 原样保留 P5C 的 Fatal。
|
||||
死锁论证要两半才成立,所以两半都在:`EventRingConsumer::Drained()` **清标志的同时敲门铃**(清了不敲就是正向那条"先 publish 再敲"专门在防的丢唤醒),而 `SessionProducer::WaitForAppliedOrEventBacklog` / `WaitForPresentAckOrEventBacklog` 让 client 的每一次 park 都能被 `eventRingFull` 打断——于是"等着 client 排空的 server" 总能叫醒那个 client。`ServerLoop` 的 park 条件因此多一条:环满时等待中的记录**还不是**工作,因为 apply 它只会让 producer 停在半路;Stop 与 Control 仍然优先,拆机不被一个不排空的 client 拖住。
|
||||
|
||||
### 11.8 fence 与无 present 负载(P10)
|
||||
|
||||
fence 完成度必须来自**真的逐 fence 退休**,不是 present 水位(DirectGLES 的 `g_completedFrameSerial` 只在 `Present()` 里前进,帧中 fence 会退化成帧计数推断——`DirectVulkan.cpp` 写明这是被修掉的 bug)。无 present 循环下 `retiredTail` 会饿死:DirectGLES 的 server 加**非 present fence tick**(距上次 `Present` 超过 8 ms 或每 4096 条已 apply 记录插一个 `glFenceSync`);P8 加一个无 present 的 split 用例。P5b 已把五条 sync 槽搬上 apply 线程(§17.5)。
|
||||
@@ -466,7 +472,9 @@ fence 完成度必须来自**真的逐 fence 退休**,不是 present 水位(
|
||||
## 14. Present、线程与帧节奏
|
||||
|
||||
- `eglSwapBuffers` → `present{frameSerial}` → publish + 敲门铃 → 返回,除非超出 credit。**`present` 与 `eglSwapBuffers` 严格 1:1**:两个后端的帧边界排空只在 `Present` 内发生,批量会饿死它们。
|
||||
- **`MOBILEGL_IPC_PRESENT_CREDIT` 默认 1**(P10):延迟叠加,server 的 `Present` 末尾已在 `vkWaitForFences` 上等 2–3 帧,credit 2 就是端到端 4–5 帧;只有实测吞吐收益能抵掉延迟代价才调高。Magma 从不注册 `SetSwapInterval`,IPC credit 是它唯一的显式限帧器。
|
||||
- **`MOBILEGL_IPC_PRESENT_CREDIT` 默认 1**(P10 提出,P5e 落地,range 1..8):延迟叠加,server 的 `Present` 末尾已在 `vkWaitForFences` 上等 2–3 帧,credit 2 就是端到端 4–5 帧;只有实测吞吐收益能抵掉延迟代价才调高。Magma 从不注册 `SetSwapInterval`,IPC credit 是它唯一的显式限帧器。
|
||||
- **P5e:credit 是 run-ahead 的唯一稳态背压,字节不是**(`CONTRACT-P5E.md` §2.4)。`MGPPresent::FrameSerial` 从"0,由 server 盖自己的帧数"改成**由 client 铸造、1 起算**——一个 credit 只能在付账方能推进的 id 空间里计量,而付账方是 client;`ServerVerbSink::OnPresent` 在 `Present()` 返回之后调 `ServerSession::ReturnPresentCredit(FrameSerial)`,一次 swap 一个 credit(那个函数自 v1 写下以来一直没有生产调用者,这是它的第一个)。credit 1 = client 在 server 应用并交换第 N 帧时发布第 N+1 帧的记录:一帧重叠,最多一帧附加延迟。等待发生在**编码之前**——先编码再 park 会把一次 SEG_CMD 预留持有整整一帧 server 时间。run-ahead server 上 `FrameSerial == 0` 是 `Fatal{ProtocolCorruption, "Present.FrameSerial"}`。
|
||||
- 预算(~850 draw + ~0.36 MB pmap/帧):SEG_CMD 每帧 ~70 KB 对 8 MiB,SEG_STAGE ~1 MB/帧对 32 MiB,SEG_REPLY 不变。`WaitForCmdSpace` 与 stage bell 是安全网而不是节奏器,两者退出时都排空 SEG_EVENT。stats 行与 wire ledger 多一个 `credit-waits`:credit 1 下它是 0 就说明 server 从来不在帧的关键路径上,接近帧数就说明它一直是。
|
||||
- 线程——client:**v1 不加线程**,编码在 GL 线程上直接写 ring;外来线程的 sync/query 读从 `RingControl` 无锁回答,必须发射的少数取 `ctrlMutex` 走 `AuxRequest`(SPSC ring 不允许第二个 producer)。server:`mgl-srv-io`(封帧、`SCM_RIGHTS`、doorbell、CTRL RPC)、`mgl-srv-apply`(**终身持有原生 context**,`MakeCurrent` 的缓存失效风暴变启动期一次性)。
|
||||
- **核心放置**:全库无亲和性控制。规则:报总 CPU 工作量差(client tracker + encode + decode + server apply vs monolith `PrepareForDraw`);复用 `ShaderCompilePool` 的大核探测把 `mgl-srv-apply` 绑到大核(`MOBILEGL_IPC_SERVER_AFFINITY`,默认 auto)。
|
||||
- 拆机顺序:publish + server 排空并 ack → 停 apply 线程 → 关 transport → client 排空 compile pool → `MobileGL::Destroy()` → 释放 sync/query handle(P5 落地形状见 §17.3)。
|
||||
@@ -571,6 +579,16 @@ P5c 的设计决定(六条全部落地,落地形状与偏差以 `MobileGL/MG
|
||||
|
||||
落地时的三条结构性补充(契约 §3.1 与 §5.4 的具名豁免):审计漏了三族无句柄站点——绑定记录 P4b 才发射的 ensure/通告族(`MGPipeReverseAnnouncementScope`)、G6 前端键 twin registry(`MGPipeFrontendKeyedRegistryScope`,P3b/P4b 重键)、Magma 拆除期的隐藏资源;两个 scope 内的只读探测是具名、可 grep、带退役阶段的债,不是守卫的洞。
|
||||
|
||||
### 17.7 P5e:client 在 Espryt draw 路径上跑在 apply 前面(权威为 `MobileGL/MG_Remote/CONTRACT-P5E.md`)
|
||||
|
||||
P5d 把 `inproc` 变成 client 线程 CPU-bound,帧仍是 `client 工作 +(每 draw)等 apply + 交接`。那个等待存在,是因为 draw 的 apply 仍在解引用 client 拥有的内存:`FieldOwnership.def` 的对象类 BARRIER-PULLED 行,以及两个 P5C 具名豁免 scope 里的前端键 twin registry。**P5e 的一句话:client 发布完就走;server 用记录带的句柄解析每一个对象;它唯一可以读的 client 内存,是一条 client 确实被挡在后面的记录的残余 fill。**
|
||||
|
||||
- **barriered 谓词**(§2.1,裁定 3 / ID-83):`MGPipeBarriered(op, payload, applierState)` = 静态 `WaitClass` 列 ≠ `kWaitNone`,或 XFB span 打开时的 `kCtxVerb`,或带 `kDrawClientArrays` 的 draw。两侧算同一个函数、同一份数据——client 在 emit 时用 `ctx.IsTransformFeedbackActive()`,server 用应用过的 `MGPContextValues` 镜像,而 `set_context_values` 在 ring 上先于 verb,所以两者一致。没有第四条升级;加一条是整合者的裁定加契约里的一行。
|
||||
- **规则 F**(§0):**unbarriered 记录的 apply 不读任何 client 内存**——不解引用前端对象、不读 BARRIER-PULLED 行、不探测也不铸造 `MGPipeSlots()`、不持有前端对象的 `SharedPtr`。违反是具名 Fatal,**与 `MOBILEGL_IPC_STRICT_ERRORS` 无关**:那个值按构造就是撕裂或陈旧的,没有"计一笔"的臂。barriered 记录原样保留 P5C 的语义(裁定 4 / ID-84),这正是第一次落地能做小的原因:回读、CopyTex、`GetTexImage`、`set_storage_block_binding`、XFB 都留在 barriered 且拖后。
|
||||
- **`gPipeInputs` 变成 server 角色内存**(§3):unbarriered verb 的 validate 只跑第 2 步(tracker 走查)和第 3 步(发射器)——它们产出记录,这正是 unbarriered verb 被允许交给 server 的全部——序号自增、verb 戳、撤回 server 戳与整个第 4 步都不跑。`MGPipeInputUnfreshRead` 的计数器在 unbarriered 记录下变成**无条件中止**,这是把 `integration-split-strict` 从"预期红"变成硬绿门的那一处(§7)。
|
||||
- **run-ahead 的臂**:`RunAheadArmed()` = `m_barrierArmed && Ipc.RunAhead && Caps().HasCap(kCapRunAheadApply)`,在 `Start` 之后第一次 caps 采纳时**锁存**,此后只能被关掉、不能被打开——已经不等就发布出去的记录收不回来。Magma 的 `CallMask` 永远不带 bit 10(`MGPipeRunAheadCapBitsFor` 是纯函数,`MagmaPipeIdentityTest` 钉住),Espryt 则由 `MG_Backend/Init.cpp` 的 `kMGPipeP5eRunAheadReady` 一个常量把整相位关在惰性里,直到整合提交把它翻过来。
|
||||
- **对照臂**:`MOBILEGL_IPC_RUN_AHEAD=0` 是 A/B 的**对照**而不是负控制——server 代码、fill 决定、每一条记录两臂都一样,唯一的差别是 client 等不等;`MOBILEGL_IPC_VERB_BARRIER=0` 保留它自己的意思,并且因为它是那个合取式的第一项,它会直接把 run-ahead 关掉。
|
||||
|
||||
## 附 A:开关
|
||||
|
||||
CMake:
|
||||
@@ -610,13 +628,15 @@ CMake:
|
||||
| `MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS` | 1 | 推送只发内容变了的块(追踪 buffer 走 mprotect 位图,未追踪走内容哈希);`0` 恢复全范围推送(A/B 对照) |
|
||||
| `MOBILEGL_IPC_BATCH_WAITS` | 1 | 值类记录(无 reply slot 的 kCtxState/kCtxCso/kCtxObject)发布即返回,barrier 推迟到下一个拉取类 verb;**`generate_mipmap` 例外**(它的 apply 读 `MGB_CTX->GetActiveTextureUnit()`,规则:只有 apply 不读残余填充字段的记录才可免等);`0` 恢复 R-1 逐条 barrier(`MOBILEGL_PIPE_VERIFY` 强制 0) |
|
||||
| `MOBILEGL_IPC_ADOPT_TIER` | 2 | `auto/0/1/2`;P5 split 用 emulated(T2) |
|
||||
| `MOBILEGL_IPC_VERB_BARRIER` | 1 | 每 verb 等 `appliedSeq == emitSeq`;`0` 只作 E1 阴性对照 |
|
||||
| `MOBILEGL_IPC_VERB_BARRIER` | 1 | 每 verb 等 `appliedSeq == emitSeq`;`0` 只作 E1 阴性对照,并且因为它是 `RunAheadArmed()` 合取式的第一项,它同时把 run-ahead 关掉 |
|
||||
| `MOBILEGL_IPC_RUN_AHEAD` | 1 | P5e:发布完 unbarriered 记录即返回(§11.6 的 `WaitClass` 列)。**只是合取式的一半**——server 不发布 `kCapRunAheadApply` 时它等于 0 并日志一次;`0` 是 **A/B 对照**(两臂 server 代码与记录完全相同,只差 client 等不等),不是阴性对照。`MOBILEGL_PIPE_VERIFY` 强制 0 |
|
||||
| `MOBILEGL_IPC_PRESENT_CREDIT` | 1 | P5e:client 可以在手的 present 数,range 1..8。credit 1 = 一帧重叠、最多一帧附加延迟;2 是设备测量臂。stats 行的 `credit-waits` 是它真正阻塞的次数 |
|
||||
| `MOBILEGL_IPC_STRICT_ERRORS` | 0 | BARRIER-PULLED residual input 提升为具名 Fatal |
|
||||
| `MOBILEGL_IPC_AUDIT` | 0 | retire 后 `0xDD` 填退休 staging |
|
||||
| `MOBILEGL_IPC_SERVER_AFFINITY` | `auto` | apply 线程亲和性;日志报告内核实际采纳的掩码(Redmi 的内核对 app 线程一律忽略 `sched_setaffinity`,解析为 0xff) |
|
||||
| `MOBILEGL_IPC_SERVER_PATH` | 空 | P6 消费 |
|
||||
|
||||
P6+ 生效:`MOBILEGL_IPC_PRESENT_CREDIT`、`MOBILEGL_IPC_POLL_ESCALATE`、shadow shm、`MOBILEGL_IPC_RESPAWN`、`MOBILEGL_IPC_IDLE_EXIT_S`。显式不设立:`MOBILEGL_IPC_PROGRAM`(没有 relink 档)、`MOBILEGL_IPC_VALIDATE_SERVER`(server 没有 `MG_Impl` 校验器)。
|
||||
P6+ 生效:`MOBILEGL_IPC_POLL_ESCALATE`、shadow shm、`MOBILEGL_IPC_RESPAWN`、`MOBILEGL_IPC_IDLE_EXIT_S`(`MOBILEGL_IPC_PRESENT_CREDIT` 已在 P5e 生效,见上表)。显式不设立:`MOBILEGL_IPC_PROGRAM`(没有 relink 档)、`MOBILEGL_IPC_VALIDATE_SERVER`(server 没有 `MG_Impl` 校验器)。
|
||||
|
||||
## 附 B:边界计数器(`MobileGL/MG_Util/Metrics/PipeStats.h`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user