mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-17 00:28:31 +09:00
[Test] (MG_Test/Wire): ServerLoopTest round 3 - an EGL fixture brings the server's REAL DirectGLES backend up on the apply thread through the twelve forwarders alone and counts native eglMakeCurrent at the EGL function table; the review-v2 N-5 gates each red on their own string: C7 one native bind / zero native releases / owner unchanged, N-3 destroy-recreate with the same handle values binds again, ID-49 the reply is the tight extent whatever DstSize the client sent, M-3 sparse shadow through pool-reuse and through respecify dies by name, M-1 a queued range outside the staged coverage dies by name at the drain, m-5 a twin surviving context loss drops its freed base, M-2 the ensure uploads the server shadow not MappedData (item 10), M-6 ShutdownSplitRoles after an early Start failure drops the backend, codex-11 the effective mask via an absent cpu, C6 the seven reads answered by two objects; N-9 the park case bounds its own post and the tally case names its tally
This commit is contained in:
@@ -32,20 +32,27 @@
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
|
||||
#include <MG_Backend/DirectGLES/DirectGLES.h>
|
||||
#include <MG_Backend/DirectGLES/Managers.h>
|
||||
#include <MG_Backend/DirectGLES/Utils.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Remote/CapsCodec.h>
|
||||
#include <MG_Remote/Client/ClientSession.h>
|
||||
#include <MG_Remote/Protocol/generated/protocol_generated.h>
|
||||
#include <MG_Remote/Server/PipeApplier.h>
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
#include <MG_Remote/Server/ServerSession.h>
|
||||
#include <MG_Remote/Server/StagedShadow.h>
|
||||
#include <MG_Remote/Transport/InProcessTransport.h>
|
||||
#include <MG_Remote/Transport/ReplySlot.h>
|
||||
#include <MG_Remote/Transport/Ring.h>
|
||||
#include <MG_Remote/Transport/SessionRings.h>
|
||||
#include <MG_Remote/Wire/PipeWireCodec.h>
|
||||
#include <MG_State/GLState/BufferState/BufferObject.h>
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/TextureState/TextureEnum.h>
|
||||
#include <MGGitHash.h>
|
||||
|
||||
@@ -53,11 +60,13 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
@@ -68,6 +77,9 @@
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#if defined(__linux__) || defined(__ANDROID__)
|
||||
#include <sched.h>
|
||||
#endif
|
||||
|
||||
using namespace MobileGL;
|
||||
|
||||
@@ -262,14 +274,39 @@ TEST(ServerLoopTest, AControlRequestRunsOnTheApplyThreadAndUnparksIt) {
|
||||
std::thread::id ranOn{};
|
||||
Bool onApplyThread = false;
|
||||
} probe;
|
||||
const MobileGLResult rc = loop.RunOnApplyThread(
|
||||
+[](void* user) -> MobileGLResult {
|
||||
auto* p = static_cast<Probe*>(user);
|
||||
p->ranOn = std::this_thread::get_id();
|
||||
p->onApplyThread = Server::ServerLoop::OnApplyThread();
|
||||
return MOBILEGL_OK;
|
||||
},
|
||||
&probe);
|
||||
// POSTED FROM A HELPER THREAD AND WAITED FOR WITH A DEADLINE (review v2 N-9). A park predicate
|
||||
// that lost the control flag never un-parks the apply thread and the post blocks for ever; the
|
||||
// case has to SAY that, in its own words, rather than hit ctest's timeout - a timeout is what an
|
||||
// unrelated hang produces too. Stop() afterwards is what frees the helper: m_stopRequested is in
|
||||
// the predicate, the thread exits, and its exit block answers the still-posted request
|
||||
// NOT_INITIALIZED (C2's block), so the join below cannot wedge either.
|
||||
MobileGLResult rc = MOBILEGL_ERR_INVALID_ARGUMENT;
|
||||
std::atomic<Bool> answered{false};
|
||||
std::thread poster([&] {
|
||||
rc = loop.RunOnApplyThread(
|
||||
+[](void* user) -> MobileGLResult {
|
||||
auto* p = static_cast<Probe*>(user);
|
||||
p->ranOn = std::this_thread::get_id();
|
||||
p->onApplyThread = Server::ServerLoop::OnApplyThread();
|
||||
return MOBILEGL_OK;
|
||||
},
|
||||
&probe);
|
||||
answered.store(true, std::memory_order_release);
|
||||
});
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (!answered.load(std::memory_order_acquire) && std::chrono::steady_clock::now() < deadline) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
if (!answered.load(std::memory_order_acquire)) {
|
||||
ADD_FAILURE() << "a posted control request did not un-park the apply thread within 5 s: the park "
|
||||
"predicate no longer carries the control flag, so a Notify alone cannot break a "
|
||||
"kWaitForever park (Doorbell::Wait consumes it, re-tests a condition nothing "
|
||||
"published, and parks again)";
|
||||
fixture.Stop();
|
||||
poster.join();
|
||||
return;
|
||||
}
|
||||
poster.join();
|
||||
|
||||
EXPECT_EQ(rc, MOBILEGL_OK);
|
||||
EXPECT_NE(probe.ranOn, std::this_thread::get_id())
|
||||
@@ -366,13 +403,13 @@ TEST(ServerLoopTest, TheAffinityStringResolvesToAMaskAndTheMaskIsLogged) {
|
||||
MG_Config::Ipc.ServerAffinity = saved;
|
||||
}
|
||||
|
||||
// M-4 / codex 11, the other half: an EXPLICIT mask must resolve to itself and be LOGGED as the
|
||||
// EFFECTIVE mask the kernel took, not the requested one. `off` -> 0 and `0x3` -> 0x3 are two
|
||||
// answers that must DIFFER, so the resolver cannot be a constant; and reading the mask back with
|
||||
// sched_getaffinity is what makes a cpuset that trimmed the request visible (codex 11), which the
|
||||
// requested-mask log hid. Red once by making ApplyAffinity return `requested`: the effective read
|
||||
// is what a trimmed request diverges from, and the explicit-mask assert is what catches a broken
|
||||
// resolver.
|
||||
// M-4, the explicit-mask half: an EXPLICIT mask the box can honour must resolve to itself and be
|
||||
// LOGGED. `off` -> 0 and `0x3` -> 0x3 are two answers that must DIFFER, so the resolver cannot be a
|
||||
// constant. This case does NOT gate codex 11's effective-mask read-back - on an unrestricted box the
|
||||
// request and the effective set are the same and `return requested` stays green here (review v2
|
||||
// item 6, which struck the claim the first version of this comment made); the read-back's own gate
|
||||
// is TheResolvedAffinityMaskIsTheKernelsEffectiveSetNotTheRequest below. Red once by making the
|
||||
// resolver return a constant 0: the explicit-mask assert reads 0.
|
||||
TEST(ServerLoopTest, TheExplicitAffinityMaskResolvesToItselfAndIsLogged) {
|
||||
#if defined(__linux__) || defined(__ANDROID__)
|
||||
if (std::thread::hardware_concurrency() < 2) {
|
||||
@@ -454,7 +491,9 @@ TEST(ServerLoopTest, TheSessionWatermarkAndTheDecoderTallyAgreeAfterEveryRecord)
|
||||
EXPECT_EQ(fixture.session->Consumer().AppliedSeq(), static_cast<Uint64>(i + 1));
|
||||
EXPECT_EQ(fixture.session->Applier().DecoderAppliedSeq(), static_cast<Uint64>(i + 1));
|
||||
}
|
||||
EXPECT_EQ(Server::ServerLoopInstance().DrainedRecords(), 8u);
|
||||
EXPECT_EQ(Server::ServerLoopInstance().DrainedRecords(), 8u)
|
||||
<< "the loop's own record tally (DrainedRecords) did not move with the eight records it "
|
||||
"applied, so the ordering-defect control has nothing of its own to hold against appliedSeq";
|
||||
|
||||
// retiredSeq is the watermark w1's SEG_STAGE allocator reclaims against; a loop that
|
||||
// applies and never retires ends the first MOBILEGL_IPC_STAGE_MB in Fatal{RingOverrun}.
|
||||
@@ -821,11 +860,11 @@ TEST(ServerLoopTest, TheServerFormatCapsAccessorReadsTheServersOwnBackendNotTheG
|
||||
MG_Backend::pActiveBackendObject.reset();
|
||||
}
|
||||
|
||||
// C7 / ID-54: the make-current decision. A new tuple binds; an identical repeat is a no-op; a
|
||||
// release request (the three NO_* markers) is recorded, not forwarded. The native-bind COUNT is a
|
||||
// joint-lane control (it needs a real EGL context); this is the pure decision, which is what
|
||||
// keeps "the context is bound once and held for life" from being a per-call storm. Red once by
|
||||
// deleting the RepeatNoOp arm - an identical repeat then classifies as NativeBind.
|
||||
// C7 / ID-54: the make-current DECISION, pure. A new tuple binds; an identical repeat is a no-op; a
|
||||
// release request (the three NO_* markers) is recorded, not forwarded. The native-bind COUNT - what
|
||||
// the driver actually saw - is measured by ServerLoopEglTest's C7 control on a real context below,
|
||||
// which is the control round 2 said needed the joint lane. Red once by deleting the RepeatNoOp arm:
|
||||
// an identical repeat then classifies as NativeBind.
|
||||
TEST(ServerLoopTest, MakeCurrentClassifiesRepeatAndReleaseWithoutRebinding) {
|
||||
const EGLDisplay dpy = reinterpret_cast<EGLDisplay>(0x1);
|
||||
const EGLSurface draw = reinterpret_cast<EGLSurface>(0x2);
|
||||
@@ -851,6 +890,724 @@ TEST(ServerLoopTest, MakeCurrentClassifiesRepeatAndReleaseWithoutRebinding) {
|
||||
Server::EglBindAction::ClientRelease);
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// Round 3: the gates the v2 review found missing (N-5), each driving the PRODUCTION wiring
|
||||
// =====================================================================================
|
||||
|
||||
// M-6 (review v2 item 7): the fix is the ServerLoop::Stop() call INSIDE ShutdownSplitRoles, so the
|
||||
// gate calls ShutdownSplitRoles - not Stop - after the shape M-6 names: InitSplitRoles step 1 built
|
||||
// the server's private backend and step 3 (ClientSession::Start) failed EARLY. The failure is a real
|
||||
// one, not a skipped step: P5's client refuses every transport but InProcess by name, before Accept
|
||||
// and before any ring exists. Red once by deleting Init.cpp's `ServerLoopInstance().Stop()` line:
|
||||
// the backend outlives the shutdown and the next CreateBackend is refused.
|
||||
TEST(ServerLoopTest, ShutdownSplitRolesAfterAnEarlyStartFailureDropsTheServersPrivateBackend) {
|
||||
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
||||
ASSERT_EQ(loop.CreateBackend(BackendType::DirectGLES), MOBILEGL_OK);
|
||||
ASSERT_NE(MobileGL::MG_Remote::Client::ClientSessionInstance().Start(MG_Config::TransportMode::Spawn, ""),
|
||||
MOBILEGL_OK)
|
||||
<< "the forced early Start failure did not fail; the case has nothing to shut down after";
|
||||
ASSERT_NE(loop.Backend(), nullptr);
|
||||
ASSERT_FALSE(loop.Running());
|
||||
|
||||
MG_Backend::ShutdownSplitRoles();
|
||||
|
||||
EXPECT_EQ(loop.Backend(), nullptr)
|
||||
<< "ShutdownSplitRoles left the server's private backend alive after an early "
|
||||
"ClientSession::Start failure (M-6): every later split bring-up in this process is refused "
|
||||
"at CreateBackend's m_backend guard";
|
||||
EXPECT_FALSE(loop.Running());
|
||||
EXPECT_EQ(loop.CreateBackend(BackendType::DirectGLES), MOBILEGL_OK)
|
||||
<< "the leaked backend blocks the next split bring-up";
|
||||
loop.Stop();
|
||||
}
|
||||
|
||||
// codex 11 (review v2 item 6): the resolved mask must be the EFFECTIVE set the kernel took, read
|
||||
// back with sched_getaffinity, not the request. The review's point was that on an unrestricted box
|
||||
// the two never differ, so `return requested` stays green; this makes them differ without a cpuset:
|
||||
// the request names one cpu this thread may use (taken from the kernel's own answer, so the case
|
||||
// constructs nothing it observes) PLUS cpu 63, which does not exist on any box with fewer than 64
|
||||
// cpus. sched_setaffinity accepts the mask with the absent cpu dropped, so the effective set is the
|
||||
// one real cpu - and a resolver that echoes the request reports a cpu the thread can never run on.
|
||||
// Red once by making ApplyAffinity `return requested;`: the resolved mask reads 0x8000000000000001-
|
||||
// shaped instead of 0x1-shaped, and the log line names the wrong mask.
|
||||
#if defined(__linux__) || defined(__ANDROID__)
|
||||
TEST(ServerLoopTest, TheResolvedAffinityMaskIsTheKernelsEffectiveSetNotTheRequest) {
|
||||
const unsigned hw = std::thread::hardware_concurrency();
|
||||
if (hw == 0 || hw >= 64) {
|
||||
GTEST_SKIP() << "needs a box with fewer than 64 cpus so that cpu 63 is absent (has " << hw << ")";
|
||||
}
|
||||
cpu_set_t allowed;
|
||||
CPU_ZERO(&allowed);
|
||||
ASSERT_EQ(sched_getaffinity(0, sizeof(allowed), &allowed), 0);
|
||||
int usable = -1;
|
||||
for (int cpu = 0; cpu < 63; ++cpu) {
|
||||
if (CPU_ISSET(cpu, &allowed)) {
|
||||
usable = cpu;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (usable < 0 || CPU_ISSET(63, &allowed)) {
|
||||
GTEST_SKIP() << "this thread's allowed set is not the shape the case needs";
|
||||
}
|
||||
const Uint64 requested = (1ull << usable) | (1ull << 63);
|
||||
const Uint64 effective = 1ull << usable;
|
||||
char requestedText[40];
|
||||
std::snprintf(requestedText, sizeof(requestedText), "0x%llx", static_cast<unsigned long long>(requested));
|
||||
char effectiveLine[64];
|
||||
std::snprintf(effectiveLine, sizeof(effectiveLine), "RESOLVED mask 0x%llx (0 = no affinity applied)",
|
||||
static_cast<unsigned long long>(effective));
|
||||
|
||||
const String saved = MG_Config::Ipc.ServerAffinity;
|
||||
MG_Config::Ipc.ServerAffinity = requestedText;
|
||||
|
||||
ServerFixture fixture;
|
||||
ASSERT_TRUE(fixture.Handshake());
|
||||
ASSERT_TRUE(fixture.StartLoop());
|
||||
ASSERT_TRUE(fixture.WaitUntilTrulyParked());
|
||||
|
||||
EXPECT_EQ(Server::ServerLoopInstance().ResolvedAffinityMask(), effective)
|
||||
<< "the resolved affinity mask reports the REQUESTED set (" << requestedText
|
||||
<< ") rather than the effective one the kernel took: cpu 63 does not exist on this " << hw
|
||||
<< "-cpu box, sched_setaffinity dropped it, and codex 11's sched_getaffinity read-back is what "
|
||||
"makes that visible";
|
||||
const std::string log = ReadLog();
|
||||
EXPECT_NE(log.find(effectiveLine), std::string::npos)
|
||||
<< "the logged RESOLVED mask is not the effective one (" << effectiveLine << ")";
|
||||
|
||||
fixture.Stop();
|
||||
MG_Config::Ipc.ServerAffinity = saved;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
// A second DirectGLES object whose format cache the case can FILL, so that the server's (empty,
|
||||
// never InitCapabilities'd) cache and the global's answer differently. Filling an INPUT is not
|
||||
// constructing the observed state (R-16): what is observed is which object each read follows.
|
||||
struct CapsProbeBackend final : MG_Backend::DirectGLES::BackendObject_DirectGLES {
|
||||
MG_Backend::FormatCapabilityCache& Caps() { return MutableFormatCapabilities(); }
|
||||
};
|
||||
} // namespace
|
||||
|
||||
// C6 (review v2 item 9): the seven table-3 reads, asserted by their ANSWERS with two objects
|
||||
// installed - not by the accessor's address, which is what round 2 gated and what reverting one
|
||||
// call site at a time never moved. The server's cache is empty; the global's is not.
|
||||
//
|
||||
// Four of the seven are VALUE reads and the two objects' caches disagree for them: the
|
||||
// HasCachedFormatCapability pair behind the caveat decision (Utils.cpp) and the
|
||||
// ClampSamplesToBackendSupport pair (BackendObject_DirectGLES.cpp). Two are NULL-CHECK reads -
|
||||
// GenerateFormatInfo's and BackendFormatAddsAlpha's "is there a backend at all" - which two live
|
||||
// objects cannot tell apart, so for those the global is NULL and the server is present, which is
|
||||
// exactly the window between InitSplitRoles step 1 (the server backend) and step 4 (the client
|
||||
// object) that the reads used to answer "no backend" in. The seventh, UsesWidenedPacked16NormStorage's
|
||||
// gate in front of a memoised driver probe, is a null check whose two arms give the same answer on
|
||||
// the reference rasterizer (the probe says "not mirrored", the no-backend arm says false); it has
|
||||
// no answer-level control on this box and the report says so.
|
||||
//
|
||||
// Red once, one site at a time: revert the HasCachedFormatCapability pair -> the caveat decision
|
||||
// follows the global; revert ClampSamples -> the clamp answers 6; revert GenerateFormatInfo's null
|
||||
// check -> RGB8 widens to RGBA8; revert BackendFormatAddsAlpha's -> adds-alpha answers true.
|
||||
TEST(ServerLoopTest, TheSevenFormatCapabilityReadsAnswerFromTheServersBackendNotTheGlobal) {
|
||||
namespace TextureImpl = MG_Backend::DirectGLES::TextureImpl;
|
||||
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
||||
ASSERT_EQ(loop.CreateBackend(BackendType::DirectGLES), MOBILEGL_OK);
|
||||
const SizeT tex2d = MG_Backend::GetFormatCapabilityTargetIndex(TextureTarget::Texture2D);
|
||||
const SizeT rb = MG_Backend::GetRenderbufferFormatCapabilityTargetIndex();
|
||||
const SizeT rgb8 = static_cast<SizeT>(TextureInternalFormat::RGB8);
|
||||
const SizeT rgba8 = static_cast<SizeT>(TextureInternalFormat::RGBA8);
|
||||
|
||||
// (A) the global holds a DIFFERENT object whose cache says the opposite of "empty": RGB8 is
|
||||
// caveat-only there, and RGBA8 was "probed" at six samples - a count no driver's own maximum
|
||||
// is, so it cannot collide with the fallback the server's empty cache falls to.
|
||||
auto client = MakeUnique<CapsProbeBackend>();
|
||||
for (const SizeT target : {tex2d, rb}) {
|
||||
client->Caps().CaveatCaps[target][rgb8] |= MG_Backend::FormatCapability::Creatable;
|
||||
client->Caps().CaveatCaps[target][rgb8] |= MG_Backend::FormatCapability::FramebufferRenderable;
|
||||
client->Caps().SampleCounts[target][rgba8] = {6, 2};
|
||||
}
|
||||
MG_Backend::pActiveBackendObject = std::move(client);
|
||||
|
||||
EXPECT_FALSE(TextureImpl::ShouldUseCaveatTextureFormat(TextureInternalFormat::RGB8, TextureTarget::Texture2D))
|
||||
<< "the caveat decision followed the global's cache (caveat-only RGB8), not the server's "
|
||||
"(empty): the HasCachedFormatCapability pair reads pActiveBackendObject again";
|
||||
EXPECT_FALSE(TextureImpl::ShouldUseCaveatRenderbufferFormat(TextureInternalFormat::RGB8))
|
||||
<< "the renderbuffer caveat decision followed the global's cache, not the server's";
|
||||
EXPECT_NE(MG_Backend::DirectGLES::ClampSamplesToBackendSupport(tex2d, TextureInternalFormat::RGBA8, GL_RGBA8, 8), 6)
|
||||
<< "the sample clamp answered from the global's probed counts (6), not the server's";
|
||||
EXPECT_NE(MG_Backend::DirectGLES::ClampSamplesToBackendSupport(rb, TextureInternalFormat::RGBA8, GL_RGBA8, 8), 6)
|
||||
<< "the renderbuffer sample clamp answered from the global's probed counts (6), not the server's";
|
||||
|
||||
// (B) the null-check reads: global NULL, server present.
|
||||
MG_Backend::pActiveBackendObject.reset();
|
||||
GLenum internalFormat = 0;
|
||||
GLenum format = 0;
|
||||
GLenum type = 0;
|
||||
TextureImpl::GenerateTextureFormatInfo(TextureInternalFormat::RGB8, &internalFormat, &format, &type,
|
||||
TextureTarget::Texture2D);
|
||||
EXPECT_EQ(internalFormat, static_cast<GLenum>(GL_RGB8))
|
||||
<< "the format info followed the global (null, so the fallback normalisation widened RGB8) "
|
||||
"instead of the server's backend (present, so native RGB8); got 0x" << std::hex << internalFormat;
|
||||
EXPECT_FALSE(TextureImpl::BackendTextureFormatAddsAlpha(TextureInternalFormat::RGB8, TextureTarget::Texture2D))
|
||||
<< "the adds-alpha answer followed the global (null, so the fallback adds alpha) instead of "
|
||||
"the server's backend";
|
||||
EXPECT_FALSE(TextureImpl::BackendRenderbufferFormatAddsAlpha(TextureInternalFormat::RGB8))
|
||||
<< "the renderbuffer adds-alpha answer followed the global (null) instead of the server's backend";
|
||||
|
||||
loop.Stop();
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// The EGL fixture: the SERVER's real DirectGLES backend, a real headless context (mesa
|
||||
// surfaceless, llvmpipe) on the apply thread, driven only through v1's twelve forwarders
|
||||
// =====================================================================================
|
||||
//
|
||||
// Round 2 shipped its C7, M-3 and m-5 claims with the caveat "the native count is a joint-lane
|
||||
// control (needs a real EGL context)", and the review answered that nothing then gates them. This
|
||||
// fixture is the answer to that: the server role's own backend, brought up exactly the way
|
||||
// InitSplitRoles and the client's EGL virtuals bring it up - CreateBackend on the app thread, then
|
||||
// InitializeEGLDisplay / CreateEGLPbufferSurface / MakeEGLCurrent as BLOCKING control requests on
|
||||
// mgl-srv-apply - with no client object, no emit table and no scenario. What the joint lane adds is
|
||||
// c1's client; what it does not add is anything these cases observe: the driver's own eglMakeCurrent
|
||||
// count, the applier's read_pixels reply, and the backend's buffer twins.
|
||||
//
|
||||
// THE NATIVE COUNT IS READ WHERE THE DRIVER READS IT. DirectGLES dispatches every EGL call through
|
||||
// its function table (g_EGLFuncs, filled by BackendObject_DirectGLES::Initialize); the fixture puts
|
||||
// a counting trampoline in front of that table's eglMakeCurrent and forwards to the driver's entry
|
||||
// point. Nothing in the production tree is edited or told, and no production counter is trusted for
|
||||
// this number - ServerLoop::NativeBindCount() is the forwarder's view, and the whole point of the C7
|
||||
// control is that the two can disagree (they did: 2 native binds per process with the forwarder
|
||||
// counting 1, review v2 item 10).
|
||||
//
|
||||
// NO USABLE EGL IS A SKIP, UNLESS MOBILEGL_ITEST_REQUIRE_GPU SAYS OTHERWISE - the integration
|
||||
// harness's own rule (ScenarioFixture.h): a developer box without mesa should not fail a run it could
|
||||
// not perform, but a lane that relies on these cases sets the variable, and then an unusable EGL is
|
||||
// a FAILURE naming the step, never a green that ran nothing (review v2 N-1's lesson).
|
||||
#if !defined(_WIN32)
|
||||
namespace {
|
||||
|
||||
std::atomic<int> g_nativeEglBinds{0};
|
||||
std::atomic<int> g_nativeEglReleases{0};
|
||||
decltype(MG_Backend::DirectGLES::g_EGLFuncs.eglMakeCurrent) g_driverEglMakeCurrent = nullptr;
|
||||
|
||||
EGLBoolean CountingEglMakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||
if (ctx == EGL_NO_CONTEXT) {
|
||||
g_nativeEglReleases.fetch_add(1, std::memory_order_acq_rel);
|
||||
} else {
|
||||
g_nativeEglBinds.fetch_add(1, std::memory_order_acq_rel);
|
||||
}
|
||||
return g_driverEglMakeCurrent(dpy, draw, read, ctx);
|
||||
}
|
||||
|
||||
Bool RequireGpuFromEnvironment() {
|
||||
const char* value = std::getenv("MOBILEGL_ITEST_REQUIRE_GPU");
|
||||
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
||||
}
|
||||
|
||||
// The integration harness's headless pin (HeadlessGL.cpp, EnsureHeadlessPlatform), for the same
|
||||
// reason: mesa's surfaceless platform needs no display, and glvnd has to be told which vendor to
|
||||
// load. The vendor json is the one MG_IntegrationTest/CMakeLists.txt pins by default; an operator
|
||||
// who set __EGL_VENDOR_LIBRARY_FILENAMES keeps theirs.
|
||||
void PinHeadlessEglEnvironment() {
|
||||
if (std::getenv("EGL_PLATFORM") == nullptr) setenv("EGL_PLATFORM", "surfaceless", 1);
|
||||
unsetenv("DISPLAY");
|
||||
unsetenv("WAYLAND_DISPLAY");
|
||||
if (std::getenv("__EGL_VENDOR_LIBRARY_FILENAMES") == nullptr) {
|
||||
const char* mesa = "/usr/share/glvnd/egl_vendor.d/50_mesa.json";
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(mesa, ec)) setenv("__EGL_VENDOR_LIBRARY_FILENAMES", mesa, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// A control request that runs an arbitrary callable on the apply thread. A std::function is
|
||||
// fine in a test; production's ControlWork is a raw pointer for the teardown path's sake.
|
||||
MobileGLResult OnApply(const std::function<void()>& body) {
|
||||
std::function<void()> copy = body;
|
||||
return Server::ServerLoopInstance().RunOnApplyThread(
|
||||
+[](void* user) -> MobileGLResult {
|
||||
(*static_cast<std::function<void()>*>(user))();
|
||||
return MOBILEGL_OK;
|
||||
},
|
||||
©);
|
||||
}
|
||||
|
||||
struct EglServerFixture : ServerFixture {
|
||||
// The client's VIRTUAL handles. EGLImpl mints small integers, and every one of them is 0x1
|
||||
// on this host (the N-3 finding's whole point), so the cases use exactly that shape.
|
||||
static EGLDisplay Dpy() { return reinterpret_cast<EGLDisplay>(static_cast<std::uintptr_t>(0x1)); }
|
||||
static EGLSurface Surf() { return reinterpret_cast<EGLSurface>(static_cast<std::uintptr_t>(0x1)); }
|
||||
static EGLContext Ctx() { return reinterpret_cast<EGLContext>(static_cast<std::uintptr_t>(0x1)); }
|
||||
|
||||
// The backend's draw-shaped paths (ReadPixels syncs the bound textures and the read
|
||||
// framebuffer) walk the process's frontend GLContext, which a bare unit process does not
|
||||
// have; installed for the case's life and put back afterwards (SplitBufferTest's shape).
|
||||
UniquePtr<MG_State::GLState::GLContext> savedContext;
|
||||
|
||||
// Empty on success, else the step that failed - the harness's SkipReason shape.
|
||||
std::string BringUp() {
|
||||
PinHeadlessEglEnvironment();
|
||||
savedContext = Move(MG_State::pGLContext);
|
||||
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
|
||||
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
||||
// InitSplitRoles step 1: the server's private backend, no GL and no EGL yet; its
|
||||
// Initialize() loads the driver's EGL/GLES entry points into the two tables.
|
||||
if (loop.CreateBackend(BackendType::DirectGLES) != MOBILEGL_OK) return "CreateBackend refused";
|
||||
const MG_External::EGLFunctionsTable& egl = MG_Backend::DirectGLES::g_EGLFuncs;
|
||||
if (egl.eglMakeCurrent == nullptr) return "libEGL did not load (no eglMakeCurrent entry point)";
|
||||
if (egl.eglMakeCurrent != &CountingEglMakeCurrent) g_driverEglMakeCurrent = egl.eglMakeCurrent;
|
||||
g_nativeEglBinds.store(0);
|
||||
g_nativeEglReleases.store(0);
|
||||
MG_External::EGLFunctionsTable counted = egl;
|
||||
counted.eglMakeCurrent = &CountingEglMakeCurrent;
|
||||
MG_Backend::DirectGLES::SetEGLFuncsTable(counted);
|
||||
if (!Handshake()) return "the session handshake";
|
||||
if (!StartLoop()) return "ServerLoop::Start";
|
||||
EGLint major = 0;
|
||||
EGLint minor = 0;
|
||||
if (!Server::ServerInitializeEGLDisplay(Dpy(), &major, &minor)) return "ServerInitializeEGLDisplay";
|
||||
// THE SURFACE'S OWN CREATION is what binds natively (InitPbufferSurface -> MakeCurrent).
|
||||
if (!Server::ServerCreateEGLPbufferSurface(Surf(), 64, 64)) {
|
||||
return "ServerCreateEGLPbufferSurface - no usable headless EGL (EGL_PLATFORM=surfaceless, "
|
||||
"__EGL_VENDOR_LIBRARY_FILENAMES -> mesa/llvmpipe) on this box";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Bool MakeCurrent() { return Server::ServerMakeEGLCurrent(Dpy(), Surf(), Surf(), Ctx()); }
|
||||
Bool ReleaseCurrent() {
|
||||
return Server::ServerMakeEGLCurrent(Dpy(), EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
}
|
||||
|
||||
void TearDown() {
|
||||
Server::ServerReleaseEGLResources();
|
||||
Stop();
|
||||
MG_State::pGLContext = Move(savedContext);
|
||||
}
|
||||
};
|
||||
|
||||
// FAIL() and GTEST_SKIP() both return from the enclosing void test body.
|
||||
#define MGL_EGL_BRING_UP_OR_BAIL(fixture) \
|
||||
do { \
|
||||
const std::string why = (fixture).BringUp(); \
|
||||
if (!why.empty()) { \
|
||||
(fixture).Stop(); \
|
||||
MG_State::pGLContext = Move((fixture).savedContext); \
|
||||
if (RequireGpuFromEnvironment()) { \
|
||||
FAIL() << "MOBILEGL_ITEST_REQUIRE_GPU is set, so the server's EGL bring-up failing is " \
|
||||
"a failure, not a skip. It failed at: " \
|
||||
<< why; \
|
||||
} \
|
||||
GTEST_SKIP() << "no usable headless EGL for the server backend: " << why; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// The production shape of a buffer under split, step by step through the applier's own entry
|
||||
// points and the REAL op table (Initialize registered it): resource_create mints the record,
|
||||
// resource_respecify stores the descriptor - its bytes never cross under split (table 1 row 19),
|
||||
// they arrive as resource_subdata right behind it - and resource_subdata stages bytes into the
|
||||
// server shadow, minting the twin if no draw has (ID-52 item 3). `definedContent` is the
|
||||
// descriptor's HasDefinedContent: what glBufferData(size, data) sets and glBufferData(size, NULL)
|
||||
// clears.
|
||||
MG_Pipe::MGPResourceDesc BufferDesc(Uint32 slot, Uint32 width, Bool definedContent) {
|
||||
MG_Pipe::MGPResourceDesc desc{};
|
||||
desc.Resource = MG_Pipe::MGPipeHandle{slot, 1};
|
||||
desc.Target = MG_Pipe::kMGPipeResourceTargetBuffer;
|
||||
desc.Width = width;
|
||||
desc.Height = 1;
|
||||
desc.Depth = 1;
|
||||
desc.ArrayLayers = 1;
|
||||
desc.Levels = 1;
|
||||
desc.Samples = 1;
|
||||
desc.Usage = static_cast<Uint32>(BufferUsage::StaticDraw);
|
||||
desc.HasDefinedContent = definedContent ? 1 : 0;
|
||||
return desc;
|
||||
}
|
||||
|
||||
MG_Pipe::MGPipeHandle DeclareBuffer(const MG_Pipe::MGPResourceDesc& desc) {
|
||||
EXPECT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc)) << "resource_create refused slot " << desc.Resource.Slot;
|
||||
EXPECT_TRUE(MG_Pipe::MGPipeApplyResourceRespecify(desc, nullptr)) << "resource_respecify refused";
|
||||
return desc.Resource;
|
||||
}
|
||||
|
||||
void StageBytes(MG_Pipe::MGPipeHandle res, SizeT offset, SizeT size, Uint8 fill) {
|
||||
const MG_Pipe::MGPipeResourceOps* ops = MG_Pipe::MGPipeGetResourceOps();
|
||||
ASSERT_NE(ops, nullptr) << "no resource op table is registered";
|
||||
ASSERT_NE(ops->SubData, nullptr);
|
||||
Vector<Uint8> bytes(size, fill);
|
||||
MG_Pipe::MGPSubData rec{};
|
||||
rec.Res = res;
|
||||
rec.Target = MG_Pipe::MGPipePackSubDataTarget(MG_Pipe::kMGPipeResourceTargetBuffer, 0u);
|
||||
ASSERT_TRUE(MG_Pipe::MGPipeSetSubDataBufferRange(rec, offset, size));
|
||||
ops->SubData(res, rec, bytes.data());
|
||||
}
|
||||
|
||||
MG_Backend::DirectGLES::BufferImpl::GLESBufferResource* Ensure(MG_Pipe::MGPipeHandle res) {
|
||||
return MG_Backend::DirectGLES::BufferImpl::EnsureBufferResourceForHandle({}, res);
|
||||
}
|
||||
|
||||
MG_Backend::DirectGLES::BufferImpl::GLESBufferResource* Twin(MG_Pipe::MGPipeHandle res) {
|
||||
return MG_Backend::DirectGLES::BufferImpl::FindBufferResourceForHandle(res);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// C7 / ID-54, THE NATIVE HALF, measured where the review said it was not: at the driver. Surface
|
||||
// creation binds the context natively on the apply thread (that is bind #1 of the "2 per process"
|
||||
// the review counted); the client's first eglMakeCurrent onto that surface used to be bind #2 and is
|
||||
// now deduped by BackendObject_DirectGLES's ID-54 arm; an identical repeat is a forwarder-level
|
||||
// RepeatNoOp; a client release-current is recorded and never reaches the driver; a bind after the
|
||||
// release is a RepeatNoOp again because the driver never lost the context. ONE native bind, ZERO
|
||||
// native releases, and the apply thread is still the owner at the end. Red once, three ways: (a) make
|
||||
// ApplyMakeCurrent ignore the classification (the repeat is forwarded and the release reaches the
|
||||
// driver), (b) make the backend's native call unconditional again (2 native binds), (c) forward the
|
||||
// ClientRelease arm to the backend (1 native release).
|
||||
TEST(ServerLoopEglTest, MakeCurrentBindsNativelyOncePerContextAndNeverForwardsAClientRelease) {
|
||||
EglServerFixture fixture;
|
||||
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
||||
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
||||
|
||||
ASSERT_EQ(g_nativeEglBinds.load(), 1) << "the surface's own creation did not bind natively exactly once";
|
||||
ASSERT_EQ(loop.NativeBindCount(), 0u);
|
||||
|
||||
ASSERT_TRUE(fixture.MakeCurrent());
|
||||
EXPECT_EQ(g_nativeEglBinds.load(), 1)
|
||||
<< "the client's first make-current cost a second native eglMakeCurrent on a surface its own "
|
||||
"creation had already made current on this thread (ID-54: bind once per tuple)";
|
||||
EXPECT_EQ(loop.NativeBindCount(), 1u) << "the first make-current was not forwarded at all";
|
||||
|
||||
ASSERT_TRUE(fixture.MakeCurrent());
|
||||
EXPECT_EQ(loop.NativeBindCount(), 1u)
|
||||
<< "an identical repeat make-current was forwarded as a native bind; the owner slot would be "
|
||||
"written again and the caches invalidated for nothing";
|
||||
EXPECT_EQ(g_nativeEglBinds.load(), 1);
|
||||
|
||||
ASSERT_TRUE(fixture.ReleaseCurrent());
|
||||
EXPECT_EQ(loop.ClientReleaseCount(), 1u) << "the client release-current was not recorded";
|
||||
EXPECT_EQ(g_nativeEglReleases.load(), 0)
|
||||
<< "a client release-current reached the driver as eglMakeCurrent(NO_CONTEXT): the apply thread "
|
||||
"lost the context it holds for life (ID-54: never unbind on a client release)";
|
||||
|
||||
ASSERT_TRUE(fixture.MakeCurrent());
|
||||
EXPECT_EQ(loop.NativeBindCount(), 1u) << "a bind after the recorded, unforwarded release was forwarded again";
|
||||
EXPECT_EQ(g_nativeEglBinds.load(), 1);
|
||||
|
||||
// The owner is still the apply thread by EGL ground truth (IsBackendContextCurrentOnThisThread
|
||||
// re-verifies against eglGetCurrentContext), and not this thread.
|
||||
Bool ownerIsApplyThread = false;
|
||||
ASSERT_EQ(OnApply([&] { ownerIsApplyThread = MG_Backend::DirectGLES::IsBackendContextCurrentOnThisThread(); }),
|
||||
MOBILEGL_OK);
|
||||
EXPECT_TRUE(ownerIsApplyThread) << "after the sequence the apply thread no longer owns the context";
|
||||
EXPECT_FALSE(MG_Backend::DirectGLES::IsBackendContextCurrentOnThisThread()) << "the app thread owns the context";
|
||||
|
||||
fixture.TearDown();
|
||||
}
|
||||
|
||||
// N-3: a destroy-recreate with the SAME handle values must be a real bind again. Before the fix the
|
||||
// tuple survived ServerReleaseEGLResources, the recreated context's first make-current classified as
|
||||
// a RepeatNoOp, nothing ran in the base class (no InitCapabilities, no current-thread record) and no
|
||||
// caps were republished - a silent no-context-current from the forwarder's point of view, visible
|
||||
// only as the next swap failing. Red once by deleting the ForgetCurrentTuple() calls the forwarders
|
||||
// make: NativeBindCount() stays 1 and the swap after the re-create fails.
|
||||
TEST(ServerLoopEglTest, ADestroyedContextForgetsTheTupleSoTheSameHandleValuesBindAgain) {
|
||||
EglServerFixture fixture;
|
||||
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
||||
Server::ServerLoop& loop = Server::ServerLoopInstance();
|
||||
ASSERT_TRUE(fixture.MakeCurrent());
|
||||
ASSERT_EQ(loop.NativeBindCount(), 1u);
|
||||
ASSERT_TRUE(Server::ServerSwapEGLBuffers(EglServerFixture::Dpy(), EglServerFixture::Surf()))
|
||||
<< "the swap BEFORE the destroy failed, so the case could not tell lost bookkeeping from a swap "
|
||||
"that never worked";
|
||||
ASSERT_EQ(g_nativeEglBinds.load(), 1);
|
||||
|
||||
// The context goes (DestroyEGLContext on the apply thread) and comes back under the same
|
||||
// handle values.
|
||||
Server::ServerReleaseEGLResources();
|
||||
EGLint major = 0;
|
||||
EGLint minor = 0;
|
||||
ASSERT_TRUE(Server::ServerInitializeEGLDisplay(EglServerFixture::Dpy(), &major, &minor));
|
||||
ASSERT_TRUE(Server::ServerCreateEGLPbufferSurface(EglServerFixture::Surf(), 64, 64));
|
||||
ASSERT_EQ(g_nativeEglBinds.load(), 2) << "the recreated surface did not bind natively";
|
||||
|
||||
ASSERT_TRUE(fixture.MakeCurrent());
|
||||
EXPECT_EQ(loop.NativeBindCount(), 2u)
|
||||
<< "the make-current after a destroy-recreate with the same handle values was classified as a "
|
||||
"RepeatNoOp: the tuple outlived the context it named (N-3), so no base-class bookkeeping "
|
||||
"ran and the caps snapshot was not republished";
|
||||
EXPECT_TRUE(Server::ServerSwapEGLBuffers(EglServerFixture::Dpy(), EglServerFixture::Surf()))
|
||||
<< "the swap after the re-create failed: BackendObject::SwapEGLBuffers found no current-thread "
|
||||
"record, because the make-current that should have made one was treated as a repeat";
|
||||
EXPECT_EQ(g_nativeEglBinds.load(), 2) << "one native bind per context lifetime, not more";
|
||||
|
||||
fixture.TearDown();
|
||||
}
|
||||
|
||||
// ID-49's tight-size half, gated (review v2 N-5: "nothing, unit and joint"). The client's DstSize is
|
||||
// deliberately WRONG - 80, the size a ROW_LENGTH=8 / SKIP_* client would compute for a 4x3 RGBA8
|
||||
// read whose tight extent is 48 - and the reply must still be the tight 48 bytes: posted at 48,
|
||||
// counted at 48, read into a scratch that grew to 48 and never to the client's 80, and stamped 48 in
|
||||
// the slot header the client reads. Red once, three ways: post at info.DstSize, size the scratch from
|
||||
// info.DstSize, compute `tight` from info.DstSize.
|
||||
TEST(ServerLoopEglTest, AReadPixelsReplyIsTheTightExtentWhateverDstSizeTheClientSent) {
|
||||
EglServerFixture fixture;
|
||||
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
||||
ASSERT_TRUE(fixture.MakeCurrent());
|
||||
|
||||
// THE CLIENT'S HALF OF THE VERB, exactly as glReadPixels runs it before it reaches a backend
|
||||
// (MGP_FILL(ReadPixels) in MG_Impl): the validate point fills gPipeInputs' residual fields
|
||||
// from the frontend context - the texture-unit base, the bound framebuffers, the pack state
|
||||
// - which the server's ReadPixels reads BARRIER-PULLED under R-1 while the client is parked
|
||||
// in the barrier (PipeInputs.h's class table). Without it the apply thread reads a block
|
||||
// nobody filled and walks a null texture-unit base; with it the case drives the same two
|
||||
// halves the inproc lane drives, in the same order, on the same process-wide block.
|
||||
MG_Pipe::MGPipeValidateForVerb(MG_Pipe::MGPipeVerb::ReadPixels);
|
||||
|
||||
MG_Pipe::MGPReadbackInfo info{};
|
||||
info.Res = MG_Pipe::kMGPipeNullHandle; // read_pixels: the bound read surface answers
|
||||
info.Box = MG_Pipe::MGPBox{0, 0, 0, 4, 3, 1};
|
||||
info.Format = 0x1908; // GL_RGBA
|
||||
info.Type = 0x1401; // GL_UNSIGNED_BYTE
|
||||
info.DstSize = 80; // wrong on purpose; tight is 4 * 3 * 4 = 48
|
||||
const Uint64 seq = fixture.encoder.EncodeRecord(MG_Pipe::MGPWireOp::ReadPixels, &info, sizeof(info));
|
||||
ASSERT_NE(seq, Codec::kInvalidSeq);
|
||||
fixture.encoder.Publish();
|
||||
fixture.producer.PublishAndNotify(seq);
|
||||
ASSERT_EQ(fixture.producer.WaitForApplied(seq, 5000), Transport::SessionWait::Reached);
|
||||
|
||||
const Server::ServerVerbSink& verbs = fixture.session->Applier().Verbs();
|
||||
ASSERT_EQ(verbs.Readbacks(), 1u) << "the read_pixels record was declined or errored rather than applied";
|
||||
EXPECT_EQ(verbs.ReadbackBytes(), 48u)
|
||||
<< "the reply was posted at the client's DstSize (80), not the tight w*h*bpp extent (48) (ID-49)";
|
||||
EXPECT_EQ(verbs.ReadbackScratchBytes(), 48u)
|
||||
<< "the scratch was sized from the client's DstSize rather than the tight extent; a DstSize "
|
||||
"smaller than tight would then be the heap overflow codex 1 found";
|
||||
|
||||
// And through the client's own view of the slot: the seq stamped back, OK, 48 bytes.
|
||||
Transport::ReplySlotPool replies(fixture.clientSegments.ReplyBase(), fixture.clientSegments.ReplyBytes(),
|
||||
fixture.clientSegments.ReplySlotCount());
|
||||
ASSERT_TRUE(replies.Valid());
|
||||
Vector<Uint8> pixels(128, 0);
|
||||
Int32 status = -1;
|
||||
Uint64 size = 0;
|
||||
EXPECT_TRUE(replies.Read(seq, pixels.data(), pixels.size(), &status, &size));
|
||||
EXPECT_EQ(status, static_cast<Int32>(Transport::kReplyStatusOk));
|
||||
EXPECT_EQ(size, 48u) << "the slot header carries the client's DstSize, not the tight extent";
|
||||
|
||||
fixture.TearDown();
|
||||
}
|
||||
|
||||
// M-3 / codex 4, the POOL-REUSE reader (review v2 item 4: "nothing can notice it going away"). The
|
||||
// production sequence, on the real op table and the real backend: A - glBufferData(64, data), drawn
|
||||
// (ensured), then deleted on the apply thread, so its id retires into the buffer pool; two presents
|
||||
// with a finish between them move the frame-completion watermark past the id's retire serial, which
|
||||
// is the pool's hand-out rule. B - glBufferData(64, NULL) then glBufferSubData(0, 16), the ordinary
|
||||
// streaming idiom, so its server shadow covers [0, 16) of 64. B's first ensure finds A's id in the
|
||||
// pool and would seed the driver's whole 64-byte store from that shadow, 48 bytes of which nothing
|
||||
// staged. The refusal fires BEFORE the glBufferSubData, so the forked child - whose only thread is a
|
||||
// copy of this one and holds no context - reaches it with no GL call that matters (a no-context call
|
||||
// dispatches to a no-op). Red once by deleting the pool-reuse MGL_SERVER_STAGED_REQUIRE: the child
|
||||
// seeds the store and does not die.
|
||||
TEST(StagedShadowProductionTest, ASparseShadowForcedThroughAPoolReuseUploadIsFatalByName) {
|
||||
EglServerFixture fixture;
|
||||
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
||||
ASSERT_TRUE(fixture.MakeCurrent());
|
||||
|
||||
const MG_Pipe::MGPipeHandle a = DeclareBuffer(BufferDesc(21, 64, true));
|
||||
StageBytes(a, 0, 64, 0x11);
|
||||
Bool aEnsured = false;
|
||||
ASSERT_EQ(OnApply([&] {
|
||||
aEnsured = Ensure(a) != nullptr;
|
||||
MG_Pipe::MGPipeGetResourceOps()->Destroy(a);
|
||||
for (int frame = 0; frame < 2; ++frame) {
|
||||
if (MG_Backend::DirectGLES::g_GLESFuncs.glFinish) MG_Backend::DirectGLES::g_GLESFuncs.glFinish();
|
||||
MG_Backend::DirectGLES::Present();
|
||||
}
|
||||
}),
|
||||
MOBILEGL_OK);
|
||||
ASSERT_TRUE(aEnsured) << "A's ensure minted no storage, so nothing retired into the pool";
|
||||
|
||||
const MG_Pipe::MGPipeHandle b = DeclareBuffer(BufferDesc(22, 64, false));
|
||||
StageBytes(b, 0, 16, 0x22);
|
||||
ASSERT_NE(Twin(b), nullptr) << "the subdata did not mint B's twin (ID-52 item 3)";
|
||||
ASSERT_EQ(Twin(b)->id, 0u) << "B already has a store; the pool-reuse arm cannot be reached";
|
||||
|
||||
EXPECT_EXIT(Ensure(b), ::testing::KilledBySignal(SIGABRT), ".*");
|
||||
const std::string log = ReadLog();
|
||||
EXPECT_NE(log.find("Fatal{StageSnapshotTooNarrow, \"pool_reuse_whole_store\"}"), std::string::npos)
|
||||
<< "the abort was not the pool-reuse reader's coverage refusal (M-3); the log says: " << log;
|
||||
|
||||
fixture.TearDown();
|
||||
}
|
||||
|
||||
// M-3 / codex 4, the RESPECIFY reader. A - glBufferData(64, data), drawn (ensured: id, storage,
|
||||
// serial all current). Then glBufferData(64, data2) again, whose bytes do not cross (table 1 row 19):
|
||||
// the record says "defined content", the old shadow is dropped, the store is pending a respecify -
|
||||
// and only 16 of the 64 bytes then arrive, staged from the app thread the way the applier queues
|
||||
// them off the context thread. The next ensure would RespecifyStorageWith(initialData = the sparse
|
||||
// shadow) over the whole store. The refusal fires before that glBufferData. Red once by deleting the
|
||||
// respecify MGL_SERVER_STAGED_REQUIRE.
|
||||
TEST(StagedShadowProductionTest, ASparseShadowForcedThroughAStorageRespecifyIsFatalByName) {
|
||||
EglServerFixture fixture;
|
||||
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
||||
ASSERT_TRUE(fixture.MakeCurrent());
|
||||
|
||||
const MG_Pipe::MGPResourceDesc desc = BufferDesc(23, 64, true);
|
||||
const MG_Pipe::MGPipeHandle res = DeclareBuffer(desc);
|
||||
StageBytes(res, 0, 64, 0x33);
|
||||
Bool ensured = false;
|
||||
ASSERT_EQ(OnApply([&] { ensured = Ensure(res) != nullptr; }), MOBILEGL_OK);
|
||||
ASSERT_TRUE(ensured);
|
||||
ASSERT_NE(Twin(res)->id, 0u);
|
||||
|
||||
// Off the context thread on purpose (the apply thread is parked): the respecify takes the
|
||||
// "cannot touch GL now" arm and leaves the store PENDING, exactly as an emitted record does.
|
||||
ASSERT_TRUE(MG_Pipe::MGPipeApplyResourceRespecify(desc, nullptr));
|
||||
ASSERT_TRUE(Twin(res)->pendingRespecify) << "the respecify did not leave the store pending";
|
||||
StageBytes(res, 0, 16, 0x44);
|
||||
|
||||
EXPECT_EXIT(Ensure(res), ::testing::KilledBySignal(SIGABRT), ".*");
|
||||
const std::string log = ReadLog();
|
||||
EXPECT_NE(log.find("Fatal{StageSnapshotTooNarrow, \"respecify_whole_store\"}"), std::string::npos)
|
||||
<< "the abort was not the respecify reader's coverage refusal (M-3); the log says: " << log;
|
||||
|
||||
fixture.TearDown();
|
||||
}
|
||||
|
||||
// M-1 / codex 8, THE COVERAGE HALF (review v2 item 2: "gated nowhere"). glBufferData(64, NULL) +
|
||||
// glBufferSubData(0, 16) + a draw: the shadow covers [0, 16) and the NULL respecify seeds nothing, so
|
||||
// that is legal. Then a resource_flush_range over [16, 48) with no bytes (that is what the row
|
||||
// carries under split, item 12) queues a range NOTHING STAGED, and the next draw-time drain would
|
||||
// take it through the three-tier ladder - whose tier 1 is a range-invalidating map that declares
|
||||
// the old bytes dead. RequireStagedCoverageForPendingRanges refuses before the drain, by name. Red
|
||||
// once by neutering its loop body: the child then dies one tier later, inside UploadRangeFrom's own
|
||||
// check, with a DIFFERENT site name - which this assertion refuses - or, on the tier-1 map, not at
|
||||
// all.
|
||||
TEST(StagedShadowProductionTest, AQueuedRangeOutsideTheStagedCoverageIsFatalAtTheDrainByName) {
|
||||
EglServerFixture fixture;
|
||||
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
||||
ASSERT_TRUE(fixture.MakeCurrent());
|
||||
|
||||
const MG_Pipe::MGPipeHandle res = DeclareBuffer(BufferDesc(24, 64, false));
|
||||
StageBytes(res, 0, 16, 0x55);
|
||||
Bool ensured = false;
|
||||
ASSERT_EQ(OnApply([&] { ensured = Ensure(res) != nullptr; }), MOBILEGL_OK);
|
||||
ASSERT_TRUE(ensured);
|
||||
ASSERT_NE(Twin(res)->id, 0u);
|
||||
ASSERT_FALSE(Twin(res)->pendingRespecify);
|
||||
|
||||
MG_Pipe::MGPFlushRange flush{};
|
||||
flush.Res = res;
|
||||
flush.Offset = 16;
|
||||
flush.Size = 32;
|
||||
MG_Pipe::MGPipeGetResourceOps()->FlushRange(res, flush, nullptr); // off the context thread: queued
|
||||
ASSERT_FALSE(Twin(res)->pendingRanges.empty()) << "the flush queued nothing, so no drain is owed";
|
||||
|
||||
EXPECT_EXIT(Ensure(res), ::testing::KilledBySignal(SIGABRT), ".*");
|
||||
const std::string log = ReadLog();
|
||||
EXPECT_NE(log.find("Fatal{StageSnapshotTooNarrow, \"ensure_flush_pending\"}"), std::string::npos)
|
||||
<< "the abort was not the pending-coverage clamp's (M-1, RequireStagedCoverageForPendingRanges); "
|
||||
"the log says: " << log;
|
||||
|
||||
fixture.TearDown();
|
||||
}
|
||||
|
||||
// m-5 / codex 5 (review v2 item 5: "the shipped control cannot reach the freed base"). This one can:
|
||||
// glBufferData(64, data) drawn (the twin's hostBytes names the server shadow), then the context is
|
||||
// lost (ServerReleaseEGLResources -> DestroyEGLContext -> OnBackendContextDestroyed -> DropAll frees
|
||||
// every shadow; the twin table survives), then the context comes back and the twin is re-armed at
|
||||
// its next ensure. The re-armed twin's base must be NULL. Without the null it still names the freed
|
||||
// allocation and the whole-store respecify that follows reads it - the use-after-free ASan would
|
||||
// name, asserted here as the dangling pointer itself. Red once by deleting the null.
|
||||
TEST(StagedShadowProductionTest, ATwinSurvivingContextLossDropsItsFreedShadowBase) {
|
||||
EglServerFixture fixture;
|
||||
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
||||
ASSERT_TRUE(fixture.MakeCurrent());
|
||||
|
||||
const MG_Pipe::MGPipeHandle res = DeclareBuffer(BufferDesc(25, 64, true));
|
||||
StageBytes(res, 0, 64, 0x66);
|
||||
Bool ensured = false;
|
||||
ASSERT_EQ(OnApply([&] { ensured = Ensure(res) != nullptr; }), MOBILEGL_OK);
|
||||
ASSERT_TRUE(ensured);
|
||||
ASSERT_NE(Twin(res)->hostBytes, nullptr) << "the drawn twin names no shadow, so there is nothing to free";
|
||||
|
||||
Server::ServerReleaseEGLResources();
|
||||
ASSERT_NE(Twin(res), nullptr) << "the twin did not survive context loss; the m-5 hazard needs a survivor";
|
||||
|
||||
EGLint major = 0;
|
||||
EGLint minor = 0;
|
||||
ASSERT_TRUE(Server::ServerInitializeEGLDisplay(EglServerFixture::Dpy(), &major, &minor));
|
||||
ASSERT_TRUE(Server::ServerCreateEGLPbufferSurface(EglServerFixture::Surf(), 64, 64));
|
||||
ASSERT_TRUE(fixture.MakeCurrent());
|
||||
ASSERT_EQ(OnApply([&] { ensured = Ensure(res) != nullptr; }), MOBILEGL_OK);
|
||||
ASSERT_TRUE(ensured);
|
||||
|
||||
EXPECT_EQ(Twin(res)->hostBytes, nullptr)
|
||||
<< "a twin that survived context loss still names the freed server shadow (m-5): the "
|
||||
"whole-store respecify that re-armed it read freed memory (ASan: heap-use-after-free at "
|
||||
"RespecifyStorageWith)";
|
||||
|
||||
fixture.TearDown();
|
||||
}
|
||||
|
||||
// M-2 / ID-52 item 3, SHIPPED AND EXECUTED (joint report joint-v1.md 3, "Audit and its R-16
|
||||
// limit": the corrupt-staged-bytes perturbation existed only by hand). Under an active transport
|
||||
// the server's staged copy is the draw's ONLY base; the frontend object's MappedData() - which under
|
||||
// inproc is the same process's memory and always non-null - may not be read. So: a real frontend
|
||||
// BufferObject whose shadow holds pattern A, a server shadow staged with pattern B through the real
|
||||
// op table (what resource_subdata delivers), and the production ensure path given BOTH. The driver
|
||||
// store must hold B. That is the executable form of "corrupt only the staged bytes and the draw
|
||||
// shows it": B is the corruption, the upload is the draw, the mapped read-back is the picture. Red
|
||||
// once by restoring the MappedData() fallback in liveHostBase(): the store then holds A, the
|
||||
// client's bytes, and the R-2.5 audit could never reach a draw again.
|
||||
TEST(StagedShadowProductionTest, TheEnsurePathUploadsTheServerShadowNotTheClientObjectsBytes) {
|
||||
EglServerFixture fixture;
|
||||
MGL_EGL_BRING_UP_OR_BAIL(fixture);
|
||||
ASSERT_TRUE(fixture.MakeCurrent());
|
||||
|
||||
// The frontend object: glBufferData(64, A). Its shadow is A and it declares defined content.
|
||||
Vector<Uint8> clientBytes(64, Uint8{0xA5});
|
||||
auto buffer = MakeShared<MG_State::GLState::BufferObject>(31u);
|
||||
buffer->Respecify(clientBytes.size(), clientBytes.data());
|
||||
ASSERT_NE(buffer->MappedData(), nullptr);
|
||||
ASSERT_EQ(buffer->MappedData()[0], 0xA5);
|
||||
ASSERT_TRUE(buffer->HasDefinedContent());
|
||||
|
||||
// The server shadow for the twin the draw will use: B, staged the way the wire delivers it.
|
||||
const MG_Pipe::MGPipeHandle res = DeclareBuffer(BufferDesc(26, 64, true));
|
||||
StageBytes(res, 0, 64, 0x5B);
|
||||
|
||||
// The production ensure, WITH the frontend object (the draw-time shape), on the apply thread;
|
||||
// then the driver's store read back through a READ map on the same thread.
|
||||
Vector<Uint8> store(64, 0);
|
||||
Bool ensured = false;
|
||||
Bool mapped = false;
|
||||
ASSERT_EQ(OnApply([&] {
|
||||
ensured = MG_Backend::DirectGLES::BufferImpl::EnsureBufferResourceForHandle(buffer, res) != nullptr;
|
||||
auto* twin = Twin(res);
|
||||
if (!ensured || twin == nullptr || twin->id == 0) return;
|
||||
const auto& gl = MG_Backend::DirectGLES::g_GLESFuncs;
|
||||
gl.glBindBuffer(GL_ARRAY_BUFFER, twin->id);
|
||||
void* view = gl.glMapBufferRange(GL_ARRAY_BUFFER, 0, 64, GL_MAP_READ_BIT);
|
||||
if (view != nullptr) {
|
||||
std::memcpy(store.data(), view, 64);
|
||||
gl.glUnmapBuffer(GL_ARRAY_BUFFER);
|
||||
mapped = true;
|
||||
}
|
||||
}),
|
||||
MOBILEGL_OK);
|
||||
ASSERT_TRUE(ensured);
|
||||
ASSERT_TRUE(mapped) << "the driver store could not be mapped for reading, so the case cannot see the upload";
|
||||
|
||||
EXPECT_EQ(store[0], 0x5B)
|
||||
<< "the ensure path uploaded the CLIENT object's bytes (MappedData(), pattern A = 0xA5) rather "
|
||||
"than the server shadow (pattern B = 0x5B): under an active transport the staged copy must "
|
||||
"be the draw's only base (M-2 / ID-52 item 3), or a corrupt staged upload renders the "
|
||||
"frontend's correct bytes and the R-2.5 audit cannot reach a draw";
|
||||
EXPECT_EQ(store[63], 0x5B);
|
||||
// And the frontend object was not written through: its shadow is still A.
|
||||
EXPECT_EQ(buffer->MappedData()[0], 0xA5);
|
||||
|
||||
fixture.TearDown();
|
||||
}
|
||||
#endif // !_WIN32
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// Before anything logs: MG_Util::Debug::InitFile() reads the variable once, on the first
|
||||
// write, and caches the FILE*. The name carries this process's pid, because
|
||||
|
||||
Reference in New Issue
Block a user