mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-16 06:23:23 +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:
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user