mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Feat] (MG_Remote): fence polls are answered from the server's reports - the apply thread posts kEventFenceSignaled as the GPU finishes each client fence (and submits and re-checks while idle), glClientWaitSync(0) / glGetSynciv answer locally, and only every MOBILEGL_IPC_POLL_ESCALATE-th unanswered poll crosses
This commit is contained in:
@@ -603,6 +603,13 @@ namespace MobileGL::MG_Config {
|
||||
// red-once of the wait-count gate (PackBufferReadbackScenario): it must show a reply
|
||||
// wait per read.
|
||||
Uint32 PboReadbackSync = 0;
|
||||
// MOBILEGL_IPC_POLL_ESCALATE (P10, MG_Remote/CONTRACT-P10.md §1): a glClientWaitSync with a
|
||||
// zero timeout or a glGetSynciv(GL_SYNC_STATUS) is answered by the client from what the
|
||||
// server reported (kEventFenceSignaled) - "signaled" when it has been, "not yet" otherwise
|
||||
// - and only the Nth "not yet" in a row on one fence crosses as a real round trip, so a
|
||||
// poll loop cannot outlive a late report. 0 = every poll crosses, as before P10: the A/B
|
||||
// arm and the red-once of the no-reply gate (FencePollScenario).
|
||||
Uint32 PollEscalate = 64;
|
||||
// MOBILEGL_IPC_ADOPT_TIER: 2 = emulate (client keeps the shadow and pushes), which
|
||||
// is the only tier P5 implements and the reason persistent-map-push can be non-zero
|
||||
// at all (R-6). 0 and 1 parse and are Fatal at use with "P11"; they exist now so the
|
||||
|
||||
@@ -423,6 +423,8 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 10, 1, 12);
|
||||
// P9 W1: the pack-buffer readback's A/B and red-once control (Config.h says both).
|
||||
ipc.PboReadbackSync = QueryEnvUint32("MOBILEGL_IPC_PBO_READBACK_SYNC", 0, 0, 1);
|
||||
// P10: fence polls answered locally; 0 is the A/B arm (Config.h).
|
||||
ipc.PollEscalate = QueryEnvUint32("MOBILEGL_IPC_POLL_ESCALATE", 64, 0, 1000000);
|
||||
// The verify harness compares the pushed block against the applier per verb; a
|
||||
// batched queue lets the comparer read a supplied field mid-apply, which is a
|
||||
// torn read rather than a divergence. The batch is therefore off whenever the
|
||||
@@ -524,11 +526,11 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// changes the wall clock by seconds (the P12 sweep: 28.3 s at 1 down to 10.8 s at 6) a
|
||||
// log that does not name it makes every run ambiguous about its own configuration.
|
||||
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus event-wait=%ums "
|
||||
"persistent-block=%uKiB create-window=%u pbo-readback-sync=%u "
|
||||
"persistent-block=%uKiB create-window=%u pbo-readback-sync=%u poll-escalate=%u "
|
||||
"adopt-tier=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
|
||||
"cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s' surface=%s",
|
||||
ipc.RingMb, ipc.StageMb, ipc.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
|
||||
ipc.PersistentBlockKb, ipc.CreateWindow, ipc.PboReadbackSync, ipc.AdoptTier,
|
||||
ipc.PersistentBlockKb, ipc.CreateWindow, ipc.PboReadbackSync, ipc.PollEscalate, ipc.AdoptTier,
|
||||
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs,
|
||||
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
|
||||
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str(),
|
||||
|
||||
@@ -87,6 +87,8 @@ add_executable(MobileGLIntegrationTest
|
||||
# P9 (W1): reads into a pixel pack buffer answered by nothing. Ordinary GL cases too, plus
|
||||
# one wire case that skips where there is no wire.
|
||||
Scenarios/PackBufferReadbackScenario.cpp
|
||||
# P10 (A): fence polls answered from the server's reports.
|
||||
Scenarios/FencePollScenario.cpp
|
||||
Scenarios/PrimitiveRestartScenario.cpp
|
||||
Scenarios/FragCoordOriginScenario.cpp
|
||||
Scenarios/ClientVertexArrayScenario.cpp
|
||||
@@ -2458,6 +2460,11 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# reply-slot record, one mark per read). The Magma arms are the `Pbo.` block after the Band
|
||||
# block below.
|
||||
mgl_itest_register_split_arms("PackBufferReadbackScenario.*" "")
|
||||
# P10 (A, MG_Remote/CONTRACT-P10.md §1): fence polls answered from kEventFenceSignaled - the
|
||||
# idiom's poll loops end (with and without the flush bit), a signaled fence's polls cross
|
||||
# nothing, and an idle server still reports a fence that finishes. The Magma arms are the
|
||||
# `Fence.` block beside the `Pbo.` block below.
|
||||
mgl_itest_register_split_arms("FencePollScenario.*" "")
|
||||
|
||||
# DepthStencilReadbackMatrixScenario GETS ITS OWN BLOCK, because it needs a variable the
|
||||
# registration macro cannot carry: MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1.
|
||||
@@ -3644,6 +3651,19 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
${mglItestArmProperties}
|
||||
ENVIRONMENT "${mglItestMagmaBandEnvironment}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p9-pbo${mglItestArmLogTag}-${pboCase}.log")
|
||||
endforeach()
|
||||
# P10 (A): the fence polls on Magma. The server half is backend-neutral (the backend
|
||||
# table's FenceSync / GetSyncStatus / ClientWaitSync), so the same three cases.
|
||||
foreach(fenceCase ZeroTimeoutPollLoopsEndInBoundedTime
|
||||
PollsOfASignaledFenceCrossNothing
|
||||
AFenceThatFinishesWhileTheClientOnlyPollsIsReported)
|
||||
gtest_discover_tests(MobileGLIntegrationTest
|
||||
TEST_PREFIX "DirectVulkan.${mglItestArm}.Fence."
|
||||
TEST_FILTER "FencePollScenario.${fenceCase}"
|
||||
DISCOVERY_TIMEOUT 30
|
||||
PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||
${mglItestArmProperties}
|
||||
ENVIRONMENT "${mglItestMagmaBandEnvironment}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p10-fence${mglItestArmLogTag}-${fenceCase}.log")
|
||||
endforeach()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
|
||||
@@ -168,6 +168,11 @@ namespace MGITest {
|
||||
state.replyPostings = session->ReplyPostings();
|
||||
state.packBufferReadbackMarks = MobileGL::MG_Remote::Client::ProducerMarkCount(
|
||||
MobileGL::MG_Remote::Client::GpuWriteProducer::ReadPixelsPackBuffer);
|
||||
const auto fences = MobileGL::MG_Remote::Client::ReadFencePollCounters();
|
||||
state.fenceLocalAnswers = fences.LocalAnswers;
|
||||
state.fenceEscalations = fences.Escalations;
|
||||
state.fenceRoundTrips = fences.RoundTrips;
|
||||
state.fenceServerReports = fences.ServerReports;
|
||||
}
|
||||
#endif
|
||||
return state;
|
||||
|
||||
@@ -130,6 +130,15 @@ namespace MGITest {
|
||||
// apart from both sides.
|
||||
unsigned long long replyPostings = 0;
|
||||
unsigned long long packBufferReadbackMarks = 0;
|
||||
|
||||
// ---- P10 (CONTRACT-P10.md §1): what the fence polls cost ----------------------------
|
||||
// EmitTables.h FencePollCounters, verbatim: polls answered with no record, zero-timeout
|
||||
// polls escalated to a round trip, every fence answer that crossed, and the server's
|
||||
// kEventFenceSignaled reports consumed.
|
||||
unsigned long long fenceLocalAnswers = 0;
|
||||
unsigned long long fenceEscalations = 0;
|
||||
unsigned long long fenceRoundTrips = 0;
|
||||
unsigned long long fenceServerReports = 0;
|
||||
};
|
||||
|
||||
SplitRuntimeState PeekSplitRuntime();
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/FencePollScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// P10 (A, MG_Remote/CONTRACT-P10.md §1): FENCE POLLS ANSWERED FROM THE REVERSE CHANNEL.
|
||||
//
|
||||
// A glClientWaitSync / glGetSynciv(GL_SYNC_STATUS) used to be a kWaitReply record: its answer
|
||||
// came back only after the server had applied every record before it, so a poll was a full
|
||||
// pipeline drain and, over TCP, a round trip - once per frame in Minecraft's fence use. Now the
|
||||
// server reports each fence the GPU finishes (kEventFenceSignaled) and the client answers polls
|
||||
// from those reports; the Nth unanswered zero-timeout poll in a row (MOBILEGL_IPC_POLL_ESCALATE)
|
||||
// still crosses, which is what bounds a loop whose report is late.
|
||||
//
|
||||
// Three cases. The first is ordinary GL - the idiom's two poll loops end - and its flush-bit arm
|
||||
// runs on the monolith lanes too. The other two are about the wire and skip where there is none.
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
#include "../Harness/SplitRuntimePeek.h"
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
constexpr auto kLoopBound = std::chrono::seconds(10);
|
||||
|
||||
class FencePollScenario : public ScenarioTest {
|
||||
protected:
|
||||
// Some GPU work for a fence to wait behind.
|
||||
static void QueueWork() {
|
||||
glClearColor(0.25f, 0.5f, 0.75f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
};
|
||||
|
||||
// The GL idiom: `while (glClientWaitSync(s, flags, 0) == GL_TIMEOUT_EXPIRED) {}` and the
|
||||
// glGetSynciv twin. With nothing else in the loop, no record would ever be published if
|
||||
// the poll were not a doorbell point, and a local "not yet" would be repeated for ever if
|
||||
// nothing made it true - so the loop ending, bounded, is the property.
|
||||
//
|
||||
// WITHOUT GL_SYNC_FLUSH_COMMANDS_BIT GL itself does not promise the loop ends (the fence
|
||||
// may sit unflushed in the driver), so that arm is split-only: there the transport
|
||||
// promises it, because the poll flushes the link and an idle server submits.
|
||||
TEST_F(FencePollScenario, ZeroTimeoutPollLoopsEndInBoundedTime) {
|
||||
if (!Ready()) return;
|
||||
const bool split = SplitRuntimeSkipReason().empty();
|
||||
for (const GLbitfield flags : {GLbitfield(GL_SYNC_FLUSH_COMMANDS_BIT), GLbitfield(0)}) {
|
||||
if (flags == 0 && !split) continue;
|
||||
QueueWork();
|
||||
GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
ASSERT_NE(sync, nullptr);
|
||||
const auto start = Clock::now();
|
||||
GLenum status = GL_TIMEOUT_EXPIRED;
|
||||
unsigned long long polls = 0;
|
||||
while ((status = glClientWaitSync(sync, flags, 0)) == GL_TIMEOUT_EXPIRED &&
|
||||
Clock::now() - start < kLoopBound) {
|
||||
++polls;
|
||||
}
|
||||
EXPECT_TRUE(status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED)
|
||||
<< "glClientWaitSync(flags 0x" << std::hex << flags << std::dec << ", 0) still "
|
||||
<< status << " after " << polls << " polls";
|
||||
glDeleteSync(sync);
|
||||
}
|
||||
QueueWork();
|
||||
GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
ASSERT_NE(sync, nullptr);
|
||||
const auto start = Clock::now();
|
||||
GLint value = GL_UNSIGNALED;
|
||||
while (value != GL_SIGNALED && Clock::now() - start < kLoopBound) {
|
||||
glGetSynciv(sync, GL_SYNC_STATUS, 1, nullptr, &value);
|
||||
}
|
||||
EXPECT_EQ(value, GL_SIGNALED) << "glGetSynciv(GL_SYNC_STATUS) never read signaled";
|
||||
glDeleteSync(sync);
|
||||
EXPECT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
// THE WIRE HALF: once a fence is known signaled, polling it costs nothing - no reply-slot
|
||||
// record, no round trip - however often it is asked. MOBILEGL_IPC_POLL_ESCALATE=0 puts the
|
||||
// round trip back and must fail both counters (its red-once).
|
||||
TEST_F(FencePollScenario, PollsOfASignaledFenceCrossNothing) {
|
||||
if (!Ready()) return;
|
||||
const std::string why = SplitRuntimeSkipReason();
|
||||
if (!why.empty()) GTEST_SKIP() << why;
|
||||
QueueWork();
|
||||
GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
ASSERT_NE(sync, nullptr);
|
||||
const GLenum waited = glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 5000000000ull);
|
||||
ASSERT_TRUE(waited == GL_ALREADY_SIGNALED || waited == GL_CONDITION_SATISFIED) << waited;
|
||||
|
||||
constexpr unsigned kPolls = 100;
|
||||
const SplitRuntimeState before = PeekSplitRuntime();
|
||||
for (unsigned i = 0; i < kPolls; ++i) {
|
||||
EXPECT_EQ(glClientWaitSync(sync, 0, 0), static_cast<GLenum>(GL_ALREADY_SIGNALED));
|
||||
GLint value = GL_UNSIGNALED;
|
||||
glGetSynciv(sync, GL_SYNC_STATUS, 1, nullptr, &value);
|
||||
EXPECT_EQ(value, GL_SIGNALED);
|
||||
}
|
||||
const SplitRuntimeState after = PeekSplitRuntime();
|
||||
glDeleteSync(sync);
|
||||
EXPECT_EQ(after.replyPostings - before.replyPostings, 0u)
|
||||
<< "polling a signaled fence published reply-slot records (MOBILEGL_IPC_POLL_ESCALATE=0 "
|
||||
"does this on purpose)";
|
||||
EXPECT_EQ(after.fenceRoundTrips - before.fenceRoundTrips, 0u);
|
||||
EXPECT_EQ(after.fenceLocalAnswers - before.fenceLocalAnswers, 2ull * kPolls);
|
||||
}
|
||||
|
||||
// THE SERVER'S HALF: a fence the GPU finishes while the client sends nothing - it only
|
||||
// polls - is still reported. The apply thread is idle then; it must submit what the driver
|
||||
// holds and keep re-checking rather than park for ever, or the only way out of the loop is
|
||||
// the escalation. The polls are paced (10 ms apart) so the escalation cannot come first by
|
||||
// rate: 64 of them is 640 ms, far more than a clear takes. Red-once: take the idle
|
||||
// re-check out of ServerLoop and this case escalates.
|
||||
TEST_F(FencePollScenario, AFenceThatFinishesWhileTheClientOnlyPollsIsReported) {
|
||||
if (!Ready()) return;
|
||||
const std::string why = SplitRuntimeSkipReason();
|
||||
if (!why.empty()) GTEST_SKIP() << why;
|
||||
QueueWork();
|
||||
GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
ASSERT_NE(sync, nullptr);
|
||||
const SplitRuntimeState before = PeekSplitRuntime();
|
||||
const auto start = Clock::now();
|
||||
GLenum status = GL_TIMEOUT_EXPIRED;
|
||||
while ((status = glClientWaitSync(sync, 0, 0)) == GL_TIMEOUT_EXPIRED &&
|
||||
Clock::now() - start < kLoopBound) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
const SplitRuntimeState after = PeekSplitRuntime();
|
||||
glDeleteSync(sync);
|
||||
EXPECT_TRUE(status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED) << status;
|
||||
EXPECT_EQ(after.fenceEscalations - before.fenceEscalations, 0u)
|
||||
<< "the loop needed an escalated round trip: the server did not report the fence on its "
|
||||
"own while the client only polled";
|
||||
EXPECT_GE(after.fenceServerReports - before.fenceServerReports, 1u);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
@@ -30,7 +30,9 @@ TEST_F(SyncWireScenario, FenceWaitStatusAndDeletionCrossAndPreserveRenderedPixel
|
||||
glWaitSync(sync, 0, GL_TIMEOUT_IGNORED);
|
||||
glDeleteSync(sync);
|
||||
EXPECT_FALSE(glIsSync(sync));
|
||||
EXPECT_GE(PeekSplitRuntime().emitSeq, before + 5);
|
||||
// Create, the server-side wait and the delete always cross; since P10 the two polls may be
|
||||
// answered from the server's fence report instead (FencePollScenario), so they are not counted.
|
||||
EXPECT_GE(PeekSplitRuntime().emitSeq, before + 3);
|
||||
GLubyte pixel[4]{};
|
||||
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
||||
const int expected[4] = {64, 128, 191, 255};
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "../Transport/FdPassing.h"
|
||||
#include "../Transport/InProcessTransport.h"
|
||||
#include "WireTables.h"
|
||||
#include "EmitTables.h"
|
||||
|
||||
#include <MGGitHash.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
@@ -482,9 +483,22 @@ namespace MobileGL::MG_Remote::Client {
|
||||
++delivered;
|
||||
break;
|
||||
}
|
||||
case Transport::kEventFenceSignaled: {
|
||||
// P10 (CONTRACT-P10.md §1): remembered for every later poll of that fence.
|
||||
if (view.payloadSize < sizeof(Transport::EventFenceSignaledHead)) {
|
||||
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"kEventFenceSignaled\"} - "
|
||||
"a %llu-byte record is shorter than the fence handle it carries",
|
||||
static_cast<unsigned long long>(view.payloadSize));
|
||||
}
|
||||
const auto* head =
|
||||
static_cast<const Transport::EventFenceSignaledHead*>(view.payload);
|
||||
NoteFenceSignaledByServer(MG_Pipe::MGPipeHandle{head->Fence.Slot, head->Fence.Gen});
|
||||
++delivered;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
MGLOG_W("MG_Remote client: reverse-channel record kind %u is not consumed in "
|
||||
"P5c (four of the ten callbacks are armed; the rest are P9's)",
|
||||
MGLOG_W("MG_Remote client: reverse-channel record kind %u is not one this "
|
||||
"build consumes",
|
||||
static_cast<unsigned>(view.kind));
|
||||
break;
|
||||
}
|
||||
@@ -2235,6 +2249,12 @@ namespace MobileGL::MG_Remote::Client {
|
||||
}
|
||||
}
|
||||
|
||||
void ClientSession::PollEntry() {
|
||||
if (!m_started || m_events == nullptr || DeviceLost()) return;
|
||||
Flush();
|
||||
DrainEventRing(*m_events);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -281,6 +281,12 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// 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();
|
||||
// P10 (CONTRACT-P10.md §1): a polling entry point (glClientWaitSync, glGetSynciv(
|
||||
// GL_SYNC_STATUS)) is a doorbell point. Everything published so far - the polled fence's
|
||||
// own create record above all - is flushed to the server, and the reverse channel is
|
||||
// drained, so a fence the server has since reported is known before the poll answers.
|
||||
// Publishes nothing and waits for nothing.
|
||||
void PollEntry();
|
||||
// Submit published commands without waiting for application. This is
|
||||
// glFlush's contract and the frame-submit boundary for Present.
|
||||
void Flush();
|
||||
|
||||
@@ -1592,15 +1592,87 @@ namespace MobileGL::MG_Remote::Client {
|
||||
// =============================================================================
|
||||
// The frontend sees a local opaque token; neither this address nor the driver's
|
||||
// BackendSyncHandle is serialized. The Fence-kind slot allocator supplies wire identity.
|
||||
struct RemoteFenceProxy { MG_Pipe::MGPipeHandle Handle; };
|
||||
struct RemoteFenceProxy {
|
||||
MG_Pipe::MGPipeHandle Handle;
|
||||
// P10: zero-timeout polls answered "not yet" in a row since the last escalation.
|
||||
Uint32 Unanswered = 0;
|
||||
};
|
||||
std::mutex g_fenceMutex;
|
||||
UnorderedMap<MG_Backend::BackendSyncHandle, UniquePtr<RemoteFenceProxy>> g_fenceProxies;
|
||||
|
||||
MG_Pipe::MGPipeHandle FenceHandle(MG_Backend::BackendSyncHandle proxy) {
|
||||
RemoteFenceProxy& FenceProxy(MG_Backend::BackendSyncHandle proxy) {
|
||||
const auto it = g_fenceProxies.find(proxy);
|
||||
if (it == g_fenceProxies.end())
|
||||
Wire::WireProtocolFatal("Fence.proxy", "unknown client fence proxy");
|
||||
return it->second->Handle;
|
||||
return *it->second;
|
||||
}
|
||||
|
||||
MG_Pipe::MGPipeHandle FenceHandle(MG_Backend::BackendSyncHandle proxy) { return FenceProxy(proxy).Handle; }
|
||||
|
||||
// ---- P10 (CONTRACT-P10.md §1): WHAT THE SERVER HAS SAID IS SIGNALED -----------------
|
||||
//
|
||||
// {slot -> gen} of every fence a kEventFenceSignaled named, or a round trip answered
|
||||
// signaled. ITS OWN MUTEX, NOT g_fenceMutex: the report arrives in the event drain, and
|
||||
// the drain runs INSIDE the emitters below while they hold g_fenceMutex (EmitAndWait and
|
||||
// PollEntry both drain), so taking g_fenceMutex there would deadlock the GL thread on
|
||||
// itself. A report for a fence already deleted leaves a {slot, old gen} entry that no live
|
||||
// handle matches; the slot's next occupant overwrites it.
|
||||
std::mutex g_signaledFenceMutex;
|
||||
UnorderedMap<Uint32, Uint32> g_signaledFences;
|
||||
std::atomic<Uint64> g_fenceLocalAnswers{0};
|
||||
std::atomic<Uint64> g_fenceEscalations{0};
|
||||
std::atomic<Uint64> g_fenceRoundTrips{0};
|
||||
std::atomic<Uint64> g_fenceServerReports{0};
|
||||
|
||||
Bool FenceKnownSignaled(MG_Pipe::MGPipeHandle handle) {
|
||||
const std::lock_guard<std::mutex> lock(g_signaledFenceMutex);
|
||||
const auto it = g_signaledFences.find(handle.Slot);
|
||||
return it != g_signaledFences.end() && it->second == handle.Gen;
|
||||
}
|
||||
|
||||
void RememberFenceSignaled(MG_Pipe::MGPipeHandle handle) {
|
||||
const std::lock_guard<std::mutex> lock(g_signaledFenceMutex);
|
||||
g_signaledFences[handle.Slot] = handle.Gen;
|
||||
}
|
||||
|
||||
void ForgetFence(MG_Pipe::MGPipeHandle handle) {
|
||||
const std::lock_guard<std::mutex> lock(g_signaledFenceMutex);
|
||||
const auto it = g_signaledFences.find(handle.Slot);
|
||||
if (it != g_signaledFences.end() && it->second == handle.Gen) g_signaledFences.erase(it);
|
||||
}
|
||||
|
||||
// MOBILEGL_IPC_POLL_ESCALATE=0 is the A/B control: every poll crosses, as before P10. A lost
|
||||
// device takes the round-trip arm too, whose DECLINED answer is the no-op a dead fence gets.
|
||||
Bool FencePollsAnswerLocally() {
|
||||
return MG_Config::Ipc.PollEscalate != 0 && !ClientSession::DeviceLost();
|
||||
}
|
||||
|
||||
// THE LOCAL ANSWER, OR "CROSS". A poll is a doorbell point first (PollEntry: flush what is
|
||||
// published, drain the reverse channel), so the fence's own create record has reached the
|
||||
// server and every report it has posted is in. Known signaled -> answered here. Not known
|
||||
// and the call may not wait (a zero timeout, glGetSynciv) -> answered "not yet" here,
|
||||
// unless this fence has had POLL_ESCALATE such answers in a row, in which case the poll
|
||||
// takes one real round trip: the server's own reports are what make a local "not yet"
|
||||
// eventually true, and the escalation is the bound that holds even if one is late. A call
|
||||
// that may wait (a non-zero timeout) is never answered "not yet" here - it crosses and
|
||||
// waits on the server, as the GL call asks.
|
||||
enum class FencePollAnswer { Signaled, NotYet, Cross };
|
||||
FencePollAnswer AnswerFencePollLocally(ClientSession& session, RemoteFenceProxy& fence, Bool mayWait) {
|
||||
if (!FencePollsAnswerLocally()) return FencePollAnswer::Cross;
|
||||
session.PollEntry();
|
||||
if (FenceKnownSignaled(fence.Handle)) {
|
||||
fence.Unanswered = 0;
|
||||
g_fenceLocalAnswers.fetch_add(1, std::memory_order_relaxed);
|
||||
return FencePollAnswer::Signaled;
|
||||
}
|
||||
if (mayWait) return FencePollAnswer::Cross;
|
||||
if (++fence.Unanswered < MG_Config::Ipc.PollEscalate) {
|
||||
g_fenceLocalAnswers.fetch_add(1, std::memory_order_relaxed);
|
||||
return FencePollAnswer::NotYet;
|
||||
}
|
||||
fence.Unanswered = 0;
|
||||
g_fenceEscalations.fetch_add(1, std::memory_order_relaxed);
|
||||
return FencePollAnswer::Cross;
|
||||
}
|
||||
|
||||
MG_Backend::BackendSyncHandle EmitFenceSync() {
|
||||
@@ -1669,16 +1741,33 @@ namespace MobileGL::MG_Remote::Client {
|
||||
GLenum EmitClientWaitSync(MG_Backend::BackendSyncHandle proxy, GLbitfield flags, GLuint64 timeout) {
|
||||
ClientSession& session = RequireSession("ClientWaitSync");
|
||||
const std::lock_guard<std::mutex> lock(g_fenceMutex);
|
||||
const MG_Pipe::MGPFenceWait request{FenceHandle(proxy), timeout, flags, 0};
|
||||
return ReadFenceReply(session, MG_Pipe::MGPWireOp::FenceWait, &request, sizeof(request));
|
||||
RemoteFenceProxy& fence = FenceProxy(proxy);
|
||||
switch (AnswerFencePollLocally(session, fence, /*mayWait=*/timeout != 0)) {
|
||||
case FencePollAnswer::Signaled: return GL_ALREADY_SIGNALED;
|
||||
case FencePollAnswer::NotYet: return GL_TIMEOUT_EXPIRED;
|
||||
case FencePollAnswer::Cross: break;
|
||||
}
|
||||
g_fenceRoundTrips.fetch_add(1, std::memory_order_relaxed);
|
||||
const MG_Pipe::MGPFenceWait request{fence.Handle, timeout, flags, 0};
|
||||
const Uint32 result = ReadFenceReply(session, MG_Pipe::MGPWireOp::FenceWait, &request, sizeof(request));
|
||||
if (result == GL_ALREADY_SIGNALED || result == GL_CONDITION_SATISFIED) RememberFenceSignaled(fence.Handle);
|
||||
return result;
|
||||
}
|
||||
|
||||
Bool EmitGetSyncStatus(MG_Backend::BackendSyncHandle proxy) {
|
||||
ClientSession& session = RequireSession("GetSyncStatus");
|
||||
const std::lock_guard<std::mutex> lock(g_fenceMutex);
|
||||
const MG_Pipe::MGPHandleOnly desc{FenceHandle(proxy), static_cast<Uint32>(MG_Pipe::MGPipeKind::Fence), 0};
|
||||
RemoteFenceProxy& fence = FenceProxy(proxy);
|
||||
switch (AnswerFencePollLocally(session, fence, /*mayWait=*/false)) {
|
||||
case FencePollAnswer::Signaled: return true;
|
||||
case FencePollAnswer::NotYet: return false;
|
||||
case FencePollAnswer::Cross: break;
|
||||
}
|
||||
g_fenceRoundTrips.fetch_add(1, std::memory_order_relaxed);
|
||||
const MG_Pipe::MGPHandleOnly desc{fence.Handle, static_cast<Uint32>(MG_Pipe::MGPipeKind::Fence), 0};
|
||||
const Uint32 result = ReadFenceReply(session, MG_Pipe::MGPWireOp::FenceStatus, &desc, sizeof(desc));
|
||||
if (result > 1) Wire::WireProtocolFatal("FenceStatus.reply", "status must be boolean");
|
||||
if (result != 0) RememberFenceSignaled(fence.Handle);
|
||||
return result != 0;
|
||||
}
|
||||
|
||||
@@ -1701,6 +1790,7 @@ namespace MobileGL::MG_Remote::Client {
|
||||
nullptr, 0, nullptr, 0, nullptr);
|
||||
}
|
||||
MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Fence, handle);
|
||||
ForgetFence(handle);
|
||||
g_fenceProxies.erase(proxy);
|
||||
}
|
||||
|
||||
@@ -1946,6 +2036,20 @@ namespace MobileGL::MG_Remote::Client {
|
||||
|
||||
} // namespace
|
||||
|
||||
void NoteFenceSignaledByServer(MG_Pipe::MGPipeHandle fence) {
|
||||
RememberFenceSignaled(fence);
|
||||
g_fenceServerReports.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
FencePollCounters ReadFencePollCounters() {
|
||||
FencePollCounters counters;
|
||||
counters.LocalAnswers = g_fenceLocalAnswers.load(std::memory_order_relaxed);
|
||||
counters.Escalations = g_fenceEscalations.load(std::memory_order_relaxed);
|
||||
counters.RoundTrips = g_fenceRoundTrips.load(std::memory_order_relaxed);
|
||||
counters.ServerReports = g_fenceServerReports.load(std::memory_order_relaxed);
|
||||
return counters;
|
||||
}
|
||||
|
||||
const MG_Backend::GlobalBackendFunctionsTable& RemoteEmitTable() {
|
||||
// Leaked at exit like every other MG_Remote singleton (ID-8): MG_Backend::Init()
|
||||
// copies it into gBackendFunctionsTable and MobileGL::Destroy() clears that copy from
|
||||
|
||||
@@ -284,4 +284,23 @@ namespace MobileGL::MG_Remote::Client {
|
||||
GLsizei drawCount, GLsizei stride,
|
||||
GLintptr parameterOffset, Bool hasParameterBuffer);
|
||||
|
||||
// ---- P10 (CONTRACT-P10.md §1): fence polls answered from the reverse channel ----------
|
||||
//
|
||||
// The event drain's half: the server reported `fence` signaled (kEventFenceSignaled). Every
|
||||
// later glClientWaitSync / glGetSynciv(GL_SYNC_STATUS) on it answers without a round trip.
|
||||
void NoteFenceSignaledByServer(MG_Pipe::MGPipeHandle fence);
|
||||
|
||||
// What the fence polls cost, for the lanes. `LocalAnswers` = polls answered with no record
|
||||
// (signaled or not); `Escalations` = zero-timeout polls that had gone unanswered
|
||||
// MOBILEGL_IPC_POLL_ESCALATE times in a row and took a round trip; `RoundTrips` = every
|
||||
// fence answer that crossed (escalations, non-zero timeouts, POLL_ESCALATE=0);
|
||||
// `ServerReports` = kEventFenceSignaled records consumed.
|
||||
struct FencePollCounters {
|
||||
Uint64 LocalAnswers = 0;
|
||||
Uint64 Escalations = 0;
|
||||
Uint64 RoundTrips = 0;
|
||||
Uint64 ServerReports = 0;
|
||||
};
|
||||
FencePollCounters ReadFencePollCounters();
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Client
|
||||
|
||||
@@ -155,6 +155,8 @@ namespace MobileGL::MG_Remote::Server {
|
||||
entry.Live = true;
|
||||
// GL_Sync.cpp treats an absent slot or a null creation result as always signaled.
|
||||
entry.Native = table->GL.FenceSync == nullptr ? nullptr : table->GL.FenceSync();
|
||||
// P10: in creation order, for ReportSignaledFences - the order the GPU finishes them in.
|
||||
m_unreportedFences.push_back(handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -216,6 +218,58 @@ namespace MobileGL::MG_Remote::Server {
|
||||
}
|
||||
}
|
||||
m_fences.clear();
|
||||
m_unreportedFences.clear();
|
||||
}
|
||||
|
||||
// P10 (CONTRACT-P10.md §1): WHICH OF THE CLIENT'S FENCES HAS THE GPU FINISHED.
|
||||
//
|
||||
// The client answers a poll of a fence it knows is signaled without asking - that is the
|
||||
// whole of package A - and it learns what is signaled only from here. Walked in CREATION
|
||||
// order and stopped at the first one still pending: one context's GPU work completes in
|
||||
// the order it was submitted, so a later fence cannot be done while an earlier one is not,
|
||||
// and the steady-state cost is one status query per drain batch. (Were a backend ever to
|
||||
// finish them out of order, the later fence would only be reported late - an answer the
|
||||
// client then gets by its own round trip - never early.)
|
||||
//
|
||||
// `flush`: the apply thread is about to go idle. A fence whose commands the driver still
|
||||
// holds never signals on its own, and an idle server submits nothing more, so the first
|
||||
// pending fence is asked through ClientWaitSync(GL_SYNC_FLUSH_COMMANDS_BIT, 0) - the one
|
||||
// backend-neutral way both backends take to submit what they hold - instead of the plain
|
||||
// status query. Only when idle: mid-stream the next records flush soon enough, and a
|
||||
// submit per batch would cost Magma its command-buffer batching.
|
||||
Uint32 ServerVerbSink::ReportSignaledFences(Bool flush) {
|
||||
if (m_unreportedFences.empty()) return 0;
|
||||
ServerSession* session = ServerSession::Active();
|
||||
if (session == nullptr || m_backend == nullptr) return 0;
|
||||
const MG_Backend::GLFunctionsTable& gl = m_backend->GetBackendFunctions().GL;
|
||||
Uint32 reported = 0;
|
||||
while (!m_unreportedFences.empty()) {
|
||||
const MG_Pipe::MGPipeHandle handle = m_unreportedFences.front();
|
||||
const auto it = m_fences.find(handle.Slot);
|
||||
if (it == m_fences.end() || !it->second.Live || it->second.Gen != handle.Gen) {
|
||||
// Deleted before it was seen signaled: nobody can poll it any more.
|
||||
m_unreportedFences.pop_front();
|
||||
continue;
|
||||
}
|
||||
const FenceEntry& entry = it->second;
|
||||
Bool signaled = entry.Native == nullptr;
|
||||
if (!signaled) {
|
||||
if (flush && gl.ClientWaitSync != nullptr) {
|
||||
const GLenum status = gl.ClientWaitSync(entry.Native, GL_SYNC_FLUSH_COMMANDS_BIT, 0);
|
||||
signaled = status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED;
|
||||
flush = false; // one submit is all an idle server owes
|
||||
++m_fenceIdleFlushes;
|
||||
} else {
|
||||
signaled = gl.GetSyncStatus == nullptr || gl.GetSyncStatus(entry.Native);
|
||||
}
|
||||
}
|
||||
if (!signaled) break;
|
||||
m_unreportedFences.pop_front();
|
||||
session->PostFenceSignaled(handle);
|
||||
++reported;
|
||||
}
|
||||
m_fencesReported += reported;
|
||||
return reported;
|
||||
}
|
||||
|
||||
#include "QueryServer.inc"
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
#include "../Transport/Ring.h"
|
||||
#include "../Wire/PipeWireCodec.h"
|
||||
|
||||
#include <deque>
|
||||
|
||||
namespace MobileGL::MG_Remote::Server {
|
||||
|
||||
// P5e (gl, ID-111): DOES THIS SERVER PUBLISH kCapRunAheadApply? Asked of the server's OWN
|
||||
@@ -157,6 +159,15 @@ namespace MobileGL::MG_Remote::Server {
|
||||
Bool OnFenceWait(const MG_Pipe::MGPFenceWait&, Uint32&) override;
|
||||
Bool OnFenceWaitServer(const MG_Pipe::MGPFenceWait&) override;
|
||||
void ReleaseFences();
|
||||
// P10 (CONTRACT-P10.md §1): post kEventFenceSignaled for each client fence, in creation
|
||||
// order, that the GPU has finished, up to the first one still pending. `flush` = the
|
||||
// apply thread is going idle: submit what the driver holds first. Apply thread only.
|
||||
Uint32 ReportSignaledFences(Bool flush);
|
||||
// True while a created fence has not been reported: the loop then parks with a deadline
|
||||
// instead of for ever, so a fence that finishes while no record arrives is still seen.
|
||||
Bool HasUnreportedFences() const { return !m_unreportedFences.empty(); }
|
||||
Uint64 FencesReported() const { return m_fencesReported; }
|
||||
Uint64 FenceIdleFlushes() const { return m_fenceIdleFlushes; }
|
||||
Bool OnQueryCreate(const MG_Pipe::MGPQueryDesc&) override;
|
||||
Bool OnQueryBegin(const MG_Pipe::MGPQueryDesc&) override;
|
||||
Bool OnQueryEnd(const MG_Pipe::MGPQueryDesc&) override;
|
||||
@@ -321,6 +332,10 @@ namespace MobileGL::MG_Remote::Server {
|
||||
};
|
||||
FenceEntry& FindFence(MG_Pipe::MGPipeHandle handle);
|
||||
UnorderedMap<Uint32, FenceEntry> m_fences;
|
||||
// P10: created and not yet reported signaled, oldest first.
|
||||
std::deque<MG_Pipe::MGPipeHandle> m_unreportedFences;
|
||||
Uint64 m_fencesReported = 0;
|
||||
Uint64 m_fenceIdleFlushes = 0;
|
||||
struct QueryEntry {
|
||||
Uint32 Gen = 0;
|
||||
Uint32 Kind = 0;
|
||||
|
||||
@@ -476,11 +476,24 @@ namespace MobileGL::MG_Remote::Server {
|
||||
return signals.CmdHead->load(std::memory_order_acquire) != ring.LocalTail();
|
||||
};
|
||||
|
||||
// P10 (CONTRACT-P10.md §1): the client answers a fence poll from what this thread reports,
|
||||
// so a fence the GPU finishes must be reported whether or not another record ever arrives.
|
||||
// After every drain batch the pending fences are asked (a status query, no submit); a
|
||||
// thread about to go idle submits first and, while any fence is still pending, parks for
|
||||
// at most kFenceIdlePollMs rather than for ever. Never on a full SEG_EVENT: posting there
|
||||
// would park this thread inside the post, and the ring test below already says "not now".
|
||||
ServerVerbSink& verbs = session.Applier().Verbs();
|
||||
constexpr Uint32 kFenceIdlePollMs = 1;
|
||||
const auto eventRingHasRoom = [signals] {
|
||||
return signals.EventRingFull->load(std::memory_order_acquire) == 0;
|
||||
};
|
||||
|
||||
for (;;) {
|
||||
PumpControlRequest();
|
||||
if (stopOrForfeit()) break;
|
||||
if (signals.EventRingFull->load(std::memory_order_acquire) == 0) DrainRing();
|
||||
if (eventRingHasRoom()) DrainRing();
|
||||
if (stopOrForfeit()) break;
|
||||
if (eventRingHasRoom() && !SessionLatched()) verbs.ReportSignaledFences(/*flush=*/false);
|
||||
// PH-1 (3): a latched session applies nothing more, so this thread leaves the loop
|
||||
// (and does not spin on a ring whose head is still ahead of a tail DrainRing will no
|
||||
// longer move). The exit path below tears the backend down as for a Stop; the
|
||||
@@ -493,6 +506,12 @@ namespace MobileGL::MG_Remote::Server {
|
||||
break;
|
||||
}
|
||||
if (ready()) continue;
|
||||
// P10: going idle with a client fence still pending - submit what the driver holds and
|
||||
// ask again, once, before the park.
|
||||
if (verbs.HasUnreportedFences() && eventRingHasRoom()) {
|
||||
verbs.ReportSignaledFences(/*flush=*/true);
|
||||
if (ready()) continue;
|
||||
}
|
||||
|
||||
const Uint64 waits = m_parks.fetch_add(1, std::memory_order_acq_rel) + 1;
|
||||
// THE WAIT LEDGER'S SERVER HALF (P5d round 3, package T item 4). The client may not
|
||||
@@ -519,18 +538,21 @@ namespace MobileGL::MG_Remote::Server {
|
||||
}
|
||||
// Flush-on-idle publishes the last reply record even below the batch threshold.
|
||||
session.FlushDataProgress();
|
||||
const Bool fencePending = verbs.HasUnreportedFences();
|
||||
const bool woke = bell.Wait(*signals.ConsumerParked, ready, spinUs,
|
||||
Transport::kWaitForever, &m_parkBlocks);
|
||||
fencePending ? kFenceIdlePollMs : Transport::kWaitForever,
|
||||
&m_parkBlocks);
|
||||
if (!woke) {
|
||||
// Wait returns false only on a dead bell or an expired deadline, and this park
|
||||
// has no deadline. A dead bell IS the shutdown signal (table 3's teardown step
|
||||
// 2); treating it as anything else would spin at full clock for ever, since
|
||||
// parking on a dead bell no longer blocks.
|
||||
// Wait returns false only on a dead bell or an expired deadline. A dead bell IS the
|
||||
// shutdown signal (table 3's teardown step 2); treating it as anything else would
|
||||
// spin at full clock for ever, since parking on a dead bell no longer blocks.
|
||||
if (bell.Dead()) {
|
||||
MGLOG_D("MG_Remote server: the consumer doorbell is dead; mgl-srv-apply is "
|
||||
"shutting down");
|
||||
break;
|
||||
}
|
||||
// P10: the deadline is the fence re-check, and the only park that has one.
|
||||
if (fencePending) continue;
|
||||
MGLOG_E("MG_Remote server: Doorbell::Wait(kWaitForever) returned false on a live "
|
||||
"bell - that is impossible by Doorbell::Wait's park loop (the only "
|
||||
"false returns are the Dead() arm and an expired deadline) and means "
|
||||
|
||||
@@ -1128,6 +1128,22 @@ namespace MobileGL::MG_Remote::Server {
|
||||
PublishEvents();
|
||||
}
|
||||
|
||||
void ServerSession::PostFenceSignaled(MG_Pipe::MGPipeHandle fence) {
|
||||
if (!m_accepted) {
|
||||
MGLOG_E_ONCE("MG_Remote server: PostFenceSignaled {%u, %u} before Accept - dropped, "
|
||||
"there is no SEG_EVENT to carry it on",
|
||||
fence.Slot, fence.Gen);
|
||||
return;
|
||||
}
|
||||
void* slot = ReserveEventOrBlock(*this, Transport::kEventFenceSignaled, "kEventFenceSignaled",
|
||||
sizeof(Transport::EventFenceSignaledHead));
|
||||
Transport::EventFenceSignaledHead head{};
|
||||
head.Fence.Slot = fence.Slot;
|
||||
head.Fence.Gen = fence.Gen;
|
||||
std::memcpy(slot, &head, sizeof(head));
|
||||
PublishEvents();
|
||||
}
|
||||
|
||||
// PH-6 (ID-P7-2). THE FIRST DROP, AND THE LATCH IT RAISES.
|
||||
//
|
||||
// Logged ONCE, by name, with everything a triage needs: which event, how big, what the
|
||||
|
||||
@@ -210,6 +210,10 @@ namespace MobileGL::MG_Remote::Server {
|
||||
// ruling (§1), not a check. A session that has not accepted drops the error with a
|
||||
// loud line, exactly as RecordError's own no-live-context arm does.
|
||||
void PostGlError(Uint32 code, const char* message);
|
||||
// P10 (CONTRACT-P10.md §1): one kEventFenceSignaled for a client fence the apply thread
|
||||
// found signaled. Posted and published at once - a client may be polling on it right now.
|
||||
// Dropped with the same loud line as PostGlError before Accept.
|
||||
void PostFenceSignaled(MG_Pipe::MGPipeHandle fence);
|
||||
Transport::ITransport* Control_Plane();
|
||||
|
||||
// ---- PH-6 (ID-P7-2): THE REVERSE CHANNEL IS FORFEITED, NOT FATAL -------------------
|
||||
|
||||
@@ -50,6 +50,7 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
kEventGpuWritten = 2, // MGPipeCallbacks::OnGpuWritten
|
||||
kEventSurfaceChanged = 3, // MGPipeCallbacks::OnSurfaceChanged
|
||||
kEventGlError = 4, // PipeInputs::RecordError, posted by the server session
|
||||
kEventFenceSignaled = 5, // P10: a client fence the server saw signaled (CONTRACT-P10 §1)
|
||||
};
|
||||
|
||||
// The 8-byte {slot, gen} pair, mirrored (MGPipeHandles.h:54-65).
|
||||
@@ -110,6 +111,14 @@ namespace MobileGL::MG_Remote::Transport {
|
||||
std::uint32_t MessageBytes; // strlen + 1, the NUL included
|
||||
};
|
||||
static_assert(sizeof(EventGlErrorHead) == 8, "wire shape");
|
||||
// P10 (CONTRACT-P10.md §1): the fence the server's apply thread found signaled, once per
|
||||
// fence. The client keeps the answer and gives it to every later glClientWaitSync /
|
||||
// glGetSynciv(GL_SYNC_STATUS) on that fence without a round trip.
|
||||
struct EventFenceSignaledHead {
|
||||
EventHandle Fence;
|
||||
};
|
||||
static_assert(sizeof(EventFenceSignaledHead) == 8, "wire shape");
|
||||
|
||||
inline constexpr std::uint32_t kEventGlErrorMaxMessageBytes = 1024;
|
||||
static_assert(kEventGlErrorMaxMessageBytes == 1024,
|
||||
"CONTRACT-P5C §1 caps an inline GL-error message at 1024 bytes, NUL included");
|
||||
|
||||
@@ -789,6 +789,9 @@ struct DecliningSyncPeer : Codec::WireVerbSink {
|
||||
|
||||
TEST(RemoteClientControls, DeclinedFenceReplyAnswersSignaledInsteadOfAborting) {
|
||||
const auto child = RunInChild([] {
|
||||
// The ROUND-TRIP arm is the subject: since P10 a zero-timeout poll of a fence the server
|
||||
// has not reported is answered "not yet" locally, and would never reach the reply.
|
||||
MG_Config::Ipc.PollEscalate = 0;
|
||||
StartControlSession();
|
||||
DecliningSyncPeer peer;
|
||||
peer.Install();
|
||||
@@ -831,6 +834,7 @@ TEST(RemoteClientControls, ErrorFenceReplyStillRefusesByName) {
|
||||
// THE TOLERANCE IS NOT BLANKET. An ERROR is a transport fault or a server-side refusal,
|
||||
// never "the server said no" (R-5/M4), and it keeps the named Fatal it always had.
|
||||
const auto child = RunInChild([] {
|
||||
MG_Config::Ipc.PollEscalate = 0; // the round-trip arm, as above
|
||||
StartControlSession();
|
||||
// The rows still decline (so the fence is never handed to a backend that cannot
|
||||
// create one) and EVERY answer that is posted says ERROR instead.
|
||||
@@ -845,6 +849,60 @@ TEST(RemoteClientControls, ErrorFenceReplyStillRefusesByName) {
|
||||
ExpectNamedAbort(child, "Fatal{ProtocolCorruption, \"Fence.reply\"}");
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// P10 A (CONTRACT-P10.md §1): the escalation bound, counted at the record
|
||||
// =====================================================================================
|
||||
//
|
||||
// A peer that never reports the fence and answers every FenceWait record TIMEOUT_EXPIRED: with
|
||||
// MOBILEGL_IPC_POLL_ESCALATE = 4 the first three zero-timeout polls are answered locally and
|
||||
// cross nothing, the fourth crosses, and the count starts again. A peer that DOES report (here:
|
||||
// the reply of a wait that saw it signaled) is answered locally from then on, with no record.
|
||||
struct CountingFencePeer : Codec::WireVerbSink {
|
||||
std::atomic<Uint32> waits{0};
|
||||
std::atomic<Bool> signaled{false};
|
||||
Bool OnFenceCreate(const MG_Pipe::MGPHandleOnly&) override { return true; }
|
||||
Bool OnFenceWait(const MGPFenceWait&, Uint32& result) override {
|
||||
waits.fetch_add(1);
|
||||
result = signaled.load() ? GL_ALREADY_SIGNALED : GL_TIMEOUT_EXPIRED;
|
||||
return true;
|
||||
}
|
||||
void Install() {
|
||||
const auto rc = Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) {
|
||||
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
|
||||
decoder.SetVerbSink(static_cast<CountingFencePeer*>(self));
|
||||
return MOBILEGL_OK;
|
||||
}, this);
|
||||
if (rc != MOBILEGL_OK) ::_exit(160);
|
||||
}
|
||||
};
|
||||
|
||||
TEST(RemoteClientControls, AnUnreportedFencePollCrossesOnlyEveryPollEscalateTimes) {
|
||||
const auto child = RunInChild([] {
|
||||
MG_Config::Ipc.PollEscalate = 4;
|
||||
StartControlSession();
|
||||
CountingFencePeer peer;
|
||||
peer.Install();
|
||||
auto* sync = RemoteEmitTable().GL.FenceSync();
|
||||
if (sync == nullptr) ::_exit(161);
|
||||
for (int i = 0; i < 3; ++i)
|
||||
if (RemoteEmitTable().GL.ClientWaitSync(sync, 0, 0) != GL_TIMEOUT_EXPIRED) ::_exit(162);
|
||||
ClientSessionInstance().WaitForApplyToCatchUp("P10 control");
|
||||
if (peer.waits.load() != 0) ::_exit(163); // three local answers, no record
|
||||
if (RemoteEmitTable().GL.ClientWaitSync(sync, 0, 0) != GL_TIMEOUT_EXPIRED) ::_exit(164);
|
||||
if (peer.waits.load() != 1) ::_exit(165); // the fourth crossed
|
||||
peer.signaled.store(true);
|
||||
for (int i = 0; i < 3; ++i) (void)RemoteEmitTable().GL.ClientWaitSync(sync, 0, 0);
|
||||
if (RemoteEmitTable().GL.ClientWaitSync(sync, 0, 0) != GL_ALREADY_SIGNALED) ::_exit(166);
|
||||
if (peer.waits.load() != 2) ::_exit(167); // the next escalation learned it
|
||||
for (int i = 0; i < 10; ++i)
|
||||
if (RemoteEmitTable().GL.ClientWaitSync(sync, 0, 0) != GL_ALREADY_SIGNALED) ::_exit(168);
|
||||
if (peer.waits.load() != 2) ::_exit(169); // known signaled: answered locally for good
|
||||
RemoteEmitTable().GL.DeleteSync(sync);
|
||||
ClientSessionInstance().Stop();
|
||||
});
|
||||
ExpectChildSuccess(child);
|
||||
}
|
||||
|
||||
TEST(RemoteClientControls, DeviceLostRespecifyDoesNotDieOfTheUncarriedBytesCheck) {
|
||||
// P12 exit gate (a): killing the server must give a CLEAN device-lost latch. With the
|
||||
// latch set every verb is DECLINED, so the resource_subdata that R-13.3 owes emits
|
||||
|
||||
Reference in New Issue
Block a user