mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-18 09:08:31 +09:00
Merge branch 'p5c-ct' into feat/disaggregated (P5c ct: applier_reset / object_death control records)
# Conflicts: # MobileGL/MG_Backend/DirectGLES/Managers.h
This commit is contained in:
@@ -22,6 +22,10 @@
|
|||||||
#include <MG_Remote/Server/StagedShadow.h>
|
#include <MG_Remote/Server/StagedShadow.h>
|
||||||
#include <MG_Remote/Server/StagedTextureStore.h>
|
#include <MG_Remote/Server/StagedTextureStore.h>
|
||||||
#include <MG_Remote/Server/ServerLoop.h>
|
#include <MG_Remote/Server/ServerLoop.h>
|
||||||
|
// P5c (ct): object_death's producer (CONTRACT-P5C.md §5.2) - the death notice's split arm
|
||||||
|
// emits the record through the client's emit helper instead of hopping a stack struct to the
|
||||||
|
// apply thread.
|
||||||
|
#include <MG_Remote/Client/WireTables.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "Utils.h"
|
#include "Utils.h"
|
||||||
@@ -207,10 +211,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void OnFrontendStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
|
void OnFrontendStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
|
||||||
if (InProcessTeardown()) return;
|
if (InProcessTeardown()) return;
|
||||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
// Death notices have no framebuffer wire opcode. Keep the lifetime/slot valid
|
// P5c (ct), CONTRACT-P5C.md §5.2: with an active transport the death crosses AS A
|
||||||
// until the context owner has destroyed its twin and updated its binding cache.
|
// RECORD - object_death, the framebuffer family's first wire delete opcode. The GL
|
||||||
|
// thread resolves the dying object's handle in its OWN allocator inside
|
||||||
|
// EmitObjectDeathRecord: no handle means the server never saw the object and
|
||||||
|
// NOTHING crosses (which replaces the mailbox's unconditional delivery), and a
|
||||||
|
// handle means the record's EmitAndWait orders the death against in-flight verbs
|
||||||
|
// that name it - the one property the blocking RunOnApplyThread hop provided and
|
||||||
|
// the only one P5c keeps. The sink (ServerVerbSink::OnObjectDeath) releases the
|
||||||
|
// kind's twin table by handle on the apply thread, so neither a lifetime-id probe
|
||||||
|
// nor the client's allocator is touched from there any more.
|
||||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||||
!MG_Remote::Server::ServerLoop::OnApplyThread()) {
|
!MG_Remote::Server::ServerLoop::OnApplyThread()) {
|
||||||
|
if (MG_Remote::Client::EmitObjectDeathRecord(kind, lifetimeId) !=
|
||||||
|
MG_Remote::Client::ObjectDeathEmit::NoSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// NoSession is the ONE shape the record cannot carry: a transport configured
|
||||||
|
// with no client session at all - a ServerLoop fixture driving a server role
|
||||||
|
// with no client, never a real split (there a session exists whenever the
|
||||||
|
// server does). The server loop IS there, so the death takes the delivery
|
||||||
|
// that predates the record, kept VERBATIM for exactly this arm: a stack
|
||||||
|
// struct hopped to the apply thread, which runs the switch below. NoHandle
|
||||||
|
// does NOT land here - a handle that never existed has no twin to kill (§5.2)
|
||||||
|
// - and a loop that is not RUNNING (teardown, pre-Start) has no thread to hop
|
||||||
|
// to; the twin dies with the server either way.
|
||||||
|
if (!MG_Remote::Server::ServerLoopInstance().Running()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
struct Death { MG_Pipe::MGPipeKind kind; Uint64 lifetimeId; } death{kind, lifetimeId};
|
struct Death { MG_Pipe::MGPipeKind kind; Uint64 lifetimeId; } death{kind, lifetimeId};
|
||||||
MG_Remote::Server::ServerLoopInstance().RunOnApplyThread(
|
MG_Remote::Server::ServerLoopInstance().RunOnApplyThread(
|
||||||
+[](void* user) -> MobileGLResult {
|
+[](void* user) -> MobileGLResult {
|
||||||
@@ -296,6 +324,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
Bool ReleaseTwinsForWireObjectDeath(MG_Pipe::MGPipeHandle handle, MG_Pipe::MGPipeKind kind) {
|
||||||
|
// The handle-keyed twin of the notice switch above, and deliberately NOT that switch:
|
||||||
|
// the record carried the handle, so no lifetime id is probed and the client's
|
||||||
|
// allocator - a client-only surface under a transport (CONTRACT-P5C.md §3.1) - is
|
||||||
|
// never touched from the apply thread. Each arm names its kind's registry GLOBAL for
|
||||||
|
// the same spelling reason as the notice arms: ReleaseByHandle /
|
||||||
|
// ReleaseTwinByHandle is static and is answered by every table of the kind that
|
||||||
|
// exists, this global's own and any by-value copy a fixture or a context reset holds.
|
||||||
|
switch (kind) {
|
||||||
|
case MG_Pipe::MGPipeKind::Texture:
|
||||||
|
return TextureImpl::g_backendTextureObjects.ReleaseByHandle(handle);
|
||||||
|
case MG_Pipe::MGPipeKind::Framebuffer:
|
||||||
|
return FramebufferImpl::g_backendFramebufferObjects.ReleaseByHandle(handle);
|
||||||
|
case MG_Pipe::MGPipeKind::Renderbuffer:
|
||||||
|
return RenderbufferImpl::g_backendRenderbufferObjects.ReleaseByHandle(handle);
|
||||||
|
case MG_Pipe::MGPipeKind::SamplerCso:
|
||||||
|
return SamplerImpl::g_backendSamplerObjects.ReleaseByHandle(handle);
|
||||||
|
case MG_Pipe::MGPipeKind::ShaderCso:
|
||||||
|
return PrgramImpl::g_backendProgramObjects.ReleaseByHandle(handle);
|
||||||
|
case MG_Pipe::MGPipeKind::SamplerViewCso:
|
||||||
|
// The notice arm's REDUNDANT SECOND PATH, keyed by the view's handle the same
|
||||||
|
// way (CONTRACT-P5C.md §5.2): the client's delete_sampler_view may already have
|
||||||
|
// released the twin, in which case the generation check inside fails and this
|
||||||
|
// answers false. A false here is idempotency, never a leak - the view twin owns
|
||||||
|
// no driver id of its own.
|
||||||
|
return SamplerViewImpl::BackendSamplerViewTable::ReleaseTwinByHandle(handle);
|
||||||
|
case MG_Pipe::MGPipeKind::VertexElementsCso:
|
||||||
|
return VertexArrayImpl::g_backendVertexArrayObjects.ReleaseByHandle(handle);
|
||||||
|
default:
|
||||||
|
// Buffer's death crosses as resource_destroy (P3a), exactly as the notice
|
||||||
|
// switch's default arm rules; every other kind has no twin table here.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// The one sentence that decides the arm, written once so that a test can drive every
|
// The one sentence that decides the arm, written once so that a test can drive every
|
||||||
// combination of the two knobs and so that bring-up and first-use cannot disagree.
|
// combination of the two knobs and so that bring-up and first-use cannot disagree.
|
||||||
EsprytSlotArmVerdict ClassifyEsprytSlotArm(Bool subsystemBitSet, Bool legacyMemosEnabled) {
|
EsprytSlotArmVerdict ClassifyEsprytSlotArm(Bool subsystemBitSet, Bool legacyMemosEnabled) {
|
||||||
|
|||||||
@@ -484,17 +484,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// NO ReleaseByHandle HERE, AND THAT IS A DECISION (review M-4). The death half of
|
// NO ReleaseByHandle HERE THROUGH P5, AND THAT WAS A DECISION (review M-4). The death
|
||||||
// GetOrCreateByHandle exists for a kind whose announcement is its own destroy CALL
|
// half of GetOrCreateByHandle existed only for a kind whose announcement is its own
|
||||||
// rather than the shared death notice - which is the BUFFER family
|
// destroy CALL rather than the shared death notice - which was the BUFFER family
|
||||||
// (BackendBufferResourceTable::ReleaseByHandle, SlotTables.h, called from
|
// (BackendBufferResourceTable::ReleaseByHandle, SlotTables.h, called from
|
||||||
// resource_destroy) and none of the five kinds this registry serves: every one of them
|
// resource_destroy) and none of the five kinds this registry serves: every one of
|
||||||
// dies through DestroyByLifetimeId below, because P4a adds no server-side destroy arm
|
// them died through DestroyByLifetimeId below, because P4a added no server-side
|
||||||
// for a texture, a renderbuffer, a framebuffer, a sampler CSO or a shader CSO. v1
|
// destroy arm for a texture, a renderbuffer, a framebuffer, a sampler CSO or a shader
|
||||||
// declared one here anyway and it had no caller on either arm, which made its bound and
|
// CSO. P5c (ct) is the commit that gives the wrapper its caller: object_death carries
|
||||||
// its wording things nobody would exercise until P5. The one-line wrapper comes back in
|
// the dead object's HANDLE on the wire (CONTRACT-P5C.md §5.2), and the static
|
||||||
// the commit that gives it a caller; SlotTable::ReleaseByHandle underneath is untouched
|
// ReleaseByHandle below is what the sink's per-kind dispatch calls.
|
||||||
// and is what SanityTest drives directly.
|
|
||||||
|
// P5c (ct), CONTRACT-P5C.md §5.2: the wrapper M-4 below deferred, given its caller by
|
||||||
|
// object_death. The record carried the handle, so the release is keyed by it and the
|
||||||
|
// client's allocator is never asked from this side (rule E); every holder of the kind
|
||||||
|
// lets go, exactly as DestroyByLifetimeId walks them. What this does NOT do is the
|
||||||
|
// allocator Free the notice arm performs - the slot's owner is the client, which
|
||||||
|
// already returned it after the record went out. STATIC for the same reason
|
||||||
|
// DestroyByLifetimeId is: a death is about an object, not a table instance.
|
||||||
|
static Bool ReleaseByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||||
|
if (EsprytSlotTablesEnabled()) {
|
||||||
|
return SlotTable::ReleaseTwinByHandle(handle);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// P2 step e2. STATIC, because a death notice is about an object and not about a
|
// P2 step e2. STATIC, because a death notice is about an object and not about a
|
||||||
// registry instance: it is answered by EVERY table of this kind that exists - this
|
// registry instance: it is answered by EVERY table of this kind that exists - this
|
||||||
@@ -2806,4 +2819,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
extern TwinRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject, MG_Pipe::MGPipeKind::Renderbuffer>
|
extern TwinRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject, MG_Pipe::MGPipeKind::Renderbuffer>
|
||||||
g_backendRenderbufferObjects;
|
g_backendRenderbufferObjects;
|
||||||
} // namespace RenderbufferImpl
|
} // namespace RenderbufferImpl
|
||||||
|
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
// P5c (ct), CONTRACT-P5C.md §5.2: object_death's per-kind release, one entry point for all
|
||||||
|
// seven kinds for the same reason the notice switch is one - the answer is the same for
|
||||||
|
// all of them: every holder of the kind's twin table lets go of the twin at this handle.
|
||||||
|
// Called from ServerVerbSink::OnObjectDeath ON THE APPLY THREAD, with the handle the
|
||||||
|
// record carried; the client's allocator is never consulted (rule E). Returns whether any
|
||||||
|
// table released a twin - false for a kind this backend does not twin (Buffer: its death
|
||||||
|
// crosses as resource_destroy) and for a handle no holder holds, which the kind's own
|
||||||
|
// delete opcode may already have released (the idempotent-second-path shape the notice
|
||||||
|
// arms document).
|
||||||
|
Bool ReleaseTwinsForWireObjectDeath(MG_Pipe::MGPipeHandle handle, MG_Pipe::MGPipeKind kind);
|
||||||
|
#endif
|
||||||
} // namespace MobileGL::MG_Backend::DirectGLES
|
} // namespace MobileGL::MG_Backend::DirectGLES
|
||||||
|
|||||||
@@ -484,6 +484,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// P5c (ct), CONTRACT-P5C.md §5.2: the handle-keyed half of the above, for a death that
|
||||||
|
// arrived AS A WIRE RECORD (object_death) rather than as the shared notice. The
|
||||||
|
// handle IS the resolution - it crossed in the record's payload - so the client's
|
||||||
|
// allocator is never asked: under an active transport MGPipeSlots() is a client-only
|
||||||
|
// surface (rule E, §3.1) and this function runs on the apply thread. Every holder
|
||||||
|
// lets go exactly as the notice arm does, in the same successor-first order. What
|
||||||
|
// does NOT happen here is the allocator Free: the slot's owner - the client -
|
||||||
|
// returned it itself after the record went out (PipeFill.cpp's NotifyAndFree), and a
|
||||||
|
// double Free would be refused by generation anyway. Idempotent against the kind's
|
||||||
|
// own delete opcode, exactly as the notice arm is: a twin the delete already released
|
||||||
|
// fails ReleaseTwinAt's generation check and the walk moves on.
|
||||||
|
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||||
|
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
|
||||||
|
Bool released = false;
|
||||||
|
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
|
||||||
|
BackendSlotTable* const next = holder->m_nextHolder;
|
||||||
|
released = holder->ReleaseTwinAt(handle) || released;
|
||||||
|
holder = next;
|
||||||
|
}
|
||||||
|
return released;
|
||||||
|
}
|
||||||
|
|
||||||
// How many tables of this type exist right now. For the tests that pin the holder
|
// How many tables of this type exist right now. For the tests that pin the holder
|
||||||
// list; nothing on a shipping path asks.
|
// list; nothing on a shipping path asks.
|
||||||
static Uint32 HolderCount() {
|
static Uint32 HolderCount() {
|
||||||
|
|||||||
@@ -2690,8 +2690,38 @@ namespace MobileGL::MG_Pipe {
|
|||||||
// - what the server has is no longer what any suppressor slot last emitted;
|
// - what the server has is no longer what any suppressor slot last emitted;
|
||||||
// - and the residual block owes a fresh publication whatever else moved.
|
// - and the residual block owes a fresh publication whatever else moved.
|
||||||
if (tracker.FreshlyPrimed()) {
|
if (tracker.FreshlyPrimed()) {
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
// P5c (ct), CONTRACT-P5C.md §5.1: with an active transport the server's reset
|
||||||
|
// crosses AS A RECORD, ahead of every reset below - the client-side ones (the CSO
|
||||||
|
// cache, the hash suppressor, the vertex-input emitter) and the emitters' latches
|
||||||
|
// - because the record's barrier is what orders the server's MGPipeApplierReset()
|
||||||
|
// against every verb that follows. The GL-thread direct call it replaces is
|
||||||
|
// Fatal{RoleViolation, "g_applier"} inside MGPipeApplierReset itself (§6 layer 2),
|
||||||
|
// so reverting this arm to the direct call goes red by name rather than rendering
|
||||||
|
// stale. Monolith keeps the direct call, byte for byte (G1).
|
||||||
|
//
|
||||||
|
// THE APPLY THREAD IS EXCLUDED (M5's rule): a validate running on the server's own
|
||||||
|
// thread produces no client record - EmitAndWait there would wait on the thread
|
||||||
|
// that has to apply the record - and the direct call is exactly what the apply
|
||||||
|
// thread is allowed to make (the sink's own path runs it there).
|
||||||
|
//
|
||||||
|
// AND A CONFIGURED-BUT-WIRELESS TRANSPORT TAKES THE DIRECT CALL TOO:
|
||||||
|
// EmitApplierResetRecord answers false when no live session could carry the
|
||||||
|
// record (the pre-Start bring-up window, a ServerLoop fixture with no client at
|
||||||
|
// all), and in that shape this process IS the only place the reset can run.
|
||||||
|
const Bool transportActive =
|
||||||
|
MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||||
|
!MG_Remote::Client::RunsAsTheServerRole();
|
||||||
|
Bool resetCrossed = false;
|
||||||
|
if (transportActive) {
|
||||||
|
resetCrossed = MG_Remote::Client::EmitApplierResetRecord();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
MGPipeCsoCacheInstance().Reset();
|
MGPipeCsoCacheInstance().Reset();
|
||||||
MGPipeApplierReset();
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
if (!resetCrossed)
|
||||||
|
#endif
|
||||||
|
MGPipeApplierReset();
|
||||||
MGPipeSetHashSuppressorInstance().InvalidateAll();
|
MGPipeSetHashSuppressorInstance().InvalidateAll();
|
||||||
// P3a: and the vertex-input emitter's latches. NOT because the applier dropped
|
// P3a: and the vertex-input emitter's latches. NOT because the applier dropped
|
||||||
// its vertex-elements records - it does not, they are share-group object state
|
// its vertex-elements records - it does not, they are share-group object state
|
||||||
|
|||||||
@@ -2172,6 +2172,29 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
|||||||
endforeach()
|
endforeach()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records, split-only source for the
|
||||||
|
# same reason as f1's - the scenario reads the SERVER sink's tallies, which a monolith build
|
||||||
|
# has no symbol for. One block per case with a LOG PATH OF ITS OWN, the F1 pattern:
|
||||||
|
# SplitLogPaths.PrivateAndDistinct fails a lane whose cases share one file, and the death
|
||||||
|
# case's child truncates whatever path it inherits.
|
||||||
|
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||||
|
target_sources(MobileGLIntegrationTest PRIVATE Scenarios/CtWireScenario.cpp)
|
||||||
|
foreach(ctCase ApplierResetCrossesAtThePrimedEdgeInSerialOrder
|
||||||
|
TextureDeathCrossesAndTheRecycledSlotAnswersTheNewObject
|
||||||
|
FramebufferDeathCrossesAndTheRecycledSlotAnswersTheNewObject
|
||||||
|
TheDirectApplierResetCallOnTheGLThreadIsRoleViolation)
|
||||||
|
gtest_discover_tests(MobileGLIntegrationTest
|
||||||
|
TEST_PREFIX "DirectGLES.Split.Ct."
|
||||||
|
TEST_FILTER "CtWireScenario.${ctCase}"
|
||||||
|
DISCOVERY_TIMEOUT 30
|
||||||
|
PROPERTIES
|
||||||
|
LABELS "integration-gpu\;integration-split"
|
||||||
|
TIMEOUT ${MGL_ITEST_TIMEOUT}
|
||||||
|
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p5c-ct-${ctCase}.log"
|
||||||
|
)
|
||||||
|
endforeach()
|
||||||
|
endif()
|
||||||
|
|
||||||
# P5b measured named-blit blockers, exercising both backend bound-form lowerings.
|
# P5b measured named-blit blockers, exercising both backend bound-form lowerings.
|
||||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||||
foreach(namedBlitBackend DirectGLES DirectVulkan)
|
foreach(namedBlitBackend DirectGLES DirectVulkan)
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CtWireScenario.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
|
||||||
|
|
||||||
|
// P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records, end to end over inproc.
|
||||||
|
//
|
||||||
|
// applier_reset (§5.1): this process's first validate primes the pipe tracker, and the
|
||||||
|
// FreshlyPrimed edge is the record's producer (MG_Impl/Pipe/PipeFill.cpp). What the scenario
|
||||||
|
// asserts is the SERVER sink's own counters - moved by nobody else - plus the pixels, so a
|
||||||
|
// reset that never crossed is red by the tally and a client that fell through to the driver
|
||||||
|
// is red by ScenarioFixture's emit-ordinal rule.
|
||||||
|
//
|
||||||
|
// object_death (§5.2): a texture and a framebuffer die, the deaths cross, and the slots
|
||||||
|
// recycle. The picture after recycling is the behavioural half: a stale twin answering for
|
||||||
|
// the recycled object renders the DEAD object's content (or errors), which is the exact
|
||||||
|
// shape LiveGenAt's generation check exists to refuse.
|
||||||
|
//
|
||||||
|
// RED ONCE for the reset (executed, recorded in the package report): reverting PipeFill's
|
||||||
|
// FreshlyPrimed arm to the GL-thread direct MGPipeApplierReset() call aborts this whole lane
|
||||||
|
// with Fatal{RoleViolation, "g_applier"} out of the applier's layer-2 guard. The automated
|
||||||
|
// half of that control is TheDirectApplierResetCallOnTheGLThreadIsRoleViolation below, and
|
||||||
|
// the guard's two non-Fatal arms are pinned in PipeWireCodecTest's CtWireFatals suite.
|
||||||
|
|
||||||
|
#include "../Harness/ScenarioFixture.h"
|
||||||
|
#include "../Harness/SplitRuntimePeek.h"
|
||||||
|
|
||||||
|
#include <MG_Pipe/PipeApply.h>
|
||||||
|
#include <MG_Remote/Server/ServerSession.h>
|
||||||
|
|
||||||
|
#if !defined(_WIN32)
|
||||||
|
#include <csignal>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef GLAPI
|
||||||
|
#undef GLAPI
|
||||||
|
#endif
|
||||||
|
#define GL_GLEXT_PROTOTYPES
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glcorearb.h>
|
||||||
|
#undef GL_GLEXT_PROTOTYPES
|
||||||
|
|
||||||
|
namespace MGITest {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
MobileGL::MG_Remote::Server::ServerVerbSink& ServerVerbs() {
|
||||||
|
return MobileGL::MG_Remote::Server::ServerSessionInstance().Applier().Verbs();
|
||||||
|
}
|
||||||
|
|
||||||
|
class CtWireScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
const auto why = SplitRuntimeSkipReason();
|
||||||
|
if (!why.empty()) GTEST_SKIP() << why;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One 4x4 RGBA8 texture whose every texel is `rgba`, uploaded so the object crosses
|
||||||
|
// (no handle, no record - CONTRACT-P5C.md §5.2: an object that never crossed emits
|
||||||
|
// nothing at death, and this case needs a real death).
|
||||||
|
GLuint MakeSolidTexture(const GLubyte rgba[4]) {
|
||||||
|
GLuint texture = 0;
|
||||||
|
glGenTextures(1, &texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
GLubyte texels[4 * 4 * 4];
|
||||||
|
for (int i = 0; i < 4 * 4; ++i) {
|
||||||
|
for (int c = 0; c < 4; ++c) texels[i * 4 + c] = rgba[c];
|
||||||
|
}
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, texels);
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The texture's level-0 content, read back through a throwaway FBO, as four bytes.
|
||||||
|
// The FBO is bound AND deleted here, so it never leaks a framebuffer death into the
|
||||||
|
// tally a case is watching.
|
||||||
|
void ReadTextureLevel(GLuint texture, GLubyte out[4]) {
|
||||||
|
GLuint fbo = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
|
||||||
|
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(CtWireScenario, ApplierResetCrossesAtThePrimedEdgeInSerialOrder) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
// The first verb of the process primes the tracker; the edge is the producer. The
|
||||||
|
// clear also gives the case its pixels, so a lane that emitted nothing is red twice
|
||||||
|
// (here and in the fixture's emit-ordinal rule).
|
||||||
|
glClearColor(0.2f, 0.4f, 0.6f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
EXPECT_GE(ServerVerbs().ApplierResets(), 1u)
|
||||||
|
<< "the FreshlyPrimed edge emitted no applier_reset; the server's g_applier was "
|
||||||
|
"never reset for this session";
|
||||||
|
// The serial sequence is the session's own count (§1: asserted, never dispatched
|
||||||
|
// on): every record the sink accepted carried the serial it expected, or the counter
|
||||||
|
// and the accepted tally would disagree.
|
||||||
|
EXPECT_EQ(ServerVerbs().ExpectedApplierResetSerial(), ServerVerbs().ApplierResets())
|
||||||
|
<< "an applier_reset record was dropped or replayed on the wire";
|
||||||
|
|
||||||
|
GLubyte pixel[4]{};
|
||||||
|
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "Ct.ApplierReset.error";
|
||||||
|
const int expected[4] = {51, 102, 153, 255};
|
||||||
|
for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1) << "Ct.ApplierReset.pixels";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CtWireScenario, TextureDeathCrossesAndTheRecycledSlotAnswersTheNewObject) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
const GLubyte red[4] = {255, 0, 0, 255};
|
||||||
|
const GLubyte green[4] = {0, 255, 0, 255};
|
||||||
|
|
||||||
|
// A texture lives and crosses (the upload emits its records). It is also READ BACK
|
||||||
|
// once before it dies: the read forces the server-side sync that creates the twin -
|
||||||
|
// and, one level down, resolves the Espryt slot arm that INSTALLS the death-notice
|
||||||
|
// ops, which no process has before its first twin lookup. The red picture is the
|
||||||
|
// pre-death control the recycle below is read against.
|
||||||
|
GLuint texture = MakeSolidTexture(red);
|
||||||
|
GLubyte before[4]{};
|
||||||
|
ReadTextureLevel(texture, before);
|
||||||
|
const int redExpected[4] = {255, 0, 0, 255};
|
||||||
|
for (int i = 0; i < 4; ++i) ASSERT_NEAR(before[i], redExpected[i], 1) << "Ct.TextureDeath.before";
|
||||||
|
|
||||||
|
// The object dies unbound so the destructor - and the death record - fire at the
|
||||||
|
// delete, and the tally is read AFTER the EmitAndWait the delete blocked on.
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
const MobileGL::Uint64 deathsBefore = ServerVerbs().ObjectDeaths();
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
EXPECT_GT(ServerVerbs().ObjectDeaths(), deathsBefore)
|
||||||
|
<< "the texture's death produced no object_death record; the server's twin was "
|
||||||
|
"never told to let go";
|
||||||
|
|
||||||
|
// The slot recycles forward: a new texture (the frontend hands the same GL name back
|
||||||
|
// more often than not, which is exactly the ABA shape) must answer with ITS content,
|
||||||
|
// not the dead twin's. Red then green, read back as pixels: a stale twin is red.
|
||||||
|
texture = MakeSolidTexture(green);
|
||||||
|
GLubyte pixel[4]{};
|
||||||
|
ReadTextureLevel(texture, pixel);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glDeleteTextures(1, &texture);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "Ct.TextureDeath.error";
|
||||||
|
const int expected[4] = {0, 255, 0, 255};
|
||||||
|
for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1)
|
||||||
|
<< "Ct.TextureDeath.pixels - the recycled texture read back the dead twin's content";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CtWireScenario, FramebufferDeathCrossesAndTheRecycledSlotAnswersTheNewObject) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
// Framebuffer is the kind object_death EXISTS for: it has no other wire delete
|
||||||
|
// opcode (CONTRACT-P5C.md §5.2). The renderbuffer goes along so the FBO has storage.
|
||||||
|
GLuint fbo = 0, renderbuffer = 0;
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glGenRenderbuffers(1, &renderbuffer);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 4, 4);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE))
|
||||||
|
<< "Ct.FramebufferDeath.setup";
|
||||||
|
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
GLubyte pixel[4]{};
|
||||||
|
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
||||||
|
const int blue[4] = {0, 0, 255, 255};
|
||||||
|
for (int i = 0; i < 4; ++i) ASSERT_NEAR(pixel[i], blue[i], 1) << "Ct.FramebufferDeath.first";
|
||||||
|
|
||||||
|
// Die unbound, framebuffer first so the attachment's own death is a separate record.
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||||
|
const MobileGL::Uint64 deathsBefore = ServerVerbs().ObjectDeaths();
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
EXPECT_GT(ServerVerbs().ObjectDeaths(), deathsBefore)
|
||||||
|
<< "the framebuffer's death produced no object_death record - and no other "
|
||||||
|
"opcode can carry it";
|
||||||
|
glDeleteRenderbuffers(1, &renderbuffer);
|
||||||
|
|
||||||
|
// Recycle: a new FBO and a new backing store, cleared to a different colour. A stale
|
||||||
|
// twin answering for the recycled framebuffer renders the blue of the dead one.
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glGenRenderbuffers(1, &renderbuffer);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 4, 4);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||||
|
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE))
|
||||||
|
<< "Ct.FramebufferDeath.recycle";
|
||||||
|
glClearColor(1.0f, 1.0f, 0.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
glDeleteRenderbuffers(1, &renderbuffer);
|
||||||
|
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "Ct.FramebufferDeath.error";
|
||||||
|
const int yellow[4] = {255, 255, 0, 255};
|
||||||
|
for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], yellow[i], 1)
|
||||||
|
<< "Ct.FramebufferDeath.pixels - the recycled framebuffer read back the dead twin's content";
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !defined(_WIN32)
|
||||||
|
TEST_F(CtWireScenario, TheDirectApplierResetCallOnTheGLThreadIsRoleViolation) {
|
||||||
|
if (!Ready()) return;
|
||||||
|
// A verb in the PARENT, so the fixture's emit-ordinal rule has something to see: the
|
||||||
|
// death statement below runs only in the child, and a parent that emitted nothing
|
||||||
|
// is the shape that rule exists to fail.
|
||||||
|
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
// THE RED-ONCE FOR §5.1, AUTOMATED. This process has a LIVE inproc session, and the
|
||||||
|
// GL-thread direct call the applier_reset record replaced is a named Fatal there
|
||||||
|
// (CONTRACT-P5C.md §5.1 / §6 layer 2) - which is exactly the shape reverting
|
||||||
|
// PipeFill's FreshlyPrimed arm would take, so the guard is what turns that revert
|
||||||
|
// red. EXPECT_EXIT re-runs the case in a child process: the child brings up its own
|
||||||
|
// session (its SetUp is this same fixture's) and aborts at the statement. The two
|
||||||
|
// NON-Fatal arms - monolith, and a transport with no client session - are the unit
|
||||||
|
// suite's CtWireFatals.TheDirectApplierResetCallSurvivesMonolithAndAWirelessTransport.
|
||||||
|
//
|
||||||
|
// The regex is ".*" because the library's Fatal line goes to its log file, not to
|
||||||
|
// the stderr a death test matches (ServerLoopEglTest's own EXPECT_EXIT does the
|
||||||
|
// same); the NAME is asserted from the log below, which the child truncated and
|
||||||
|
// wrote before it died.
|
||||||
|
EXPECT_EXIT(MobileGL::MG_Pipe::MGPipeApplierReset(), ::testing::KilledBySignal(SIGABRT), ".*");
|
||||||
|
if (const char* logPath = std::getenv("MOBILEGL_LOG_FILE_PATH"); logPath != nullptr) {
|
||||||
|
std::ifstream in(logPath, std::ios::binary);
|
||||||
|
std::ostringstream log;
|
||||||
|
log << in.rdbuf();
|
||||||
|
EXPECT_NE(log.str().find("Fatal{RoleViolation, \"g_applier\"}"), std::string::npos)
|
||||||
|
<< "the child aborted, but not with the layer-2 guard's own line:\n"
|
||||||
|
<< log.str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -1633,6 +1633,33 @@ namespace MobileGL::MG_Pipe {
|
|||||||
};
|
};
|
||||||
MGP_ASSERT_POD(MGPCopyFromFramebuffer, 48);
|
MGP_ASSERT_POD(MGPCopyFromFramebuffer, 48);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
// P5c: the two appended rows (MG_Remote/CONTRACT-P5C.md §5). APPENDED, never inserted:
|
||||||
|
// applier_reset is opcode 77 and object_death opcode 78, and no earlier opcode moved.
|
||||||
|
//
|
||||||
|
// Neither has an MGPipeApply* entry point and neither gains one. Both reach
|
||||||
|
// MG_Remote::Wire::WireVerbSink like the five P5b verbs, and under monolith neither has a
|
||||||
|
// producer - the reset is the GL thread's direct MGPipeApplierReset() call and the death
|
||||||
|
// notice's mailbox hop, byte for byte as today (G1/G2).
|
||||||
|
// ---------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// applier_reset = tracker.FreshlyPrimed()'s server half (P5c §5.1). The one field is the
|
||||||
|
// client-side context's make-current serial at the edge that primed it: 0 is the first
|
||||||
|
// make-current. P5c has exactly one context per session, so the sink ASSERTS the value
|
||||||
|
// against the session's own count rather than dispatching on it; P6's multi-context shape
|
||||||
|
// is what reads it for real.
|
||||||
|
struct MGPApplierReset {
|
||||||
|
Uint64 ContextSerial;
|
||||||
|
};
|
||||||
|
MGP_ASSERT_POD(MGPApplierReset, 8);
|
||||||
|
|
||||||
|
// object_death carries no payload of its own: MGPHandleOnly (:93-98) IS the payload
|
||||||
|
// (CONTRACT-P5C.md §1) - Handle the dead frontend object's handle, Kind its MGPipeKind
|
||||||
|
// widened, one row for all seven kinds the death switch handles. A null handle never
|
||||||
|
// crosses: the client emits nothing when its own allocator cannot resolve the dying
|
||||||
|
// object (§5.2), so a null handle arriving is Fatal{ProtocolCorruption,
|
||||||
|
// "ObjectDeath.Handle"} at the sink.
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
// Reverse channel payloads (section 7.1)
|
// Reverse channel payloads (section 7.1)
|
||||||
// ---------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -37,6 +37,13 @@
|
|||||||
// MOBILEGL_PIPE_VERIFY block above on purpose: the tier is a property of the BUILD, not of the
|
// MOBILEGL_PIPE_VERIFY block above on purpose: the tier is a property of the BUILD, not of the
|
||||||
// comparator, and a split build without the comparator still declines every acquisition.
|
// comparator, and a split build without the comparator still declines every acquisition.
|
||||||
#include <MG_Remote/Client/PersistentMapTracker.h>
|
#include <MG_Remote/Client/PersistentMapTracker.h>
|
||||||
|
// P5c (ct), CONTRACT-P5C.md §6 layer 2: MGPipeApplierReset's role guard asks
|
||||||
|
// ServerLoop::OnApplyThread() - the one predicate that tells the GL thread from the thread
|
||||||
|
// that owns g_applier under an active transport - and ClientSession::Active(), which is what
|
||||||
|
// tells a live wire from a configured-but-wireless one (the bring-up window and the
|
||||||
|
// server-role-only fixture: there the direct call is the only reset that exists).
|
||||||
|
#include <MG_Remote/Client/ClientSession.h>
|
||||||
|
#include <MG_Remote/Server/ServerLoop.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -1212,6 +1219,28 @@ namespace MobileGL::MG_Pipe {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void MGPipeApplierReset() {
|
void MGPipeApplierReset() {
|
||||||
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
// P5c (ct), CONTRACT-P5C.md §5.1 / §6 layer 2. With an active transport g_applier is
|
||||||
|
// SERVER-PRIVATE and the GL thread's reset crosses as the applier_reset RECORD
|
||||||
|
// (PipeFill.cpp's FreshlyPrimed arm emits it; ServerVerbSink::OnApplierReset runs this
|
||||||
|
// function ON the apply thread). Reaching here from any other thread WITH A LIVE WIRE
|
||||||
|
// is the direct call the record replaced - a write into server memory with no wire
|
||||||
|
// shape, and exactly the revert the red-once gate must catch - so it is a named Fatal
|
||||||
|
// rather than a silent reset of state another role owns. The window the Fatal
|
||||||
|
// deliberately does NOT cover is a configured-but-wireless transport: before
|
||||||
|
// ClientSession::Start() there is no wire for a record to cross (§6 layer 2's
|
||||||
|
// documented bring-up exception), and a ServerLoop fixture with no client at all has
|
||||||
|
// none either - there this call is the only reset that exists. Monolith keeps the
|
||||||
|
// direct call, byte for byte (G1); in a pull build none of this is compiled at all.
|
||||||
|
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||||
|
MG_Remote::Client::ClientSession::Active() != nullptr &&
|
||||||
|
!MG_Remote::Server::ServerLoop::OnApplyThread()) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{RoleViolation, \"g_applier\"} - MGPipeApplierReset() called "
|
||||||
|
"off the apply thread with an active transport; under split the reset "
|
||||||
|
"crosses as the applier_reset record (CONTRACT-P5C.md §5.1)");
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
g_applier.RenderStateCsos.clear();
|
g_applier.RenderStateCsos.clear();
|
||||||
g_applier.BoundRenderStateCso = kMGPipeNullHandle;
|
g_applier.BoundRenderStateCso = kMGPipeNullHandle;
|
||||||
g_applier.Residual = ResidualValueBlock{};
|
g_applier.Residual = ResidualValueBlock{};
|
||||||
|
|||||||
@@ -41,17 +41,18 @@
|
|||||||
// that the expansion, the two generated tables and this number agree.
|
// that the expansion, the two generated tables and this number agree.
|
||||||
//
|
//
|
||||||
// class entries group (as the plan tabulates it)
|
// class entries group (as the plan tabulates it)
|
||||||
// kScreen 11 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the
|
// kScreen 12 screen: caps 1 + resource 3 + persistent map 2 + fence 4, plus the
|
||||||
// appended server-side fence wait 1
|
// appended server-side fence wait 1 and P5c's applier_reset 1
|
||||||
// kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2
|
// kCtxQuery 8 query object namespace 6, plus the appended timestamp pair 2
|
||||||
// kCtxCso 13 CSO create/bind/delete
|
// kCtxCso 13 CSO create/bind/delete
|
||||||
// kCtxState 17 16 of the 17 set_* calls + the temporary set_residual_value_state
|
// kCtxState 17 16 of the 17 set_* calls + the temporary set_residual_value_state
|
||||||
// kCtxObject 9 set_texture_params (the 17th set_*) + 8 object-scoped transfers
|
// kCtxObject 10 set_texture_params (the 17th set_*) + 8 object-scoped transfers, plus
|
||||||
|
// P5c's object_death 1
|
||||||
// kCtxVerb 18 3 context-reading transfer calls + the 10 commands, plus the five
|
// kCtxVerb 18 3 context-reading transfer calls + the 10 commands, plus the five
|
||||||
// P5b-appended verbs (bind_shader_image, patch_parameter,
|
// P5b-appended verbs (bind_shader_image, patch_parameter,
|
||||||
// bind_stream_output, set_storage_block_binding,
|
// bind_stream_output, set_storage_block_binding,
|
||||||
// copy_framebuffer_to_texture - MG_Remote/CONTRACT-P5B.md)
|
// copy_framebuffer_to_texture - MG_Remote/CONTRACT-P5B.md)
|
||||||
// total 76
|
// total 78
|
||||||
//
|
//
|
||||||
// Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they
|
// Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they
|
||||||
// do not add up to a set of UNIQUE records and this file has to hold unique records:
|
// do not add up to a set of UNIQUE records and this file has to hold unique records:
|
||||||
@@ -88,9 +89,15 @@
|
|||||||
// 76 unique records. The catalogue footer's "ShaderStorageBlockBinding is folded into the
|
// 76 unique records. The catalogue footer's "ShaderStorageBlockBinding is folded into the
|
||||||
// reflection archive" is CORRECTED by the row: the archive carries the bindings the
|
// reflection archive" is CORRECTED by the row: the archive carries the bindings the
|
||||||
// program LINKED with, and glShaderStorageBlockBinding moves one AFTER link.
|
// program LINKED with, and glShaderStorageBlockBinding moves one AFTER link.
|
||||||
|
// - P5c (MG_Remote/CONTRACT-P5C.md §5.1/§5.2) appended TWO more after those (opcodes 77..78),
|
||||||
|
// same rule: applier_reset, the make-current edge's server-side reset that used to be a
|
||||||
|
// GL-thread direct call into server memory, and object_death, the framebuffer family's
|
||||||
|
// FIRST wire delete opcode and the handle-keyed replacement for the death-notice mailbox.
|
||||||
|
// Neither has an MGPipeApply* entry point and neither gains one; both reach WireVerbSink.
|
||||||
|
// 78 unique records.
|
||||||
// ---------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------
|
||||||
|
|
||||||
#define MGP_CALL_LIST_DOCUMENTED_COUNT 76
|
#define MGP_CALL_LIST_DOCUMENTED_COUNT 78
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
#define MGP_CALL_LIST(X) \
|
#define MGP_CALL_LIST(X) \
|
||||||
@@ -230,7 +237,26 @@
|
|||||||
X(SetStorageBlockBinding, MGPStorageBlockBinding, kCtxVerb, kHasBlob) \
|
X(SetStorageBlockBinding, MGPStorageBlockBinding, kCtxVerb, kHasBlob) \
|
||||||
/* glCopyTexImage2D / glCopyTexSubImage2D - a copy whose SOURCE is the read framebuffer, */ \
|
/* glCopyTexImage2D / glCopyTexSubImage2D - a copy whose SOURCE is the read framebuffer, */ \
|
||||||
/* which resource_copy_region (resource -> resource) cannot express. */ \
|
/* which resource_copy_region (resource -> resource) cannot express. */ \
|
||||||
X(CopyFramebufferToTexture, MGPCopyFromFramebuffer, kCtxVerb, kNone)
|
X(CopyFramebufferToTexture, MGPCopyFromFramebuffer, kCtxVerb, kNone) \
|
||||||
|
/* ---- APPENDED BY P5c (MG_Remote/CONTRACT-P5C.md §5): the two control records. Opcodes ---- */ \
|
||||||
|
/* ---- 77..78. Neither has an MGPipeApply* entry point and neither gains one: both reach ---- */ \
|
||||||
|
/* ---- WireVerbSink, and under monolith neither has a producer - the reset is a direct ---- */ \
|
||||||
|
/* ---- MGPipeApplierReset() call and the death notice's mailbox hop, byte for byte as ---- */ \
|
||||||
|
/* ---- today (G1/G2). ---- */ \
|
||||||
|
/* tracker.FreshlyPrimed() - a make-current is a fresh server. Under a transport the GL */ \
|
||||||
|
/* thread may not reset the server's g_applier itself (rule E), so the reset crosses AS */ \
|
||||||
|
/* THE CALL, ahead of the block's client-side resets; the record's barrier orders it */ \
|
||||||
|
/* against every verb that follows. ContextSerial is the client context's make-current */ \
|
||||||
|
/* serial at the edge - ASSERTED against the session's, never dispatched on (P5c: one */ \
|
||||||
|
/* context per session). */ \
|
||||||
|
X(ApplierReset, MGPApplierReset, kScreen, kNone) \
|
||||||
|
/* A frontend state object died. The record carries the HANDLE (and kind) the client's own */ \
|
||||||
|
/* allocator resolves for it - nothing is emitted for an object that never crossed (the */ \
|
||||||
|
/* server never saw it, so there is no twin to kill) - and the sink releases the kind's */ \
|
||||||
|
/* twin table by handle. The framebuffer family's FIRST wire delete opcode: its death used */ \
|
||||||
|
/* to hop to the apply thread through a stack-struct mailbox and a lifetime-id probe of */ \
|
||||||
|
/* the client's allocator, both rule-E surfaces. */ \
|
||||||
|
X(ObjectDeath, MGPHandleOnly, kCtxObject, kNone)
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"):
|
// Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"):
|
||||||
|
|||||||
@@ -262,6 +262,12 @@
|
|||||||
F(Dst) F(Target) F(Level) F(InternalFormat) F(X) F(Y) F(Width) F(Height) F(XOffset) F(YOffset) \
|
F(Dst) F(Target) F(Level) F(InternalFormat) F(X) F(Y) F(Width) F(Height) F(XOffset) F(YOffset) \
|
||||||
F(SubImage)
|
F(SubImage)
|
||||||
|
|
||||||
|
// ---- P5c's two appended rows (MG_Remote/CONTRACT-P5C.md §5). object_death's payload IS
|
||||||
|
// MGPHandleOnly, whose list already exists above; applier_reset's is one field.
|
||||||
|
|
||||||
|
#define MGP_FIELDS_MGPApplierReset(F) \
|
||||||
|
F(ContextSerial)
|
||||||
|
|
||||||
// ---- the value structs and the host span (P1 brief D8). Not call payloads themselves, but
|
// ---- the value structs and the host span (P1 brief D8). Not call payloads themselves, but
|
||||||
// members of ones (ResidualValueBlock, MGPPixelPackState, MGPCaps) and of PipeInputs, so the
|
// members of ones (ResidualValueBlock, MGPPixelPackState, MGPCaps) and of PipeInputs, so the
|
||||||
// comparator has to see INTO them: with these lists the memcmp fallback of MGPipeFieldEqual is
|
// comparator has to see INTO them: with these lists the memcmp fallback of MGPipeFieldEqual is
|
||||||
@@ -379,6 +385,7 @@
|
|||||||
P(MGPSurfaceInfo) \
|
P(MGPSurfaceInfo) \
|
||||||
P(MGPImageBind) P(MGPPatchParameter) P(MGPStreamOutputBind) P(MGPStorageBlockBinding) \
|
P(MGPImageBind) P(MGPPatchParameter) P(MGPStreamOutputBind) P(MGPStorageBlockBinding) \
|
||||||
P(MGPCopyFromFramebuffer) \
|
P(MGPCopyFromFramebuffer) \
|
||||||
|
P(MGPApplierReset) \
|
||||||
P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \
|
P(RenderStateParameters) P(PixelStoreParameters) P(SamplerParameters) P(PerBufferBlendState) \
|
||||||
P(StencilFaceState) \
|
P(StencilFaceState) \
|
||||||
P(DynamicBackendParameters) P(MGHostSpan) \
|
P(DynamicBackendParameters) P(MGHostSpan) \
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
|
// Regenerate with `python3 scripts/gen_pipe.py`; CI runs it and diffs the result.
|
||||||
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
|
// This file is included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe.
|
||||||
|
|
||||||
// share group: 11 calls. A null entry means the backend does not implement this
|
// share group: 12 calls. A null entry means the backend does not implement this
|
||||||
// call and the frontend keeps its own path (plan B section 4.1).
|
// call and the frontend keeps its own path (plan B section 4.1).
|
||||||
struct MGPipeScreen {
|
struct MGPipeScreen {
|
||||||
void (*GetCaps)(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply);
|
void (*GetCaps)(const MGPCaps* payload, const void* blobBytes, Uint64 blobByteCount, MGPReplySlot* reply);
|
||||||
@@ -26,9 +26,10 @@ struct MGPipeScreen {
|
|||||||
void (*FenceWait)(const MGPFenceWait* payload, MGPReplySlot* reply);
|
void (*FenceWait)(const MGPFenceWait* payload, MGPReplySlot* reply);
|
||||||
void (*FenceDestroy)(const MGPHandleOnly* payload);
|
void (*FenceDestroy)(const MGPHandleOnly* payload);
|
||||||
void (*FenceWaitServer)(const MGPFenceWait* payload);
|
void (*FenceWaitServer)(const MGPFenceWait* payload);
|
||||||
|
void (*ApplierReset)(const MGPApplierReset* payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
// context: 65 calls. A null entry means the backend does not implement this
|
// context: 66 calls. A null entry means the backend does not implement this
|
||||||
// call and the frontend keeps its own path (plan B section 4.1).
|
// call and the frontend keeps its own path (plan B section 4.1).
|
||||||
struct MGPipeContext {
|
struct MGPipeContext {
|
||||||
void (*QueryCreate)(const MGPQueryDesc* payload);
|
void (*QueryCreate)(const MGPQueryDesc* payload);
|
||||||
@@ -96,11 +97,12 @@ struct MGPipeContext {
|
|||||||
void (*BindStreamOutput)(const MGPStreamOutputBind* payload);
|
void (*BindStreamOutput)(const MGPStreamOutputBind* payload);
|
||||||
void (*SetStorageBlockBinding)(const MGPStorageBlockBinding* payload, const void* blobBytes, Uint64 blobByteCount);
|
void (*SetStorageBlockBinding)(const MGPStorageBlockBinding* payload, const void* blobBytes, Uint64 blobByteCount);
|
||||||
void (*CopyFramebufferToTexture)(const MGPCopyFromFramebuffer* payload);
|
void (*CopyFramebufferToTexture)(const MGPCopyFromFramebuffer* payload);
|
||||||
|
void (*ObjectDeath)(const MGPHandleOnly* payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
inline constexpr SizeT kMGPipeScreenCallCount = 11;
|
inline constexpr SizeT kMGPipeScreenCallCount = 12;
|
||||||
inline constexpr SizeT kMGPipeContextCallCount = 65;
|
inline constexpr SizeT kMGPipeContextCallCount = 66;
|
||||||
inline constexpr SizeT kMGPipeCallCount = 76;
|
inline constexpr SizeT kMGPipeCallCount = 78;
|
||||||
|
|
||||||
// A table that is not exactly its call count of function pointers has grown a
|
// A table that is not exactly its call count of function pointers has grown a
|
||||||
// member that no generator knows about.
|
// member that no generator knows about.
|
||||||
|
|||||||
@@ -320,3 +320,11 @@ inline void MGP_SetStorageBlockBinding(const MGPStorageBlockBinding* payload, co
|
|||||||
inline void MGP_CopyFramebufferToTexture(const MGPCopyFromFramebuffer* payload) {
|
inline void MGP_CopyFramebufferToTexture(const MGPCopyFromFramebuffer* payload) {
|
||||||
gMGPipeContext.CopyFramebufferToTexture(payload);
|
gMGPipeContext.CopyFramebufferToTexture(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void MGP_ApplierReset(const MGPApplierReset* payload) {
|
||||||
|
gMGPipeScreen.ApplierReset(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void MGP_ObjectDeath(const MGPHandleOnly* payload) {
|
||||||
|
gMGPipeContext.ObjectDeath(payload);
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ inline Bool MGPipeVerify(const MGPPatchParameter& a, const MGPPatchParameter& b,
|
|||||||
inline Bool MGPipeVerify(const MGPStreamOutputBind& a, const MGPStreamOutputBind& b, const char** outField);
|
inline Bool MGPipeVerify(const MGPStreamOutputBind& a, const MGPStreamOutputBind& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const MGPStorageBlockBinding& a, const MGPStorageBlockBinding& b, const char** outField);
|
inline Bool MGPipeVerify(const MGPStorageBlockBinding& a, const MGPStorageBlockBinding& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const MGPCopyFromFramebuffer& a, const MGPCopyFromFramebuffer& b, const char** outField);
|
inline Bool MGPipeVerify(const MGPCopyFromFramebuffer& a, const MGPCopyFromFramebuffer& b, const char** outField);
|
||||||
|
inline Bool MGPipeVerify(const MGPApplierReset& a, const MGPApplierReset& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField);
|
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParameters& b, const char** outField);
|
inline Bool MGPipeVerify(const PixelStoreParameters& a, const PixelStoreParameters& b, const char** outField);
|
||||||
inline Bool MGPipeVerify(const SamplerParameters& a, const SamplerParameters& b, const char** outField);
|
inline Bool MGPipeVerify(const SamplerParameters& a, const SamplerParameters& b, const char** outField);
|
||||||
@@ -259,6 +260,8 @@ struct MGPipeHasFieldVerifier<MGPStorageBlockBinding> : std::true_type {};
|
|||||||
template <>
|
template <>
|
||||||
struct MGPipeHasFieldVerifier<MGPCopyFromFramebuffer> : std::true_type {};
|
struct MGPipeHasFieldVerifier<MGPCopyFromFramebuffer> : std::true_type {};
|
||||||
template <>
|
template <>
|
||||||
|
struct MGPipeHasFieldVerifier<MGPApplierReset> : std::true_type {};
|
||||||
|
template <>
|
||||||
struct MGPipeHasFieldVerifier<RenderStateParameters> : std::true_type {};
|
struct MGPipeHasFieldVerifier<RenderStateParameters> : std::true_type {};
|
||||||
template <>
|
template <>
|
||||||
struct MGPipeHasFieldVerifier<PixelStoreParameters> : std::true_type {};
|
struct MGPipeHasFieldVerifier<PixelStoreParameters> : std::true_type {};
|
||||||
@@ -662,6 +665,11 @@ inline Bool MGPipeVerify(const MGPCopyFromFramebuffer& a, const MGPCopyFromFrame
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline Bool MGPipeVerify(const MGPApplierReset& a, const MGPApplierReset& b, const char** outField) {
|
||||||
|
MGP_FIELDS_MGPApplierReset(MGP_VERIFY_FIELD)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField) {
|
inline Bool MGPipeVerify(const RenderStateParameters& a, const RenderStateParameters& b, const char** outField) {
|
||||||
MGP_FIELDS_RenderStateParameters(MGP_VERIFY_FIELD)
|
MGP_FIELDS_RenderStateParameters(MGP_VERIFY_FIELD)
|
||||||
return true;
|
return true;
|
||||||
@@ -709,4 +717,4 @@ inline Bool MGPipeVerify(const MGPVertexBindingPointWire& a, const MGPVertexBind
|
|||||||
|
|
||||||
#undef MGP_VERIFY_FIELD
|
#undef MGP_VERIFY_FIELD
|
||||||
|
|
||||||
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 77;
|
inline constexpr SizeT kMGPipeVerifiedPayloadCount = 78;
|
||||||
|
|||||||
@@ -146,7 +146,9 @@ enum class MGPWireOp : Uint16 {
|
|||||||
BindStreamOutput = 74,
|
BindStreamOutput = 74,
|
||||||
SetStorageBlockBinding = 75,
|
SetStorageBlockBinding = 75,
|
||||||
CopyFramebufferToTexture = 76,
|
CopyFramebufferToTexture = 76,
|
||||||
kOpCount = 77,
|
ApplierReset = 77,
|
||||||
|
ObjectDeath = 78,
|
||||||
|
kOpCount = 79,
|
||||||
};
|
};
|
||||||
|
|
||||||
// THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is
|
// THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is
|
||||||
@@ -244,6 +246,8 @@ inline constexpr Uint32 kMGPipeCallFlags[static_cast<SizeT>(MGPWireOp::kOpCount)
|
|||||||
/* 74 BindStreamOutput */ static_cast<Uint32>(kNone),
|
/* 74 BindStreamOutput */ static_cast<Uint32>(kNone),
|
||||||
/* 75 SetStorageBlockBinding */ static_cast<Uint32>(kHasBlob),
|
/* 75 SetStorageBlockBinding */ static_cast<Uint32>(kHasBlob),
|
||||||
/* 76 CopyFramebufferToTexture*/ static_cast<Uint32>(kNone),
|
/* 76 CopyFramebufferToTexture*/ static_cast<Uint32>(kNone),
|
||||||
|
/* 77 ApplierReset */ static_cast<Uint32>(kNone),
|
||||||
|
/* 78 ObjectDeath */ static_cast<Uint32>(kNone),
|
||||||
};
|
};
|
||||||
static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) ==
|
static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) ==
|
||||||
static_cast<SizeT>(MGPWireOp::kOpCount),
|
static_cast<SizeT>(MGPWireOp::kOpCount),
|
||||||
@@ -905,6 +909,22 @@ static_assert(sizeof(MGPWireRec_CopyFramebufferToTexture) ==
|
|||||||
((sizeof(MGPWireRecHeader) + sizeof(MGPCopyFromFramebuffer) + 7u) & ~SizeT(7u)),
|
((sizeof(MGPWireRecHeader) + sizeof(MGPCopyFromFramebuffer) + 7u) & ~SizeT(7u)),
|
||||||
"MGPWireRec_CopyFramebufferToTexture gained padding; the wire format moved");
|
"MGPWireRec_CopyFramebufferToTexture gained padding; the wire format moved");
|
||||||
|
|
||||||
|
struct alignas(8) MGPWireRec_ApplierReset {
|
||||||
|
MGPWireRecHeader Header;
|
||||||
|
MGPApplierReset Payload;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(MGPWireRec_ApplierReset) ==
|
||||||
|
((sizeof(MGPWireRecHeader) + sizeof(MGPApplierReset) + 7u) & ~SizeT(7u)),
|
||||||
|
"MGPWireRec_ApplierReset gained padding; the wire format moved");
|
||||||
|
|
||||||
|
struct alignas(8) MGPWireRec_ObjectDeath {
|
||||||
|
MGPWireRecHeader Header;
|
||||||
|
MGPHandleOnly Payload;
|
||||||
|
};
|
||||||
|
static_assert(sizeof(MGPWireRec_ObjectDeath) ==
|
||||||
|
((sizeof(MGPWireRecHeader) + sizeof(MGPHandleOnly) + 7u) & ~SizeT(7u)),
|
||||||
|
"MGPWireRec_ObjectDeath gained padding; the wire format moved");
|
||||||
|
|
||||||
[[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) {
|
[[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) {
|
||||||
MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call,
|
MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call,
|
||||||
static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining));
|
static_cast<unsigned long long>(size), static_cast<unsigned long long>(remaining));
|
||||||
@@ -1182,6 +1202,12 @@ inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size,
|
|||||||
case MGPWireOp::CopyFramebufferToTexture:
|
case MGPWireOp::CopyFramebufferToTexture:
|
||||||
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CopyFramebufferToTexture, "CopyFramebufferToTexture");
|
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_CopyFramebufferToTexture, "CopyFramebufferToTexture");
|
||||||
break;
|
break;
|
||||||
|
case MGPWireOp::ApplierReset:
|
||||||
|
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ApplierReset, "ApplierReset");
|
||||||
|
break;
|
||||||
|
case MGPWireOp::ObjectDeath:
|
||||||
|
MGP_WIRE_CHECK_BOUNDS(MGPWireRec_ObjectDeath, "ObjectDeath");
|
||||||
|
break;
|
||||||
case MGPWireOp::kInvalid:
|
case MGPWireOp::kInvalid:
|
||||||
case MGPWireOp::kOpCount:
|
case MGPWireOp::kOpCount:
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -38,6 +38,10 @@
|
|||||||
|
|
||||||
#include <MG_Pipe/MGPipe.h>
|
#include <MG_Pipe/MGPipe.h>
|
||||||
#include <MG_Pipe/PipeRoute.h>
|
#include <MG_Pipe/PipeRoute.h>
|
||||||
|
// P5c (ct): EmitObjectDeathRecord resolves the dying object's handle in the CLIENT's own
|
||||||
|
// allocator - a client surface, asked on the client thread, which is the one place the lookup
|
||||||
|
// is legal under rule E (CONTRACT-P5C.md §5.2).
|
||||||
|
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||||
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
||||||
#include <MG_Util/Debug/Log.h>
|
#include <MG_Util/Debug/Log.h>
|
||||||
|
|
||||||
@@ -503,6 +507,81 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
// =====================================================================================
|
||||||
|
// P5c (ct), CONTRACT-P5C.md §5: the two control records' producers
|
||||||
|
// =====================================================================================
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// The ContextSerial of the NEXT applier_reset record. NOWHERE IN THE FRONTEND NUMBERS
|
||||||
|
// A MAKE-CURRENT: the tracker knows the EDGE (FreshlyPrimed) and no session or context
|
||||||
|
// carries a serial for it (checked at the contract commit), so the serial is this
|
||||||
|
// client's own count of applier_reset emissions - 0 for the first make-current, one
|
||||||
|
// more per primed edge. That is the only value the server can hold the client to in a
|
||||||
|
// one-context session: the sink keeps the same count and ASSERTS equality
|
||||||
|
// (CONTRACT-P5C.md §1: the value is asserted, never dispatched on; P6's multi-context
|
||||||
|
// shape is what will read it for real). GL-thread only, like every emitter here.
|
||||||
|
Uint64 g_applierResetContextSerial = 0;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Bool EmitApplierResetRecord() {
|
||||||
|
// NO SESSION, NO RECORD - and false rather than the RequireSession Fatal, because a
|
||||||
|
// validate can legitimately prime with a transport CONFIGURED but no live session:
|
||||||
|
// the bring-up window before ClientSession::Start() (§6 layer 2's documented
|
||||||
|
// exception), and the server-role-only shape a ServerLoop fixture drives with
|
||||||
|
// Transport=InProcess and no client at all. In both this process has no wire for the
|
||||||
|
// reset to cross, and the caller answers with the direct call the record replaced.
|
||||||
|
ClientSession* session = ClientSession::Active();
|
||||||
|
if (session == nullptr || !session->Started()) return false;
|
||||||
|
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) return false;
|
||||||
|
MG_Pipe::MGPApplierReset record{};
|
||||||
|
record.ContextSerial = g_applierResetContextSerial++;
|
||||||
|
session->EmitAndWait(MGPWireOp::ApplierReset, &record, sizeof(record), nullptr, 0, nullptr,
|
||||||
|
0, nullptr);
|
||||||
|
++g_emitted;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectDeathEmit EmitObjectDeathRecord(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
|
||||||
|
using MG_Pipe::MGPipeHandle;
|
||||||
|
|
||||||
|
// NO SESSION, NO RECORD - and that is NOT the RequireSession shape on purpose. A death
|
||||||
|
// notice can outlive the session by construction: frontend objects die at context
|
||||||
|
// teardown and at process exit, after Stop has joined the apply thread and freed the
|
||||||
|
// rings, and the twins this record would kill died with the server. Emitting into that
|
||||||
|
// window is a use-after-free; Fatal-ing on it is an abort at exit() for a legal death.
|
||||||
|
// NoSession is its own answer (not folded into NoHandle) because the caller's fallback
|
||||||
|
// for "no wire exists" is delivery to the server loop this process still has, while
|
||||||
|
// "no handle" means there is nothing to deliver at all.
|
||||||
|
ClientSession* session = ClientSession::Active();
|
||||||
|
if (session == nullptr || !session->Started()) return ObjectDeathEmit::NoSession;
|
||||||
|
// The same window, one step earlier: Stop has raised the teardown refusal but not yet
|
||||||
|
// joined the apply thread. A death landing here is destructor-driven, not an app bug,
|
||||||
|
// and the twin it would kill dies with the backend Stop is destroying - so it is
|
||||||
|
// skipped, where a routed call in the same window is Fatal{ClientTablesUninstalled}.
|
||||||
|
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) {
|
||||||
|
return ObjectDeathEmit::NoSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE CLIENT'S OWN ALLOCATOR, ON THE CLIENT'S OWN THREAD (§5.2 step 1). A lifetime id
|
||||||
|
// the allocator cannot resolve names an object that never crossed - no create record
|
||||||
|
// ever carried its handle, so the server has no twin to kill and NOTHING is emitted.
|
||||||
|
// This replaces the mailbox's unconditional delivery, which hopped every death to the
|
||||||
|
// apply thread whether or not the server had ever seen the object.
|
||||||
|
const MGPipeHandle handle = MG_Pipe::MGPipeSlots().FindByLifetimeId(kind, lifetimeId);
|
||||||
|
if (MG_Pipe::MGPipeHandleIsNull(handle)) return ObjectDeathEmit::NoHandle;
|
||||||
|
|
||||||
|
MG_Pipe::MGPHandleOnly record{};
|
||||||
|
record.Handle = handle;
|
||||||
|
record.Kind = static_cast<Uint32>(kind);
|
||||||
|
// The WAIT is the only property the blocking mailbox hop provided and the only one P5c
|
||||||
|
// keeps (§5.2 step 3): it orders the death against in-flight verbs that name the
|
||||||
|
// handle, so the server's twin cannot be released underneath a verb that is using it.
|
||||||
|
session->EmitAndWait(MGPWireOp::ObjectDeath, &record, sizeof(record), nullptr, 0, nullptr,
|
||||||
|
0, nullptr);
|
||||||
|
++g_emitted;
|
||||||
|
return ObjectDeathEmit::Emitted;
|
||||||
|
}
|
||||||
|
|
||||||
void RequireClientTablesInstalled(const char* row) {
|
void RequireClientTablesInstalled(const char* row) {
|
||||||
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) {
|
if (g_clientTablesUninstalled.load(std::memory_order_acquire)) {
|
||||||
MGLOG_F("MGPipe: Fatal{ClientTablesUninstalled, \"%s\"} - the client tables are "
|
MGLOG_F("MGPipe: Fatal{ClientTablesUninstalled, \"%s\"} - the client tables are "
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Includes.h>
|
#include <Includes.h>
|
||||||
|
|
||||||
|
#include <MG_Pipe/MGPipeHandles.h>
|
||||||
|
|
||||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
|
||||||
namespace MobileGL::MG_Remote::Client {
|
namespace MobileGL::MG_Remote::Client {
|
||||||
@@ -86,6 +88,33 @@ namespace MobileGL::MG_Remote::Client {
|
|||||||
// server otherwise). Declared here so PipeFill does not have to include a server header.
|
// server otherwise). Declared here so PipeFill does not have to include a server header.
|
||||||
Bool RunsAsTheServerRole();
|
Bool RunsAsTheServerRole();
|
||||||
|
|
||||||
|
// ---- P5c (ct), CONTRACT-P5C.md §5: the two control records' client halves --------------
|
||||||
|
//
|
||||||
|
// NOT ROUTED ROWS - the two are appended to PipeCalls.def with no MGPipeApply* entry point
|
||||||
|
// and no table slot at all, so their producers call these directly. PipeFill.cpp's
|
||||||
|
// FreshlyPrimed arm calls the first; DirectGLES' OnFrontendStateObjectDestroyed calls the
|
||||||
|
// second.
|
||||||
|
|
||||||
|
// applier_reset (§5.1). Emitted on the GL thread at the tracker.FreshlyPrimed() edge,
|
||||||
|
// BEFORE the block's client-side resets, so the record's barrier orders the server's
|
||||||
|
// MGPipeApplierReset() against every verb that follows. ContextSerial is the client's own
|
||||||
|
// count of applier_reset emissions (0 = the first make-current): nothing in the frontend
|
||||||
|
// numbers a make-current today, and the sink asserts the value against its own count of
|
||||||
|
// the same records rather than dispatching on it (P5c: one context per session; P6 reads
|
||||||
|
// it for real). Returns false - nothing emitted - when no live session could carry the
|
||||||
|
// record (the bring-up window before Start, a server-role-only fixture, teardown); the
|
||||||
|
// caller then makes the direct call the record replaced.
|
||||||
|
Bool EmitApplierResetRecord();
|
||||||
|
|
||||||
|
// The three answers a death notice can produce (§5.2): the record crossed; the client's
|
||||||
|
// own allocator cannot resolve the dying object (the server never saw it, so there is no
|
||||||
|
// twin to kill and NOTHING crossed); or no live session could carry the record. The
|
||||||
|
// third is NOT folded into the second: a server role with no wire at all - a ServerLoop
|
||||||
|
// fixture's shape, never a real split's - still has a server loop the caller delivers to,
|
||||||
|
// where a handle that never existed has nothing to deliver.
|
||||||
|
enum class ObjectDeathEmit : Uint8 { Emitted, NoHandle, NoSession };
|
||||||
|
ObjectDeathEmit EmitObjectDeathRecord(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId);
|
||||||
|
|
||||||
} // namespace MobileGL::MG_Remote::Client
|
} // namespace MobileGL::MG_Remote::Client
|
||||||
|
|
||||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
|
|
||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||||
|
// P5c ct: object_death's per-kind release names the Espryt twin tables (CONTRACT-P5C.md
|
||||||
|
// §5.2). The same dependency ServerLoop.cpp already takes for CreateBackend; a server built
|
||||||
|
// on Magma simply holds no twins in these tables and every release resolves to nothing.
|
||||||
|
#include <MG_Backend/DirectGLES/Managers.h>
|
||||||
#include <MG_Remote/Client/ClientSession.h>
|
#include <MG_Remote/Client/ClientSession.h>
|
||||||
#include <MG_Pipe/PipeApply.h>
|
#include <MG_Pipe/PipeApply.h>
|
||||||
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
|
||||||
@@ -942,6 +946,67 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- P5c ct (MG_Remote/CONTRACT-P5C.md §5) ------------------------------------------
|
||||||
|
//
|
||||||
|
// TWO CONTROL RECORDS, NO BACKEND TABLE AND NO DECLINE ARM. Neither body consults
|
||||||
|
// Table(): the reset belongs to the applier this process owns, and the death release
|
||||||
|
// belongs to the twin tables - a backend that registered no slots (Magma's XFB shape)
|
||||||
|
// still has an applier to reset and still answers a death with the same generation
|
||||||
|
// check. A record that cannot be proved is Fatal, not declined: both refusals are
|
||||||
|
// ProtocolCorruption because by the time the sink runs the codec has already proved the
|
||||||
|
// record's SHAPE, and what is left to check are the contract facts about the peer (§1).
|
||||||
|
|
||||||
|
Bool ServerVerbSink::OnApplierReset(const MG_Pipe::MGPApplierReset& reset) {
|
||||||
|
// §1: the serial is ASSERTED, never dispatched on. P5c has exactly one context per
|
||||||
|
// session, so the only legal sequence is 0, 1, 2, ... and the session's own count of
|
||||||
|
// accepted resets IS the expected value; anything else means the two ends disagree
|
||||||
|
// about how many make-current edges have crossed, which no backend answer can fix.
|
||||||
|
if (reset.ContextSerial != m_applierResetSerial) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ApplierReset.ContextSerial\"} - the "
|
||||||
|
"record carries %llu and this session has accepted %llu reset(s); the "
|
||||||
|
"serial is asserted against the session's own count, not dispatched on "
|
||||||
|
"(one context per session in P5c)",
|
||||||
|
static_cast<unsigned long long>(reset.ContextSerial),
|
||||||
|
static_cast<unsigned long long>(m_applierResetSerial));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
++m_applierResetSerial;
|
||||||
|
// THE WHOLE POINT OF THE RECORD: the reset runs HERE, on the apply thread, against
|
||||||
|
// the g_applier this role owns (PipeApply.cpp:409). The layer-2 guard inside
|
||||||
|
// MGPipeApplierReset passes because this IS the apply thread; the GL-thread direct
|
||||||
|
// call it replaced is the Fatal arm.
|
||||||
|
MG_Pipe::MGPipeApplierReset();
|
||||||
|
++m_applierResets;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ServerVerbSink::OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) {
|
||||||
|
// §1's zero ruling: a null handle means "the object never crossed", and the client
|
||||||
|
// emits NOTHING in that case (§5.2) - so a null handle arriving here is corruption,
|
||||||
|
// not a no-op.
|
||||||
|
if (MG_Pipe::MGPipeHandleIsNull(death.Handle)) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Handle\"} - a null "
|
||||||
|
"handle never crosses: the client emits nothing for an object its own "
|
||||||
|
"allocator cannot resolve (CONTRACT-P5C.md §5.2)");
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
if (death.Kind >= static_cast<Uint32>(MG_Pipe::MGPipeKind::KindCount)) {
|
||||||
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ObjectDeath.Kind\"} - %u is not an "
|
||||||
|
"MGPipeKind",
|
||||||
|
static_cast<unsigned>(death.Kind));
|
||||||
|
std::abort();
|
||||||
|
}
|
||||||
|
// The per-kind release, keyed by the handle the record carried. A false answer is
|
||||||
|
// NOT a decline: the kind's own delete opcode may already have released the twin
|
||||||
|
// (the idempotent second path every notice arm documents), and a kind this backend
|
||||||
|
// does not twin (Buffer, whose death crosses as resource_destroy) legally resolves
|
||||||
|
// to nothing.
|
||||||
|
MG_Backend::DirectGLES::ReleaseTwinsForWireObjectDeath(
|
||||||
|
death.Handle, static_cast<MG_Pipe::MGPipeKind>(death.Kind));
|
||||||
|
++m_objectDeaths;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------------
|
||||||
// PipeApplier
|
// PipeApplier
|
||||||
// -----------------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -150,6 +150,32 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
Bool OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) override;
|
Bool OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) override;
|
||||||
Bool OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) override;
|
Bool OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) override;
|
||||||
|
|
||||||
|
// ---- P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records ----------------
|
||||||
|
//
|
||||||
|
// REAL BODIES FROM THE DAY THE ROWS EXIST, not the P5b stub shape: the rows were
|
||||||
|
// appended by the same package that lands these bodies, so there is no window in
|
||||||
|
// which a client can emit ahead of its server half.
|
||||||
|
//
|
||||||
|
// OnApplierReset (§5.1): the make-current edge's server half. ContextSerial is
|
||||||
|
// ASSERTED against this session's own count of applier_reset records - one context
|
||||||
|
// per session in P5c, so the legal sequence is 0, 1, 2, ... - and only then is
|
||||||
|
// MGPipeApplierReset() run, here, on the apply thread that owns g_applier.
|
||||||
|
// OnObjectDeath (§5.2): a null handle never crosses (the client emits nothing for an
|
||||||
|
// object its allocator cannot resolve), so one arriving is
|
||||||
|
// Fatal{ProtocolCorruption, "ObjectDeath.Handle"}; otherwise the per-kind release
|
||||||
|
// runs by the record's handle (SlotTables.h's ReleaseTwinByHandle).
|
||||||
|
Bool OnApplierReset(const MG_Pipe::MGPApplierReset& reset) override;
|
||||||
|
Bool OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) override;
|
||||||
|
|
||||||
|
// P5c ct's tallies, for the same reason every other row's tally exists (R-16: a probe
|
||||||
|
// may not arm against a stub). ApplierResets counts the records ACCEPTED (serial
|
||||||
|
// checked, reset run); ObjectDeaths counts every record the sink dispatched.
|
||||||
|
Uint64 ApplierResets() const { return m_applierResets; }
|
||||||
|
Uint64 ObjectDeaths() const { return m_objectDeaths; }
|
||||||
|
// The ContextSerial the NEXT applier_reset record must carry. Exposed so a case can
|
||||||
|
// assert the sequence rather than only the count.
|
||||||
|
Uint64 ExpectedApplierResetSerial() const { return m_applierResetSerial; }
|
||||||
|
|
||||||
// Per-verb tallies. The lane asserts these moved, because "the scenario passed" on a
|
// Per-verb tallies. The lane asserts these moved, because "the scenario passed" on a
|
||||||
// split build is also what a scenario that ran entirely on the monolith path looks
|
// split build is also what a scenario that ran entirely on the monolith path looks
|
||||||
// like (R-16: a probe may not arm against a stub).
|
// like (R-16: a probe may not arm against a stub).
|
||||||
@@ -231,6 +257,14 @@ namespace MobileGL::MG_Remote::Server {
|
|||||||
Uint64 m_streamOutputControls = 0;
|
Uint64 m_streamOutputControls = 0;
|
||||||
Uint64 m_streamOutputBinds = 0;
|
Uint64 m_streamOutputBinds = 0;
|
||||||
Uint64 m_patchParameters = 0;
|
Uint64 m_patchParameters = 0;
|
||||||
|
// P5c ct. m_applierResetSerial is BOTH the expected ContextSerial of the next record
|
||||||
|
// and the count of accepted resets: the two are one number because the serial is the
|
||||||
|
// session's own count of applier_reset records (CONTRACT-P5C.md §1: asserted, never
|
||||||
|
// dispatched on). m_objectDeaths counts records dispatched, released twin or not -
|
||||||
|
// the idempotent second path answering "nothing held it" is a legal record.
|
||||||
|
Uint64 m_applierResetSerial = 0;
|
||||||
|
Uint64 m_applierResets = 0;
|
||||||
|
Uint64 m_objectDeaths = 0;
|
||||||
// ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable::
|
// ReadPixels' destination. The pixels go into the reply slot, but GLFunctionsTable::
|
||||||
// ReadPixels writes into a caller buffer, so one staging vector per session sits
|
// ReadPixels writes into a caller buffer, so one staging vector per session sits
|
||||||
// between them. Grown, never shrunk, and never handed out past the call.
|
// between them. Grown, never shrunk, and never handed out past the call.
|
||||||
|
|||||||
@@ -239,7 +239,9 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
X(PatchParameter, MGPPatchParameter) \
|
X(PatchParameter, MGPPatchParameter) \
|
||||||
X(BindStreamOutput, MGPStreamOutputBind) \
|
X(BindStreamOutput, MGPStreamOutputBind) \
|
||||||
X(SetStorageBlockBinding, MGPStorageBlockBinding) \
|
X(SetStorageBlockBinding, MGPStorageBlockBinding) \
|
||||||
X(CopyFramebufferToTexture, MGPCopyFromFramebuffer)
|
X(CopyFramebufferToTexture, MGPCopyFromFramebuffer) \
|
||||||
|
X(ApplierReset, MGPApplierReset) \
|
||||||
|
X(ObjectDeath, MGPHandleOnly)
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -2085,6 +2087,20 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
m_verbs->OnCopyFramebufferToTexture(
|
m_verbs->OnCopyFramebufferToTexture(
|
||||||
*static_cast<const MGPCopyFromFramebuffer*>(payload));
|
*static_cast<const MGPCopyFromFramebuffer*>(payload));
|
||||||
|
|
||||||
|
// ---- P5c's two control records, opcodes 77..78 (MG_Remote/CONTRACT-P5C.md §5) --------
|
||||||
|
//
|
||||||
|
// Fixed-size PODs the bounds gate has already proved; neither carries a blob, a tail
|
||||||
|
// or a reply, so each arm hands over and stops. ObjectDeath's null-handle refusal is
|
||||||
|
// the sink's, not the codec's: the codec proves SHAPES, and "the client emits nothing
|
||||||
|
// for an object that never crossed" (§5.2) is a contract fact about the peer.
|
||||||
|
case MGPWireOp::ApplierReset:
|
||||||
|
return m_verbs != nullptr &&
|
||||||
|
m_verbs->OnApplierReset(*static_cast<const MGPApplierReset*>(payload));
|
||||||
|
|
||||||
|
case MGPWireOp::ObjectDeath:
|
||||||
|
return m_verbs != nullptr &&
|
||||||
|
m_verbs->OnObjectDeath(*static_cast<const MGPHandleOnly*>(payload));
|
||||||
|
|
||||||
case MGPWireOp::SetSwapInterval:
|
case MGPWireOp::SetSwapInterval:
|
||||||
// Class C, wave 3 (census-classC.md "static cross"); not a verb (FillPoints.def:21).
|
// Class C, wave 3 (census-classC.md "static cross"); not a verb (FillPoints.def:21).
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -534,6 +534,25 @@ namespace MobileGL::MG_Remote::Wire {
|
|||||||
(void)copy;
|
(void)copy;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- P5c (MG_Remote/CONTRACT-P5C.md §5): the two control records, opcodes 77..78.
|
||||||
|
//
|
||||||
|
// Same hand-over shape as the P5b rows: the codec validates the record (a fixed-size
|
||||||
|
// POD, no blob, no tail) and the SERVER'S sink does the work - OnApplierReset runs the
|
||||||
|
// server's own MGPipeApplierReset() after asserting ContextSerial against the
|
||||||
|
// session's (§5.1: ASSERTED, never dispatched on, P5c has one context per session),
|
||||||
|
// and OnObjectDeath releases the kind's twin table by the handle the record carried
|
||||||
|
// (§5.2). The default bodies return false ("this build does not implement it"), so a
|
||||||
|
// unit decoder without a server declines by the same answer every other unimplemented
|
||||||
|
// row gives.
|
||||||
|
virtual Bool OnApplierReset(const MG_Pipe::MGPApplierReset& reset) {
|
||||||
|
(void)reset;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
virtual Bool OnObjectDeath(const MG_Pipe::MGPHandleOnly& death) {
|
||||||
|
(void)death;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Not thread safe: one decoder on the apply thread, by construction.
|
// Not thread safe: one decoder on the apply thread, by construction.
|
||||||
|
|||||||
@@ -125,11 +125,13 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
|
|||||||
EXPECT_EQ(kMGPipeContextCallCount, kMGPipeCallCount - ClassCount<kScreen>());
|
EXPECT_EQ(kMGPipeContextCallCount, kMGPipeCallCount - ClassCount<kScreen>());
|
||||||
|
|
||||||
// The per-class counts PipeCalls.def documents in its header.
|
// The per-class counts PipeCalls.def documents in its header.
|
||||||
EXPECT_EQ(ClassCount<kScreen>(), 11u);
|
// kScreen is 11 + P5c's applier_reset (MG_Remote/CONTRACT-P5C.md §5.1); kCtxObject is
|
||||||
|
// 9 + P5c's object_death (§5.2), the framebuffer family's first wire delete opcode.
|
||||||
|
EXPECT_EQ(ClassCount<kScreen>(), 12u);
|
||||||
EXPECT_EQ(ClassCount<kCtxQuery>(), 8u);
|
EXPECT_EQ(ClassCount<kCtxQuery>(), 8u);
|
||||||
EXPECT_EQ(ClassCount<kCtxCso>(), 13u);
|
EXPECT_EQ(ClassCount<kCtxCso>(), 13u);
|
||||||
EXPECT_EQ(ClassCount<kCtxState>(), 17u);
|
EXPECT_EQ(ClassCount<kCtxState>(), 17u);
|
||||||
EXPECT_EQ(ClassCount<kCtxObject>(), 9u);
|
EXPECT_EQ(ClassCount<kCtxObject>(), 10u);
|
||||||
// 13 + the five P5b-appended verbs (MG_Remote/CONTRACT-P5B.md): bind_shader_image,
|
// 13 + the five P5b-appended verbs (MG_Remote/CONTRACT-P5B.md): bind_shader_image,
|
||||||
// patch_parameter, bind_stream_output, set_storage_block_binding,
|
// patch_parameter, bind_stream_output, set_storage_block_binding,
|
||||||
// copy_framebuffer_to_texture.
|
// copy_framebuffer_to_texture.
|
||||||
@@ -139,8 +141,9 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
|
|||||||
// A row nobody has migrated is null - which is exactly what "this subsystem has not been
|
// A row nobody has migrated is null - which is exactly what "this subsystem has not been
|
||||||
// migrated, keep pulling" means (plan B section 4.1).
|
// migrated, keep pulling" means (plan B section 4.1).
|
||||||
//
|
//
|
||||||
// UNTIL P5 R-17 THAT WAS EVERY ROW, and this case said so. It is now EXACTLY THE 34 ROWS WITH
|
// UNTIL P5 R-17 THAT WAS EVERY ROW, and this case said so. It is now EXACTLY THE 41 ROWS WITH
|
||||||
// NO MGPipeApply* ENTRY POINT: the other 37 have an applier, R-17 installs adapters over them,
|
// NO MGPipeApply* ENTRY POINT (78 - the 37 that have one; the number was 34 at P5, 39 after
|
||||||
|
// P5b's five sink-only verbs): the other 37 have an applier, R-17 installs adapters over them,
|
||||||
// and a null there would no longer mean "keep pulling" - `MG_Impl/Pipe`'s call sites go through
|
// and a null there would no longer mean "keep pulling" - `MG_Impl/Pipe`'s call sites go through
|
||||||
// the thunks, so a null would mean "call through a null pointer". The number is asserted rather
|
// the thunks, so a null would mean "call through a null pointer". The number is asserted rather
|
||||||
// than the emptiness, because "37 installed" and "34 still null" are the two halves of a
|
// than the emptiness, because "37 installed" and "34 still null" are the two halves of a
|
||||||
@@ -156,10 +159,12 @@ TEST(PipeCatalogue, UninstalledTablesAreAllNull) {
|
|||||||
const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext);
|
const void* const* context = reinterpret_cast<const void* const*>(&gMGPipeContext);
|
||||||
#if MOBILEGL_PIPE_PUSH
|
#if MOBILEGL_PIPE_PUSH
|
||||||
MGPipeInstallMonolithTables();
|
MGPipeInstallMonolithTables();
|
||||||
// The 34 rows with no MGPipeApply* entry point are still null, and null still means "this
|
// The 41 rows with no MGPipeApply* entry point are still null, and null still means "this
|
||||||
// subsystem has not been migrated, keep pulling". Named rather than counted, because the
|
// subsystem has not been migrated, keep pulling". Named rather than counted, because the
|
||||||
// count is the other case's job and two cases asserting the same number would both go red
|
// count is the other case's job and two cases asserting the same number would both go red
|
||||||
// for one change.
|
// for one change. P5c's two control records are among them by design (CONTRACT-P5C.md §5:
|
||||||
|
// no MGPipeApply* entry point, no monolith producer - under a transport they reach
|
||||||
|
// WireVerbSink instead).
|
||||||
EXPECT_EQ(gMGPipeContext.SetShaderBuffers, nullptr);
|
EXPECT_EQ(gMGPipeContext.SetShaderBuffers, nullptr);
|
||||||
EXPECT_EQ(gMGPipeContext.SetStreamOutputTargets, nullptr);
|
EXPECT_EQ(gMGPipeContext.SetStreamOutputTargets, nullptr);
|
||||||
EXPECT_EQ(gMGPipeContext.DrawVbo, nullptr);
|
EXPECT_EQ(gMGPipeContext.DrawVbo, nullptr);
|
||||||
@@ -168,6 +173,8 @@ TEST(PipeCatalogue, UninstalledTablesAreAllNull) {
|
|||||||
EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr);
|
EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr);
|
||||||
EXPECT_EQ(gMGPipeContext.QueryCreate, nullptr);
|
EXPECT_EQ(gMGPipeContext.QueryCreate, nullptr);
|
||||||
EXPECT_EQ(gMGPipeScreen.FenceCreate, nullptr);
|
EXPECT_EQ(gMGPipeScreen.FenceCreate, nullptr);
|
||||||
|
EXPECT_EQ(gMGPipeScreen.ApplierReset, nullptr);
|
||||||
|
EXPECT_EQ(gMGPipeContext.ObjectDeath, nullptr);
|
||||||
#else
|
#else
|
||||||
// A pull build compiles no applier and no routing, so the pre-migration statement is the
|
// A pull build compiles no applier and no routing, so the pre-migration statement is the
|
||||||
// whole truth there and this case is the one that says so.
|
// whole truth there and this case is the one that says so.
|
||||||
@@ -235,6 +242,12 @@ TEST(PipeCatalogue, ExactlyTheRoutedRowsAreInstalledAndTheRestAreStillNull) {
|
|||||||
EXPECT_EQ(gMGPipeContext.Present, nullptr);
|
EXPECT_EQ(gMGPipeContext.Present, nullptr);
|
||||||
EXPECT_EQ(gMGPipeContext.SetSwapInterval, nullptr);
|
EXPECT_EQ(gMGPipeContext.SetSwapInterval, nullptr);
|
||||||
EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr);
|
EXPECT_EQ(gMGPipeScreen.GetCaps, nullptr);
|
||||||
|
// P5c's two control records (CONTRACT-P5C.md §5) take the same answer for a different
|
||||||
|
// reason: no MGPipeApply* exists for either and none may be installed - under a transport
|
||||||
|
// they cross to WireVerbSink, under monolith the GL thread's direct call and the death
|
||||||
|
// notice's mailbox are the producers, byte for byte as before (G1/G2).
|
||||||
|
EXPECT_EQ(gMGPipeScreen.ApplierReset, nullptr);
|
||||||
|
EXPECT_EQ(gMGPipeContext.ObjectDeath, nullptr);
|
||||||
#else
|
#else
|
||||||
// A pull build compiles no applier and no routing, so the pre-migration statement is still
|
// A pull build compiles no applier and no routing, so the pre-migration statement is still
|
||||||
// the whole truth there.
|
// the whole truth there.
|
||||||
@@ -557,7 +570,15 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
|
|||||||
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::BindStreamOutput), 74);
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::BindStreamOutput), 74);
|
||||||
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::SetStorageBlockBinding), 75);
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::SetStorageBlockBinding), 75);
|
||||||
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::CopyFramebufferToTexture), 76);
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::CopyFramebufferToTexture), 76);
|
||||||
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 77);
|
// P5c (MG_Remote/CONTRACT-P5C.md §5) appended the two control records AFTER P5b's five, by
|
||||||
|
// the same rule: opcodes 77..78, and nothing before them moved. applier_reset is a kScreen
|
||||||
|
// row (a make-current is a whole-server edge, exactly as FenceWaitServer is a screen call)
|
||||||
|
// and object_death a kCtxObject one; both carry no blob, no reply and no tail.
|
||||||
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ApplierReset), 77);
|
||||||
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::ObjectDeath), 78);
|
||||||
|
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 79);
|
||||||
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ApplierReset), static_cast<Uint32>(kNone));
|
||||||
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ObjectDeath), static_cast<Uint32>(kNone));
|
||||||
// And the P5b rows carry what their contract says: one blob (the block name) and nothing
|
// And the P5b rows carry what their contract says: one blob (the block name) and nothing
|
||||||
// else, and the extended draw row keeps its two flags.
|
// else, and the extended draw row keeps its two flags.
|
||||||
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetStorageBlockBinding), static_cast<Uint32>(kHasBlob));
|
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SetStorageBlockBinding), static_cast<Uint32>(kHasBlob));
|
||||||
@@ -575,6 +596,11 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
|
|||||||
EXPECT_EQ(sizeof(MGPCopyFromFramebuffer), 48u);
|
EXPECT_EQ(sizeof(MGPCopyFromFramebuffer), 48u);
|
||||||
EXPECT_EQ(sizeof(MGPCopyRegion), 72u);
|
EXPECT_EQ(sizeof(MGPCopyRegion), 72u);
|
||||||
EXPECT_EQ(sizeof(MGPDrawIndirect), 40u);
|
EXPECT_EQ(sizeof(MGPDrawIndirect), 40u);
|
||||||
|
// The P5c payload, pinned like every other MGP_ASSERT_POD at runtime: one Uint64, no
|
||||||
|
// padding. object_death REUSES MGPHandleOnly (CONTRACT-P5C.md §1), so there is no second
|
||||||
|
// struct to pin - the 16 bytes are pinned above with the handle family.
|
||||||
|
EXPECT_EQ(sizeof(MGPApplierReset), 8u);
|
||||||
|
EXPECT_EQ(sizeof(MGPHandleOnly), 16u);
|
||||||
// The two draw-flag bits P5b's d1 arms are exclusive by contract and distinct by value.
|
// The two draw-flag bits P5b's d1 arms are exclusive by contract and distinct by value.
|
||||||
EXPECT_EQ(static_cast<Uint32>(kDrawIsIndirect), 1u << 5);
|
EXPECT_EQ(static_cast<Uint32>(kDrawIsIndirect), 1u << 5);
|
||||||
EXPECT_EQ(static_cast<Uint32>(kDrawHasUserIndices) & static_cast<Uint32>(kDrawIsIndirect), 0u);
|
EXPECT_EQ(static_cast<Uint32>(kDrawHasUserIndices) & static_cast<Uint32>(kDrawIsIndirect), 0u);
|
||||||
@@ -771,8 +797,10 @@ TEST(PipeCatalogue, FloatVectorsCompareBitwise) {
|
|||||||
TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
|
TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
|
||||||
// 72 through P5; P5b appended five call payloads (MG_Remote/CONTRACT-P5B.md: MGPImageBind,
|
// 72 through P5; P5b appended five call payloads (MG_Remote/CONTRACT-P5B.md: MGPImageBind,
|
||||||
// MGPPatchParameter, MGPStreamOutputBind, MGPStorageBlockBinding, MGPCopyFromFramebuffer),
|
// MGPPatchParameter, MGPStreamOutputBind, MGPStorageBlockBinding, MGPCopyFromFramebuffer),
|
||||||
// each with its own field list, so the comparator sees every one of them: 77.
|
// each with its own field list, so the comparator sees every one of them: 77. P5c appended
|
||||||
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 77u);
|
// applier_reset's MGPApplierReset (CONTRACT-P5C.md §5.1) - object_death reuses
|
||||||
|
// MGPHandleOnly, which has had a list since P0 - so: 78.
|
||||||
|
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 78u);
|
||||||
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
|
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
|
||||||
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
|
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
|
||||||
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
|
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
|
||||||
|
|||||||
@@ -38,6 +38,10 @@
|
|||||||
#include <Config.h>
|
#include <Config.h>
|
||||||
#include <MG_Remote/Server/PipeApplier.h>
|
#include <MG_Remote/Server/PipeApplier.h>
|
||||||
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
|
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
|
||||||
|
// P5c ct: object_death's round trip releases REAL Espryt twin-table entries, so the suite
|
||||||
|
// drives the same registries the sink dispatches to (Managers.h). A suite that substituted a
|
||||||
|
// mock here would pin the dispatch and nothing about the release (R-16).
|
||||||
|
#include <MG_Backend/DirectGLES/Managers.h>
|
||||||
#include <MG_Pipe/MGPipe.h>
|
#include <MG_Pipe/MGPipe.h>
|
||||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||||
#include <MG_Pipe/PipeApply.h>
|
#include <MG_Pipe/PipeApply.h>
|
||||||
@@ -268,6 +272,16 @@ namespace {
|
|||||||
FramebufferCopies.push_back(copy);
|
FramebufferCopies.push_back(copy);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// ---- P5c's rows (CONTRACT-P5C.md §5): the two control records, recorded the same
|
||||||
|
// way so a round trip asserts the arm and the record intact.
|
||||||
|
Bool OnApplierReset(const MGPApplierReset& reset) override {
|
||||||
|
ApplierResets.push_back(reset);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Bool OnObjectDeath(const MGPHandleOnly& death) override {
|
||||||
|
ObjectDeaths.push_back(death);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
std::vector<MGPClear> Clears;
|
std::vector<MGPClear> Clears;
|
||||||
std::vector<MGPBlit> Blits;
|
std::vector<MGPBlit> Blits;
|
||||||
std::vector<MGPPresent> Presents;
|
std::vector<MGPPresent> Presents;
|
||||||
@@ -293,6 +307,8 @@ namespace {
|
|||||||
std::vector<MGPPatchParameter> Patches;
|
std::vector<MGPPatchParameter> Patches;
|
||||||
std::vector<MGPMipPlan> MipPlans;
|
std::vector<MGPMipPlan> MipPlans;
|
||||||
std::vector<MGPCopyFromFramebuffer> FramebufferCopies;
|
std::vector<MGPCopyFromFramebuffer> FramebufferCopies;
|
||||||
|
std::vector<MGPApplierReset> ApplierResets;
|
||||||
|
std::vector<MGPHandleOnly> ObjectDeaths;
|
||||||
};
|
};
|
||||||
|
|
||||||
Replies& Answers() { return m_replies; }
|
Replies& Answers() { return m_replies; }
|
||||||
@@ -1243,6 +1259,73 @@ TEST_F(PipeWireCodecTest, BindStreamOutputReachesTheSink) {
|
|||||||
EXPECT_EQ(wire.Sink().StreamOutputBinds[0].LifetimeId, 0x1234567890ull);
|
EXPECT_EQ(wire.Sink().StreamOutputBinds[0].LifetimeId, 0x1234567890ull);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================================================
|
||||||
|
// P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records round-trip
|
||||||
|
// =====================================================================================
|
||||||
|
//
|
||||||
|
// Same shape as the P5b rows above: encode, pump, and assert the record reached the RIGHT
|
||||||
|
// sink method intact. What a serial ASSERT or a twin release does with the record is the
|
||||||
|
// SERVER sink's layer (SessionTest drives that); here the codec proves the bytes and the
|
||||||
|
// dispatch.
|
||||||
|
|
||||||
|
TEST_F(PipeWireCodecTest, ApplierResetReachesTheSinkWithItsSerial) {
|
||||||
|
Wire2 wire;
|
||||||
|
MGPApplierReset reset{};
|
||||||
|
reset.ContextSerial = 0; // 0 = the first make-current (CONTRACT-P5C.md §1)
|
||||||
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ApplierReset, &reset, sizeof(reset)),
|
||||||
|
kInvalidSeq);
|
||||||
|
bool applied = false;
|
||||||
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||||
|
EXPECT_TRUE(applied);
|
||||||
|
ASSERT_EQ(wire.Sink().ApplierResets.size(), 1u);
|
||||||
|
EXPECT_EQ(wire.Sink().ApplierResets[0].ContextSerial, 0u);
|
||||||
|
|
||||||
|
// A second edge carries the next serial, and the two records arrive in order.
|
||||||
|
reset.ContextSerial = 1;
|
||||||
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ApplierReset, &reset, sizeof(reset)),
|
||||||
|
kInvalidSeq);
|
||||||
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||||
|
EXPECT_TRUE(applied);
|
||||||
|
ASSERT_EQ(wire.Sink().ApplierResets.size(), 2u);
|
||||||
|
EXPECT_EQ(wire.Sink().ApplierResets[1].ContextSerial, 1u);
|
||||||
|
EXPECT_EQ(wire.Decoder().AppliedSeq(), 2u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(PipeWireCodecTest, ObjectDeathReachesTheSinkWithItsHandleAndKind) {
|
||||||
|
Wire2 wire;
|
||||||
|
// One record per kind the death switch handles (CONTRACT-P5C.md §1: one payload for all
|
||||||
|
// seven), because the kind is what the sink dispatches on and a row that widened it
|
||||||
|
// wrong would drop every death of that kind.
|
||||||
|
const MGPipeKind kinds[] = {
|
||||||
|
MGPipeKind::Texture, MGPipeKind::Framebuffer, MGPipeKind::Renderbuffer,
|
||||||
|
MGPipeKind::SamplerCso, MGPipeKind::ShaderCso, MGPipeKind::SamplerViewCso,
|
||||||
|
MGPipeKind::VertexElementsCso,
|
||||||
|
};
|
||||||
|
Uint32 slot = 40;
|
||||||
|
for (MGPipeKind kind : kinds) {
|
||||||
|
MGPHandleOnly death = HandleOnly(slot, kind);
|
||||||
|
ASSERT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ObjectDeath, &death, sizeof(death)),
|
||||||
|
kInvalidSeq)
|
||||||
|
<< static_cast<Uint32>(kind);
|
||||||
|
++slot;
|
||||||
|
}
|
||||||
|
bool applied = false;
|
||||||
|
constexpr SizeT kKindCount = sizeof(kinds) / sizeof(kinds[0]);
|
||||||
|
for (SizeT i = 0; i < kKindCount; ++i) {
|
||||||
|
ASSERT_TRUE(wire.PumpOne(&applied)) << i;
|
||||||
|
EXPECT_TRUE(applied) << i;
|
||||||
|
}
|
||||||
|
ASSERT_EQ(wire.Sink().ObjectDeaths.size(), kKindCount);
|
||||||
|
slot = 40;
|
||||||
|
for (SizeT i = 0; i < kKindCount; ++i) {
|
||||||
|
EXPECT_EQ(wire.Sink().ObjectDeaths[i].Handle.Slot, slot) << i;
|
||||||
|
EXPECT_EQ(wire.Sink().ObjectDeaths[i].Handle.Gen, 1u) << i;
|
||||||
|
EXPECT_EQ(wire.Sink().ObjectDeaths[i].Kind, static_cast<Uint32>(kinds[i])) << i;
|
||||||
|
++slot;
|
||||||
|
}
|
||||||
|
EXPECT_EQ(wire.Decoder().AppliedSeq(), static_cast<Uint64>(kKindCount));
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
// P5b t2 (MG_Remote/CONTRACT-P5B.md §2 t2). c0b's cases above round-trip each row once; these
|
// P5b t2 (MG_Remote/CONTRACT-P5B.md §2 t2). c0b's cases above round-trip each row once; these
|
||||||
// pin the fields t2's EMITTERS actually fill and the one ordering property the span family has.
|
// pin the fields t2's EMITTERS actually fill and the one ordering property the span family has.
|
||||||
@@ -2893,3 +2976,185 @@ TEST(FenceWireRoundTrip, MissingConsumerDeclinesWithoutInventingASignaledAnswer)
|
|||||||
EXPECT_EQ(wire.Answers().All[0].Status, ReplySink::kStatusDeclined);
|
EXPECT_EQ(wire.Answers().All[0].Status, ReplySink::kStatusDeclined);
|
||||||
EXPECT_TRUE(wire.Answers().All[0].Bytes.empty());
|
EXPECT_TRUE(wire.Answers().All[0].Bytes.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =====================================================================================
|
||||||
|
// P5c ct (MG_Remote/CONTRACT-P5C.md §5): the two control records, through the REAL sink
|
||||||
|
// =====================================================================================
|
||||||
|
//
|
||||||
|
// The codec cases above pin the bytes and the dispatch with a recording sink; these pin what
|
||||||
|
// the SERVER's sink does with the record: the serial assert, the applier reset that actually
|
||||||
|
// runs, the twin release that actually retires the table entry, and the layer-2 guard that
|
||||||
|
// turns the reverted GL-thread direct call red.
|
||||||
|
|
||||||
|
TEST(ApplierResetWireRoundTrip, TheSerialSequenceIsAssertedAndTheServerResetRuns) {
|
||||||
|
Wire2 wire;
|
||||||
|
Server::ServerVerbSink sink;
|
||||||
|
wire.Decoder().SetVerbSink(&sink);
|
||||||
|
auto send = [&](Uint64 serial) {
|
||||||
|
const MGPApplierReset reset{serial};
|
||||||
|
EXPECT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ApplierReset, &reset, sizeof(reset)),
|
||||||
|
kInvalidSeq);
|
||||||
|
bool applied = false;
|
||||||
|
EXPECT_TRUE(wire.PumpOne(&applied));
|
||||||
|
EXPECT_TRUE(applied);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The reset RUNS - asserted on the applier's own state, not on the tally (R-16: a probe
|
||||||
|
// may not arm against a stub, and a tally a stub could also move is a stub's witness).
|
||||||
|
// BoundVertexElements is one of the fields MGPipeApplierReset clears (PipeApply.cpp).
|
||||||
|
MGPipeApplier().BoundVertexElements = MakeHandle(77);
|
||||||
|
send(0); // 0 = the first make-current (CONTRACT-P5C.md §1)
|
||||||
|
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundVertexElements));
|
||||||
|
EXPECT_EQ(sink.ApplierResets(), 1u);
|
||||||
|
EXPECT_EQ(sink.ExpectedApplierResetSerial(), 1u);
|
||||||
|
|
||||||
|
// The second edge carries the next serial and is accepted in order.
|
||||||
|
MGPipeApplier().BoundVertexElements = MakeHandle(78);
|
||||||
|
send(1);
|
||||||
|
EXPECT_TRUE(MGPipeHandleIsNull(MGPipeApplier().BoundVertexElements));
|
||||||
|
EXPECT_EQ(sink.ApplierResets(), 2u);
|
||||||
|
EXPECT_EQ(sink.ExpectedApplierResetSerial(), 2u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ObjectDeathWireRoundTrip, TheTextureAndFramebufferTwinsReleaseByHandleAndNeverByAStaleGeneration) {
|
||||||
|
Wire2 wire;
|
||||||
|
Server::ServerVerbSink sink;
|
||||||
|
wire.Decoder().SetVerbSink(&sink);
|
||||||
|
namespace gles = MG_Backend::DirectGLES;
|
||||||
|
|
||||||
|
// The handle arm answers only when kMGPipeSubsystemEsprytSlots is set, and a unit binary
|
||||||
|
// never runs ConfigLoader: Features.PipePush is 0 here, not the shipping default
|
||||||
|
// (kMGPipeSubsystemsMigratedAtP4a). Set the bit the way SanityTest's pipe fixtures do;
|
||||||
|
// the arm verdict latches on first use and this case is the first use in its process.
|
||||||
|
const Uint64 savedPush = MG_Config::Features.PipePush;
|
||||||
|
MG_Config::Features.PipePush = savedPush | kMGPipeSubsystemEsprytSlots;
|
||||||
|
struct RestorePush {
|
||||||
|
Uint64 bits;
|
||||||
|
~RestorePush() { MG_Config::Features.PipePush = bits; }
|
||||||
|
} restore{savedPush};
|
||||||
|
|
||||||
|
// Texture AND Framebuffer - the two kinds R-16 names for the death-recycle path. The
|
||||||
|
// others share the one dispatch and the one table walk, so two kinds pin the shape: one
|
||||||
|
// whose death also crosses as resource_destroy (Texture: the idempotent second path),
|
||||||
|
// and the kind whose FIRST wire delete opcode object_death is (Framebuffer).
|
||||||
|
struct KindCase {
|
||||||
|
MGPipeKind kind;
|
||||||
|
Uint32 slot;
|
||||||
|
};
|
||||||
|
const KindCase cases[] = {{MGPipeKind::Texture, 901}, {MGPipeKind::Framebuffer, 902}};
|
||||||
|
for (const KindCase& one : cases) {
|
||||||
|
const auto getOrCreate = [&](MGPipeHandle h) -> void* {
|
||||||
|
if (one.kind == MGPipeKind::Texture) {
|
||||||
|
return gles::TextureImpl::g_backendTextureObjects.GetOrCreateByHandle(h);
|
||||||
|
}
|
||||||
|
return gles::FramebufferImpl::g_backendFramebufferObjects.GetOrCreateByHandle(h);
|
||||||
|
};
|
||||||
|
const auto liveGen = [&](Uint32 slot) -> Uint32 {
|
||||||
|
if (one.kind == MGPipeKind::Texture) {
|
||||||
|
return gles::TextureImpl::g_backendTextureObjects.LiveGenAt(slot);
|
||||||
|
}
|
||||||
|
return gles::FramebufferImpl::g_backendFramebufferObjects.LiveGenAt(slot);
|
||||||
|
};
|
||||||
|
ASSERT_NE(getOrCreate(MakeHandle(one.slot, 1)), nullptr)
|
||||||
|
<< "the Espryt handle arm did not go live with kMGPipeSubsystemEsprytSlots set; "
|
||||||
|
"the verdict latched elsewhere in this process";
|
||||||
|
ASSERT_EQ(liveGen(one.slot), 1u) << static_cast<Uint32>(one.kind);
|
||||||
|
|
||||||
|
// The death crosses and the twin retires: the slot's live generation goes back to 0.
|
||||||
|
const Uint64 deathsBefore = sink.ObjectDeaths();
|
||||||
|
const MGPHandleOnly death{MakeHandle(one.slot, 1), static_cast<Uint32>(one.kind), 0};
|
||||||
|
EXPECT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ObjectDeath, &death, sizeof(death)),
|
||||||
|
kInvalidSeq);
|
||||||
|
bool applied = false;
|
||||||
|
EXPECT_TRUE(wire.PumpOne(&applied));
|
||||||
|
EXPECT_TRUE(applied);
|
||||||
|
EXPECT_EQ(liveGen(one.slot), 0u) << static_cast<Uint32>(one.kind);
|
||||||
|
EXPECT_EQ(sink.ObjectDeaths(), deathsBefore + 1u);
|
||||||
|
|
||||||
|
// THE ABA HALF (LiveGenAt's semantics): the slot is recycled forward to a NEW object
|
||||||
|
// at generation 2, and the dead object's handle - replayed, as a duplicated or
|
||||||
|
// reordered record would replay it - must not answer for the new object. The record
|
||||||
|
// is still APPLIED (idempotency is legal; a generation mismatch is a no-op), but the
|
||||||
|
// new twin survives it.
|
||||||
|
ASSERT_NE(getOrCreate(MakeHandle(one.slot, 2)), nullptr);
|
||||||
|
ASSERT_EQ(liveGen(one.slot), 2u);
|
||||||
|
EXPECT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ObjectDeath, &death, sizeof(death)),
|
||||||
|
kInvalidSeq);
|
||||||
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||||
|
EXPECT_TRUE(applied);
|
||||||
|
EXPECT_EQ(liveGen(one.slot), 2u)
|
||||||
|
<< "a stale handle released the recycled slot's new twin (kind "
|
||||||
|
<< static_cast<Uint32>(one.kind) << ")";
|
||||||
|
|
||||||
|
// And the new object's own death retires it, leaving the table as the case found it.
|
||||||
|
const MGPHandleOnly newDeath{MakeHandle(one.slot, 2), static_cast<Uint32>(one.kind), 0};
|
||||||
|
EXPECT_NE(wire.Encoder().EncodeRecord(MGPWireOp::ObjectDeath, &newDeath, sizeof(newDeath)),
|
||||||
|
kInvalidSeq);
|
||||||
|
ASSERT_TRUE(wire.PumpOne(&applied));
|
||||||
|
EXPECT_TRUE(applied);
|
||||||
|
EXPECT_EQ(liveGen(one.slot), 0u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if MGTEST_HAVE_FORK
|
||||||
|
|
||||||
|
// The three Fatal arms, driven through the encoder and the REAL sink in a forked child (the
|
||||||
|
// file's rule: never EXPECT_DEATH here - it re-runs the whole binary and re-enters the
|
||||||
|
// applier's globals).
|
||||||
|
|
||||||
|
TEST(CtWireFatals, ASerialTheSessionCannotProveIsProtocolCorruption) {
|
||||||
|
const ChildResult r = RunInChild([] {
|
||||||
|
Wire2 wire;
|
||||||
|
Server::ServerVerbSink sink;
|
||||||
|
wire.Decoder().SetVerbSink(&sink);
|
||||||
|
// The FIRST reset a session sees must carry serial 0 (one context, the count is the
|
||||||
|
// session's own); leading with 1 is a sequence the session cannot prove.
|
||||||
|
const MGPApplierReset reset{1};
|
||||||
|
(void)wire.Encoder().EncodeRecord(MGPWireOp::ApplierReset, &reset, sizeof(reset));
|
||||||
|
bool applied = false;
|
||||||
|
(void)wire.PumpOne(&applied);
|
||||||
|
});
|
||||||
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_NE(r.Log.find("Fatal{ProtocolCorruption, \"ApplierReset.ContextSerial\"}"),
|
||||||
|
std::string::npos)
|
||||||
|
<< r.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(CtWireFatals, ANullDeathHandleIsProtocolCorruption) {
|
||||||
|
const ChildResult r = RunInChild([] {
|
||||||
|
Wire2 wire;
|
||||||
|
Server::ServerVerbSink sink;
|
||||||
|
wire.Decoder().SetVerbSink(&sink);
|
||||||
|
// §1's zero ruling: a null handle means "the object never crossed", and the client
|
||||||
|
// emits NOTHING then - so a null handle ON the wire is corruption, not a no-op.
|
||||||
|
const MGPHandleOnly death{kMGPipeNullHandle, static_cast<Uint32>(MGPipeKind::Texture), 0};
|
||||||
|
(void)wire.Encoder().EncodeRecord(MGPWireOp::ObjectDeath, &death, sizeof(death));
|
||||||
|
bool applied = false;
|
||||||
|
(void)wire.PumpOne(&applied);
|
||||||
|
});
|
||||||
|
ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log;
|
||||||
|
EXPECT_NE(r.Log.find("Fatal{ProtocolCorruption, \"ObjectDeath.Handle\"}"), std::string::npos)
|
||||||
|
<< r.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE GUARD'S TWO NON-FATAL ARMS, as unit controls. The Fatal arm itself needs a LIVE client
|
||||||
|
// session (the guard deliberately exempts the configured-but-wireless window - §6 layer 2's
|
||||||
|
// documented bring-up exception), which a unit binary has no handshake for; that arm is the
|
||||||
|
// integration lane's CtWireScenario.TheDirectApplierResetCallOnTheGLThreadIsRoleViolation,
|
||||||
|
// and the manual revert of PipeFill's FreshlyPrimed arm is the red-once beside it. What is
|
||||||
|
// pinnable here is that neither monolith nor a wireless transport is stopped.
|
||||||
|
TEST(CtWireFatals, TheDirectApplierResetCallSurvivesMonolithAndAWirelessTransport) {
|
||||||
|
const ChildResult wireless = RunInChild([] {
|
||||||
|
// Transport=InProcess with NO client session - the ServerLoop fixture's shape. The
|
||||||
|
// direct call is the only reset that exists there, so it must NOT be stopped.
|
||||||
|
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||||
|
MGPipeApplierReset();
|
||||||
|
});
|
||||||
|
EXPECT_FALSE(DiedOfAbort(wireless)) << DescribeStatus(wireless) << "\n" << wireless.Log;
|
||||||
|
|
||||||
|
const ChildResult monolith = RunInChild([] { MGPipeApplierReset(); });
|
||||||
|
EXPECT_FALSE(DiedOfAbort(monolith)) << DescribeStatus(monolith) << "\n" << monolith.Log;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // MGTEST_HAVE_FORK
|
||||||
|
|||||||
Reference in New Issue
Block a user