[WIP] (MG_Remote): implement P6.5 TCP and stream transport

This commit is contained in:
2026-09-22 07:19:18 -04:00
parent 810b9b7a9f
commit 52ffc6a7f2
68 changed files with 4934 additions and 928 deletions
+4
View File
@@ -592,6 +592,9 @@ endif()
if (MOBILEGL_BUILD_DISAGGREGATED)
message(STATUS "MobileGL: disaggregated transport ON, appending MG_Remote sources")
list(APPEND SOURCE_FILES
MobileGL/MG_Remote/Transport/ShmLink.cpp
MobileGL/MG_Remote/Transport/LinkMetrics.cpp
MobileGL/MG_Remote/Transport/StreamLink.cpp
MobileGL/MG_Remote/Transport/Ring.cpp
MobileGL/MG_Remote/Transport/Doorbell.cpp
MobileGL/MG_Remote/Transport/ShmSegment.cpp
@@ -603,6 +606,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
MobileGL/MG_Remote/Transport/FdPassing.cpp
MobileGL/MG_Remote/Transport/InProcessTransport.cpp
MobileGL/MG_Remote/Transport/SocketTransport.cpp
MobileGL/MG_Remote/Transport/ControlInbox.cpp
# Keeps MG_Util/Debug/Log.h - and through it the GL frontend's
# umbrella header - out of the header-only wire code (WireLog.h).
MobileGL/MG_Remote/Transport/WireLog.cpp
+3
View File
@@ -471,6 +471,9 @@ namespace MobileGL::MG_Config {
// SPLIT variant already carry it, and an environment variable that nothing parses is
// indistinguishable from one that is parsed and ignored.
String ServerPath;
// Control endpoint and data-plane selection are independent of topology.
String Control = "fork"; // fork | unix:<path> | tcp://host:port
String Data = "auto"; // auto | shm | stream
// MOBILEGL_IPC_RING_MB: SEG_CMD size. A RECORD MAY BE AT MOST HALF OF THIS
// (RingProducer::MaxRecordBytes), so 8 MiB caps one record at 4 MiB; R-10 makes the
// codec publish a max-record-bytes counter rather than assume that is enough.
+2
View File
@@ -378,6 +378,8 @@ namespace MobileGL::MG_ConfigLoader {
inline void InitIpc() {
auto& ipc = MG_Config::Ipc;
QueryEnvVariable("MOBILEGL_IPC_SERVER_PATH", ipc.ServerPath, "");
QueryEnvVariable("MOBILEGL_IPC_CONTROL", ipc.Control, "fork");
QueryEnvVariable("MOBILEGL_IPC_DATA", ipc.Data, "auto");
// Both ring floors are 1 MiB, not 0: a ring caps ONE record at half its size, and
// the catalogue's largest fixed payload (MGPFramebufferState, 304 bytes) plus a
// create_shader_state archive already needs far more than a toy ring. The ceilings
+5 -5
View File
@@ -332,13 +332,13 @@ namespace MobileGL {
};
struct DynamicBackendParameters {
SizeT UniformBufferOffsetAlignment = 256;
Uint64 UniformBufferOffsetAlignment = 256;
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which is a SEPARATE limit from the
// uniform one and is routinely larger: Adreno 830 reports 32 for uniform buffers and
// 64 for storage buffers. Answering the storage query with the uniform value let an
// application bind a storage range at an offset the driver cannot address, which it
// accepted without error and then wrote somewhere else entirely.
SizeT ShaderStorageBufferOffsetAlignment = 256;
Uint64 ShaderStorageBufferOffsetAlignment = 256;
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
// which is also why the extension is not advertised in that case.
Float MaxTextureMaxAnisotropy = 1.0f;
@@ -460,8 +460,8 @@ namespace MobileGL {
// VulkanRenderer::SelectProvokingVertexMode out of VK_EXT_provoking_vertex,
// provokingVertexModePerPipeline and the topology, so no single convention is true
// of the backend.
GLenum LayerProvokingVertex = GL_UNDEFINED_VERTEX;
GLenum ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
Uint32 LayerProvokingVertex = GL_UNDEFINED_VERTEX;
Uint32 ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
Int MaxViewportWidth = 16384;
Int MaxViewportHeight = 16384;
Float ViewportBoundsRangeMin = 0.0f;
@@ -558,7 +558,7 @@ namespace MobileGL {
Bool SupportsTessellationPointSize = true;
// The geometry-stage twin (OpCapability GeometryPointSize).
Bool SupportsGeometryPointSize = true;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint64 MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0;
Uint32 SubgroupSupportedStages = 0;
Uint32 SubgroupSupportedFeatures = 0;
+5 -5
View File
@@ -600,11 +600,11 @@ namespace MobileGL::MG_Pipe {
// so only a non-empty run goes through the bounds-checked resolve.
if (bytes.Size != 0) {
auto* session = MG_Remote::Client::ClientSession::Active();
const void* resolved =
session == nullptr
? nullptr
: session->Segments().Resolve(MG_Remote::Wire::kSegEvent, bytes.Offset,
bytes.Size);
const void* resolved = nullptr;
if (session && session->DataLink()) {
session->DataLink()->ResolveSpan(MG_Remote::Transport::LinkSegment::Event,
{bytes.Offset, bytes.Size}, &resolved);
}
if (resolved == nullptr) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"OnBufferWriteback.Offset\"} - "
"the writeback blobref's {%llu + %llu} does not resolve inside the "
+45 -1
View File
@@ -2003,6 +2003,38 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
"MOBILEGL_IPC_SERVER_PATH=${MGL_ITEST_SPLIT_SERVER_PATH}" "MGITEST_SPLIT_LANE=1"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
# A single supervisor serves one session at a time. Fixture teardown also reaps a
# child left behind by a failed client; the resource lock serializes this lane.
set(MOBILEGL_ITEST_TCP_ENDPOINT "tcp://127.0.0.1:40613" CACHE STRING "Loopback TCP integration endpoint")
set(MOBILEGL_ITEST_TCP_DEVICE_ENDPOINT "" CACHE STRING "Optional device TCP integration endpoint")
set(MOBILEGL_ITEST_TCP_DEVICE_TOKEN "" CACHE STRING "Token for the device TCP integration endpoint")
find_package(Python3 REQUIRED COMPONENTS Interpreter)
set(MGL_TCP_FIXTURE "${CMAKE_SOURCE_DIR}/scripts/ci/tcp_server_fixture.py")
set(MGL_TCP_STATE "${CMAKE_CURRENT_BINARY_DIR}/tcp-server.json")
add_test(NAME TcpServer.Start COMMAND "${Python3_EXECUTABLE}" "${MGL_TCP_FIXTURE}"
start --server "${MGL_ITEST_SPLIT_SERVER_PATH}" --endpoint "${MOBILEGL_ITEST_TCP_ENDPOINT}"
--state "${MGL_TCP_STATE}")
add_test(NAME TcpServer.Stop COMMAND "${Python3_EXECUTABLE}" "${MGL_TCP_FIXTURE}"
stop --state "${MGL_TCP_STATE}")
set_tests_properties(TcpServer.Start PROPERTIES FIXTURES_SETUP mobilegl-tcp TIMEOUT 20)
set_tests_properties(TcpServer.Stop PROPERTIES FIXTURES_CLEANUP mobilegl-tcp TIMEOUT 20)
add_test(NAME TcpLane.AccountingControls COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_SOURCE_DIR}/scripts/ci/tcp_lane_tools_test.py")
set_tests_properties(TcpLane.AccountingControls PROPERTIES LABELS unit TIMEOUT 20)
mgl_itest_join_environment(MGL_ITEST_GLES_TCP_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_TRANSPORT=spawn"
"MOBILEGL_IPC_CONTROL=${MOBILEGL_ITEST_TCP_ENDPOINT}" "MOBILEGL_IPC_DATA=stream"
"MOBILEGL_IPC_REQUIRE_SAME_BUILD=1" "MOBILEGL_IPC_LOG_FORWARD=1" "MOBILEGL_IPC_TOKEN="
"MGITEST_SPLIT_LANE=1" "MGITEST_TCP_LANE=1" "MOBILEGL_ITEST_REQUIRE_GPU=1"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
mgl_itest_join_environment(MGL_ITEST_GLES_TCPDEVICE_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_TRANSPORT=spawn"
"MOBILEGL_IPC_CONTROL=${MOBILEGL_ITEST_TCP_DEVICE_ENDPOINT}" "MOBILEGL_IPC_DATA=stream"
"MOBILEGL_IPC_REQUIRE_SAME_BUILD=1" "MOBILEGL_IPC_LOG_FORWARD=1"
"MOBILEGL_IPC_TOKEN=${MOBILEGL_ITEST_TCP_DEVICE_TOKEN}"
"MGITEST_SPLIT_LANE=1" "MGITEST_TCP_LANE=1" "MOBILEGL_ITEST_REQUIRE_GPU=1"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
# scenarioFilter, a PREFIX TAIL (empty for most; "F1." and "Ct." for the two per-slot loops,
# which name their sub-lane in the test name), then zero or more EXTRA LABEL STEMS. A stem is
# suffixed with the arm's own name (`integration-clientarrays` becomes `-split` on one arm and
@@ -2017,9 +2049,20 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
set(MGL_SPLIT_ARM_TEST_LISTS "")
set(MGL_SPLIT_ARM_INDEX 0)
macro(mgl_itest_register_split_arms scenarioFilter prefixTail)
foreach(mglItestArm IN ITEMS Split Spawn)
set(mglItestArms Split Spawn Tcp)
if(MOBILEGL_ITEST_TCP_DEVICE_ENDPOINT)
list(APPEND mglItestArms TcpDevice)
endif()
foreach(mglItestArm IN LISTS mglItestArms)
string(TOLOWER "${mglItestArm}" mglItestArmLower)
string(TOUPPER "${mglItestArm}" mglItestArmUpper)
set(mglItestArmProperties)
if(mglItestArm STREQUAL "Tcp")
set(mglItestArmProperties FIXTURES_REQUIRED mobilegl-tcp RESOURCE_LOCK mobilegl-tcp)
elseif(mglItestArm STREQUAL "TcpDevice")
set(mglItestArmLower tcp-device)
set(mglItestArmProperties RESOURCE_LOCK mobilegl-tcp-device)
endif()
set(mglItestArmLabels "integration-gpu\\;integration-${mglItestArmLower}")
foreach(mglItestExtra IN ITEMS ${ARGN})
set(mglItestArmLabels "${mglItestArmLabels}\\;${mglItestExtra}-${mglItestArmLower}")
@@ -2042,6 +2085,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
LABELS "${mglItestArmLabels}"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_GLES_${mglItestArmUpper}_ENVIRONMENT}"
${mglItestArmProperties}
)
list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}")
math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1")
@@ -327,7 +327,18 @@ namespace MGITest {
// has thought of. Only then does the parent do the real bring-up.
//
// Returns an empty string when the platform survived a full bring-up.
bool RemoteConnectRequested() {
const char* control = std::getenv("MOBILEGL_IPC_CONTROL");
return control != nullptr && std::strncmp(control, "tcp://", 6) == 0;
}
std::string PreflightBringUp() {
// Connect already isolates driver setup in the server's session
// child. A local pre-flight would consume a remote session, then
// race its teardown and make the actual test receive Refuse{Busy}.
if (RemoteConnectRequested()) {
return {};
}
#if !MGITEST_HAVE_FORK_PREFLIGHT
// No fork(): let the in-process bring-up speak for itself, which is
// what this module did before. Windows/macOS are not CI targets for
@@ -585,7 +596,9 @@ namespace MGITest {
// That is a real result, not a machine without a GPU, so say so: it
// means something is different between the two attempts (a leaked
// exclusive device, an environment the child did not have).
m_skipReason = reason + " - although an identical bring-up in a forked pre-flight child succeeded";
m_skipReason = reason + (RemoteConnectRequested()
? " - remote TCP bring-up failed"
: " - although an identical bring-up in a forked pre-flight child succeeded");
return false;
}
@@ -36,6 +36,10 @@
#include <iterator>
#include <string>
#if MOBILEGL_BUILD_DISAGGREGATED
extern "C" void MGPipeSyncPeerLog();
#endif
namespace MGITest::PipeStatsWindow {
// P6: MOBILEGL_LOG_FILE_PATH IS A BASE NAME, NOT A FILE. The library writes one log per
@@ -121,6 +125,9 @@ namespace MGITest::PipeStatsWindow {
}
inline LogMark MarkLaneLog() {
#if MOBILEGL_BUILD_DISAGGREGATED
MGPipeSyncPeerLog();
#endif
return LogMark{FileSizeOrZero(LibraryLogPath()), FileSizeOrZero(ServerLibraryLogPath())};
}
@@ -131,6 +138,9 @@ namespace MGITest::PipeStatsWindow {
// which is the most expensive possible way to be wrong: it accuses the product of a defect
// that the reader itself invented.
inline std::string ReadLaneLogSince(const LogMark& mark) {
#if MOBILEGL_BUILD_DISAGGREGATED
MGPipeSyncPeerLog();
#endif
return ReadFileSince(LibraryLogPath(), mark.client)
+ ReadFileSince(ServerLibraryLogPath(), mark.server);
}
@@ -23,7 +23,11 @@
#include <cctype>
#include <cstdlib>
#include <fstream>
#include <string>
#if defined(__linux__)
#include <unistd.h>
#endif
#include <gtest/gtest.h>
@@ -152,6 +156,18 @@ namespace MGITest {
RecordProperty("split_transport", state.transportName);
RecordProperty("split_implemented_verbs", static_cast<int>(state.implementedVerbs));
RecordProperty("split_emit_seq_at_setup", static_cast<int>(state.emitSeq));
#if defined(__linux__)
if (std::getenv("MGITEST_TCP_LANE")) {
// Connect must never silently launch a local server. The
// companion log gate proves the remote Welcome pid/endpoint.
std::ifstream children("/proc/self/task/" + std::to_string(::getpid()) + "/children");
ASSERT_TRUE(children.good()) << "TCP topology proof cannot read /proc children";
int child = 0;
ASSERT_FALSE(static_cast<bool>(children >> child))
<< "Dial=Connect created a local server child: " << child;
RecordProperty("tcp_local_children", 0);
}
#endif
}
// A scenario starts from a clean slate but shares the context (and so
// the renderer's memos) with every other scenario in this process -
@@ -48,7 +48,8 @@ def paths(document):
props = {p["name"]: p["value"] for p in test.get("properties", [])}
values = [v.split("=", 1)[1] for v in props.get("ENVIRONMENT", [])
if v.startswith("MOBILEGL_LOG_FILE_PATH=")]
is_split = test["name"].startswith(("DirectGLES.Split.", "DirectVulkan.Split."))
is_split = test["name"].startswith(("DirectGLES.Split.", "DirectVulkan.Split.",
"DirectGLES.Spawn.", "DirectGLES.Tcp.", "DirectGLES.TcpDevice."))
if is_split and (len(values) != 1 or not values[0]):
raise ValueError(f"{test['name']}: requires exactly one nonempty MOBILEGL_LOG_FILE_PATH")
for value in values:
+2 -4
View File
@@ -175,10 +175,8 @@ namespace MobileGL::MG_Pipe {
MGPBlobRef RendererInfo;
};
static_assert(std::is_trivially_copyable_v<MGPCaps>, "MGPCaps must be trivially copyable");
// Stated as a COMPOSITION rather than a literal: DynamicBackendParameters still carries
// SizeT fields, so its literal size is ABI-dependent until P0.5 moves the caps block
// into MGPipeValueTypes.h with fixed-width members. The assertion still fires on any
// padding introduced between the members below.
// Stated as a composition to catch padding between the wire members.
// P6.5: all dynamic scalar widths are explicit; member offsets are in the wire digest.
static_assert(sizeof(MGPCaps) == sizeof(DynamicBackendParameters) + 8 + 24 + 24,
"MGPCaps gained padding or a member; update the wire format");
+55
View File
@@ -0,0 +1,55 @@
// MobileGL - P6.5 wire compatibility facts. Generated member coverage is checked by gen_pipe.py.
#pragma once
#include "MGPipe.h"
#include "MGPipeRenderStateSpans.h"
namespace MobileGL::MG_Pipe {
#include "generated/PipeWireLayout.inc"
static_assert(sizeof(Bool) == 1 && sizeof(Float) == 4 && sizeof(Int32) == 4,
"wire scalar representation is unsupported");
static_assert(sizeof(RenderStateParameters) == 1168, "render-state wire blob changed");
constexpr void WireMixNumber(Uint64& hash, Uint64 value) {
for (unsigned i = 0; i < 8; ++i) {
hash ^= (value >> (i * 8)) & 255;
hash *= 1099511628211ull;
}
}
constexpr void WireMixName(Uint64& hash, const char* name) {
do {
hash ^= static_cast<unsigned char>(*name);
hash *= 1099511628211ull;
} while (*name++ != '\0');
}
constexpr Uint64 WireMemberLayoutDigest(const WireLayoutMember* fields, SizeT count) {
Uint64 hash = 1469598103934665603ull;
for (SizeT i = 0; i < count; ++i) {
WireMixName(hash, fields[i].Name);
WireMixNumber(hash, fields[i].Offset);
WireMixNumber(hash, fields[i].Size);
}
return hash;
}
inline constexpr Uint64 kMGPipeWireMemberLayoutDigest = WireMemberLayoutDigest(
kMGPipeWireLayoutMembers, sizeof(kMGPipeWireLayoutMembers) / sizeof(WireLayoutMember));
constexpr Uint64 WireCatalogueDigest() {
Uint64 hash = 1469598103934665603ull;
for (const auto& call : kMGPipeWireCallLayouts) {
WireMixName(hash, call.Name);
WireMixName(hash, call.Payload);
WireMixNumber(hash, call.Op);
WireMixNumber(hash, call.Flags);
WireMixNumber(hash, call.Wait);
}
return hash;
}
inline constexpr Uint64 kMGPipeWireCatalogueDigest = WireCatalogueDigest();
constexpr Uint64 WireRenderStateDigest() {
Uint64 hash = 1469598103934665603ull;
WireMixNumber(hash, kMGPipeRenderStateChunkTableVersion);
for (SizeT boundary : kMGPipeRenderStateChunkBoundaries) WireMixNumber(hash, boundary);
return hash;
}
} // namespace MobileGL::MG_Pipe
@@ -0,0 +1,756 @@
// MobileGL - MobileGL/MG_Pipe/generated/PipeWireLayout.inc
// 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
// P6.5: wire layout and ordered catalogue.
//
// GENERATED by scripts/gen_pipe.py from PipeFields.def and PipeCalls.def - DO NOT EDIT.
// 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.
struct WireLayoutMember { const char* Name; Uint64 Offset; Uint64 Size; };
inline constexpr WireLayoutMember kMGPipeWireLayoutMembers[] = {
{"MGPBlobRef", 0, sizeof(MGPBlobRef)},
{"MGPBlobRef.Offset", offsetof(MGPBlobRef, Offset), sizeof(((MGPBlobRef*)nullptr)->Offset)},
{"MGPBlobRef.Size", offsetof(MGPBlobRef, Size), sizeof(((MGPBlobRef*)nullptr)->Size)},
{"MGPBlobRef.Seg", offsetof(MGPBlobRef, Seg), sizeof(((MGPBlobRef*)nullptr)->Seg)},
{"MGPRange", 0, sizeof(MGPRange)},
{"MGPRange.Offset", offsetof(MGPRange, Offset), sizeof(((MGPRange*)nullptr)->Offset)},
{"MGPRange.Size", offsetof(MGPRange, Size), sizeof(((MGPRange*)nullptr)->Size)},
{"MGPBox", 0, sizeof(MGPBox)},
{"MGPBox.X", offsetof(MGPBox, X), sizeof(((MGPBox*)nullptr)->X)},
{"MGPBox.Y", offsetof(MGPBox, Y), sizeof(((MGPBox*)nullptr)->Y)},
{"MGPBox.Z", offsetof(MGPBox, Z), sizeof(((MGPBox*)nullptr)->Z)},
{"MGPBox.W", offsetof(MGPBox, W), sizeof(((MGPBox*)nullptr)->W)},
{"MGPBox.H", offsetof(MGPBox, H), sizeof(((MGPBox*)nullptr)->H)},
{"MGPBox.D", offsetof(MGPBox, D), sizeof(((MGPBox*)nullptr)->D)},
{"MGPReplySlot", 0, sizeof(MGPReplySlot)},
{"MGPReplySlot.Id", offsetof(MGPReplySlot, Id), sizeof(((MGPReplySlot*)nullptr)->Id)},
{"MGPStateChunk", 0, sizeof(MGPStateChunk)},
{"MGPStateChunk.Offset", offsetof(MGPStateChunk, Offset), sizeof(((MGPStateChunk*)nullptr)->Offset)},
{"MGPStateChunk.Length", offsetof(MGPStateChunk, Length), sizeof(((MGPStateChunk*)nullptr)->Length)},
{"MGPHandleOnly", 0, sizeof(MGPHandleOnly)},
{"MGPHandleOnly.Handle", offsetof(MGPHandleOnly, Handle), sizeof(((MGPHandleOnly*)nullptr)->Handle)},
{"MGPHandleOnly.Kind", offsetof(MGPHandleOnly, Kind), sizeof(((MGPHandleOnly*)nullptr)->Kind)},
{"MGPCaps", 0, sizeof(MGPCaps)},
{"MGPCaps.Dynamic", offsetof(MGPCaps, Dynamic), sizeof(((MGPCaps*)nullptr)->Dynamic)},
{"MGPCaps.CallMask", offsetof(MGPCaps, CallMask), sizeof(((MGPCaps*)nullptr)->CallMask)},
{"MGPCaps.FormatCapabilities", offsetof(MGPCaps, FormatCapabilities), sizeof(((MGPCaps*)nullptr)->FormatCapabilities)},
{"MGPCaps.RendererInfo", offsetof(MGPCaps, RendererInfo), sizeof(((MGPCaps*)nullptr)->RendererInfo)},
{"MGPResourceDesc", 0, sizeof(MGPResourceDesc)},
{"MGPResourceDesc.Resource", offsetof(MGPResourceDesc, Resource), sizeof(((MGPResourceDesc*)nullptr)->Resource)},
{"MGPResourceDesc.Target", offsetof(MGPResourceDesc, Target), sizeof(((MGPResourceDesc*)nullptr)->Target)},
{"MGPResourceDesc.StorageKind", offsetof(MGPResourceDesc, StorageKind), sizeof(((MGPResourceDesc*)nullptr)->StorageKind)},
{"MGPResourceDesc.BindMask", offsetof(MGPResourceDesc, BindMask), sizeof(((MGPResourceDesc*)nullptr)->BindMask)},
{"MGPResourceDesc.InternalFormat", offsetof(MGPResourceDesc, InternalFormat), sizeof(((MGPResourceDesc*)nullptr)->InternalFormat)},
{"MGPResourceDesc.Width", offsetof(MGPResourceDesc, Width), sizeof(((MGPResourceDesc*)nullptr)->Width)},
{"MGPResourceDesc.Height", offsetof(MGPResourceDesc, Height), sizeof(((MGPResourceDesc*)nullptr)->Height)},
{"MGPResourceDesc.Depth", offsetof(MGPResourceDesc, Depth), sizeof(((MGPResourceDesc*)nullptr)->Depth)},
{"MGPResourceDesc.ArrayLayers", offsetof(MGPResourceDesc, ArrayLayers), sizeof(((MGPResourceDesc*)nullptr)->ArrayLayers)},
{"MGPResourceDesc.Levels", offsetof(MGPResourceDesc, Levels), sizeof(((MGPResourceDesc*)nullptr)->Levels)},
{"MGPResourceDesc.Samples", offsetof(MGPResourceDesc, Samples), sizeof(((MGPResourceDesc*)nullptr)->Samples)},
{"MGPResourceDesc.FixedSampleLocations", offsetof(MGPResourceDesc, FixedSampleLocations), sizeof(((MGPResourceDesc*)nullptr)->FixedSampleLocations)},
{"MGPResourceDesc.Immutable", offsetof(MGPResourceDesc, Immutable), sizeof(((MGPResourceDesc*)nullptr)->Immutable)},
{"MGPResourceDesc.Usage", offsetof(MGPResourceDesc, Usage), sizeof(((MGPResourceDesc*)nullptr)->Usage)},
{"MGPResourceDesc.StorageFlags", offsetof(MGPResourceDesc, StorageFlags), sizeof(((MGPResourceDesc*)nullptr)->StorageFlags)},
{"MGPResourceDesc.HasDefinedContent", offsetof(MGPResourceDesc, HasDefinedContent), sizeof(((MGPResourceDesc*)nullptr)->HasDefinedContent)},
{"MGPResourceDesc.ImageBindableHint", offsetof(MGPResourceDesc, ImageBindableHint), sizeof(((MGPResourceDesc*)nullptr)->ImageBindableHint)},
{"MGPResourceDesc.HasRespecifiedLevel", offsetof(MGPResourceDesc, HasRespecifiedLevel), sizeof(((MGPResourceDesc*)nullptr)->HasRespecifiedLevel)},
{"MGPResourceDesc.GlNameForDiag", offsetof(MGPResourceDesc, GlNameForDiag), sizeof(((MGPResourceDesc*)nullptr)->GlNameForDiag)},
{"MGPResourceDesc.RespecifiedUploadTarget", offsetof(MGPResourceDesc, RespecifiedUploadTarget), sizeof(((MGPResourceDesc*)nullptr)->RespecifiedUploadTarget)},
{"MGPResourceDesc.RespecifiedLevel", offsetof(MGPResourceDesc, RespecifiedLevel), sizeof(((MGPResourceDesc*)nullptr)->RespecifiedLevel)},
{"MGPResourceDesc.ViewOf", offsetof(MGPResourceDesc, ViewOf), sizeof(((MGPResourceDesc*)nullptr)->ViewOf)},
{"MGPResourceDesc.BufferForTexBuffer", offsetof(MGPResourceDesc, BufferForTexBuffer), sizeof(((MGPResourceDesc*)nullptr)->BufferForTexBuffer)},
{"MGPResourceDesc.BufOffset", offsetof(MGPResourceDesc, BufOffset), sizeof(((MGPResourceDesc*)nullptr)->BufOffset)},
{"MGPResourceDesc.BufSize", offsetof(MGPResourceDesc, BufSize), sizeof(((MGPResourceDesc*)nullptr)->BufSize)},
{"MGPFenceWait", 0, sizeof(MGPFenceWait)},
{"MGPFenceWait.Fence", offsetof(MGPFenceWait, Fence), sizeof(((MGPFenceWait*)nullptr)->Fence)},
{"MGPFenceWait.TimeoutNs", offsetof(MGPFenceWait, TimeoutNs), sizeof(((MGPFenceWait*)nullptr)->TimeoutNs)},
{"MGPFenceWait.Flags", offsetof(MGPFenceWait, Flags), sizeof(((MGPFenceWait*)nullptr)->Flags)},
{"MGPQueryDesc", 0, sizeof(MGPQueryDesc)},
{"MGPQueryDesc.Query", offsetof(MGPQueryDesc, Query), sizeof(((MGPQueryDesc*)nullptr)->Query)},
{"MGPQueryDesc.Kind", offsetof(MGPQueryDesc, Kind), sizeof(((MGPQueryDesc*)nullptr)->Kind)},
{"MGPQueryDesc.Stream", offsetof(MGPQueryDesc, Stream), sizeof(((MGPQueryDesc*)nullptr)->Stream)},
{"MGPQueryResultRequest", 0, sizeof(MGPQueryResultRequest)},
{"MGPQueryResultRequest.Query", offsetof(MGPQueryResultRequest, Query), sizeof(((MGPQueryResultRequest*)nullptr)->Query)},
{"MGPQueryResultRequest.Wait", offsetof(MGPQueryResultRequest, Wait), sizeof(((MGPQueryResultRequest*)nullptr)->Wait)},
{"MGPTimestampRequest", 0, sizeof(MGPTimestampRequest)},
{"MGPTimestampRequest.Reserved", offsetof(MGPTimestampRequest, Reserved), sizeof(((MGPTimestampRequest*)nullptr)->Reserved)},
{"MGPRenderStateDesc", 0, sizeof(MGPRenderStateDesc)},
{"MGPRenderStateDesc.Cso", offsetof(MGPRenderStateDesc, Cso), sizeof(((MGPRenderStateDesc*)nullptr)->Cso)},
{"MGPRenderStateDesc.BaseCso", offsetof(MGPRenderStateDesc, BaseCso), sizeof(((MGPRenderStateDesc*)nullptr)->BaseCso)},
{"MGPRenderStateDesc.ChunkMask", offsetof(MGPRenderStateDesc, ChunkMask), sizeof(((MGPRenderStateDesc*)nullptr)->ChunkMask)},
{"MGPRenderStateDesc.Blob", offsetof(MGPRenderStateDesc, Blob), sizeof(((MGPRenderStateDesc*)nullptr)->Blob)},
{"MGPBindRenderState", 0, sizeof(MGPBindRenderState)},
{"MGPBindRenderState.Cso", offsetof(MGPBindRenderState, Cso), sizeof(((MGPBindRenderState*)nullptr)->Cso)},
{"MGPBindRenderState.Version", offsetof(MGPBindRenderState, Version), sizeof(((MGPBindRenderState*)nullptr)->Version)},
{"MGPBindRenderState.PipelineVersion", offsetof(MGPBindRenderState, PipelineVersion), sizeof(((MGPBindRenderState*)nullptr)->PipelineVersion)},
{"MGPDynamicState", 0, sizeof(MGPDynamicState)},
{"MGPDynamicState.ChunkMask", offsetof(MGPDynamicState, ChunkMask), sizeof(((MGPDynamicState*)nullptr)->ChunkMask)},
{"MGPDynamicState.Version", offsetof(MGPDynamicState, Version), sizeof(((MGPDynamicState*)nullptr)->Version)},
{"MGPDynamicState.Blob", offsetof(MGPDynamicState, Blob), sizeof(((MGPDynamicState*)nullptr)->Blob)},
{"MGPVertexElements", 0, sizeof(MGPVertexElements)},
{"MGPVertexElements.Cso", offsetof(MGPVertexElements, Cso), sizeof(((MGPVertexElements*)nullptr)->Cso)},
{"MGPVertexElements.AttributeCount", offsetof(MGPVertexElements, AttributeCount), sizeof(((MGPVertexElements*)nullptr)->AttributeCount)},
{"MGPVertexElements.BindingPointCount", offsetof(MGPVertexElements, BindingPointCount), sizeof(((MGPVertexElements*)nullptr)->BindingPointCount)},
{"MGPVertexElements.Blob", offsetof(MGPVertexElements, Blob), sizeof(((MGPVertexElements*)nullptr)->Blob)},
{"MGPSamplerDesc", 0, sizeof(MGPSamplerDesc)},
{"MGPSamplerDesc.Cso", offsetof(MGPSamplerDesc, Cso), sizeof(((MGPSamplerDesc*)nullptr)->Cso)},
{"MGPSamplerDesc.Parameters", offsetof(MGPSamplerDesc, Parameters), sizeof(((MGPSamplerDesc*)nullptr)->Parameters)},
{"MGPSamplerView", 0, sizeof(MGPSamplerView)},
{"MGPSamplerView.Cso", offsetof(MGPSamplerView, Cso), sizeof(((MGPSamplerView*)nullptr)->Cso)},
{"MGPSamplerView.Texture", offsetof(MGPSamplerView, Texture), sizeof(((MGPSamplerView*)nullptr)->Texture)},
{"MGPSamplerView.InternalFormat", offsetof(MGPSamplerView, InternalFormat), sizeof(((MGPSamplerView*)nullptr)->InternalFormat)},
{"MGPSamplerView.Target", offsetof(MGPSamplerView, Target), sizeof(((MGPSamplerView*)nullptr)->Target)},
{"MGPSamplerView.MinLevel", offsetof(MGPSamplerView, MinLevel), sizeof(((MGPSamplerView*)nullptr)->MinLevel)},
{"MGPSamplerView.NumLevels", offsetof(MGPSamplerView, NumLevels), sizeof(((MGPSamplerView*)nullptr)->NumLevels)},
{"MGPSamplerView.MinLayer", offsetof(MGPSamplerView, MinLayer), sizeof(((MGPSamplerView*)nullptr)->MinLayer)},
{"MGPSamplerView.NumLayers", offsetof(MGPSamplerView, NumLayers), sizeof(((MGPSamplerView*)nullptr)->NumLayers)},
{"MGPSamplerView.Samples", offsetof(MGPSamplerView, Samples), sizeof(((MGPSamplerView*)nullptr)->Samples)},
{"MGPSamplerView.FixedSampleLocations", offsetof(MGPSamplerView, FixedSampleLocations), sizeof(((MGPSamplerView*)nullptr)->FixedSampleLocations)},
{"MGPTextureParams", 0, sizeof(MGPTextureParams)},
{"MGPTextureParams.Res", offsetof(MGPTextureParams, Res), sizeof(((MGPTextureParams*)nullptr)->Res)},
{"MGPTextureParams.BuiltinSampler", offsetof(MGPTextureParams, BuiltinSampler), sizeof(((MGPTextureParams*)nullptr)->BuiltinSampler)},
{"MGPTextureParams.BaseLevel", offsetof(MGPTextureParams, BaseLevel), sizeof(((MGPTextureParams*)nullptr)->BaseLevel)},
{"MGPTextureParams.MaxLevel", offsetof(MGPTextureParams, MaxLevel), sizeof(((MGPTextureParams*)nullptr)->MaxLevel)},
{"MGPTextureParams.Swizzle", offsetof(MGPTextureParams, Swizzle), sizeof(((MGPTextureParams*)nullptr)->Swizzle)},
{"MGPTextureParams.DepthStencilMode", offsetof(MGPTextureParams, DepthStencilMode), sizeof(((MGPTextureParams*)nullptr)->DepthStencilMode)},
{"MGPTextureParams.ForceResync", offsetof(MGPTextureParams, ForceResync), sizeof(((MGPTextureParams*)nullptr)->ForceResync)},
{"MGPTextureParams.SamplerResync", offsetof(MGPTextureParams, SamplerResync), sizeof(((MGPTextureParams*)nullptr)->SamplerResync)},
{"MGPTextureParams.MinLod", offsetof(MGPTextureParams, MinLod), sizeof(((MGPTextureParams*)nullptr)->MinLod)},
{"MGPTextureParams.MaxLod", offsetof(MGPTextureParams, MaxLod), sizeof(((MGPTextureParams*)nullptr)->MaxLod)},
{"MGPTextureParams.LodBias", offsetof(MGPTextureParams, LodBias), sizeof(((MGPTextureParams*)nullptr)->LodBias)},
{"MGPProgramDesc", 0, sizeof(MGPProgramDesc)},
{"MGPProgramDesc.Cso", offsetof(MGPProgramDesc, Cso), sizeof(((MGPProgramDesc*)nullptr)->Cso)},
{"MGPProgramDesc.StageMask", offsetof(MGPProgramDesc, StageMask), sizeof(((MGPProgramDesc*)nullptr)->StageMask)},
{"MGPProgramDesc.GlobalUboSize", offsetof(MGPProgramDesc, GlobalUboSize), sizeof(((MGPProgramDesc*)nullptr)->GlobalUboSize)},
{"MGPProgramDesc.ReservedNumSamplesOffset", offsetof(MGPProgramDesc, ReservedNumSamplesOffset), sizeof(((MGPProgramDesc*)nullptr)->ReservedNumSamplesOffset)},
{"MGPProgramDesc.SpirvStatus", offsetof(MGPProgramDesc, SpirvStatus), sizeof(((MGPProgramDesc*)nullptr)->SpirvStatus)},
{"MGPProgramDesc.NativeFloat64", offsetof(MGPProgramDesc, NativeFloat64), sizeof(((MGPProgramDesc*)nullptr)->NativeFloat64)},
{"MGPProgramDesc.PointSizeDemoted", offsetof(MGPProgramDesc, PointSizeDemoted), sizeof(((MGPProgramDesc*)nullptr)->PointSizeDemoted)},
{"MGPProgramDesc.EnableSpirvValidation", offsetof(MGPProgramDesc, EnableSpirvValidation), sizeof(((MGPProgramDesc*)nullptr)->EnableSpirvValidation)},
{"MGPProgramDesc.LinkStatus", offsetof(MGPProgramDesc, LinkStatus), sizeof(((MGPProgramDesc*)nullptr)->LinkStatus)},
{"MGPProgramDesc.Spirv", offsetof(MGPProgramDesc, Spirv), sizeof(((MGPProgramDesc*)nullptr)->Spirv)},
{"MGPProgramDesc.Reflection", offsetof(MGPProgramDesc, Reflection), sizeof(((MGPProgramDesc*)nullptr)->Reflection)},
{"MGPSurface", 0, sizeof(MGPSurface)},
{"MGPSurface.Res", offsetof(MGPSurface, Res), sizeof(((MGPSurface*)nullptr)->Res)},
{"MGPSurface.InternalFormat", offsetof(MGPSurface, InternalFormat), sizeof(((MGPSurface*)nullptr)->InternalFormat)},
{"MGPSurface.Kind", offsetof(MGPSurface, Kind), sizeof(((MGPSurface*)nullptr)->Kind)},
{"MGPSurface.Layered", offsetof(MGPSurface, Layered), sizeof(((MGPSurface*)nullptr)->Layered)},
{"MGPSurface.Level", offsetof(MGPSurface, Level), sizeof(((MGPSurface*)nullptr)->Level)},
{"MGPSurface.Layer", offsetof(MGPSurface, Layer), sizeof(((MGPSurface*)nullptr)->Layer)},
{"MGPSurface.UploadTarget", offsetof(MGPSurface, UploadTarget), sizeof(((MGPSurface*)nullptr)->UploadTarget)},
{"MGPSurface.TextureTarget", offsetof(MGPSurface, TextureTarget), sizeof(((MGPSurface*)nullptr)->TextureTarget)},
{"MGPFramebufferState", 0, sizeof(MGPFramebufferState)},
{"MGPFramebufferState.Fbo", offsetof(MGPFramebufferState, Fbo), sizeof(((MGPFramebufferState*)nullptr)->Fbo)},
{"MGPFramebufferState.Color", offsetof(MGPFramebufferState, Color), sizeof(((MGPFramebufferState*)nullptr)->Color)},
{"MGPFramebufferState.Depth", offsetof(MGPFramebufferState, Depth), sizeof(((MGPFramebufferState*)nullptr)->Depth)},
{"MGPFramebufferState.Stencil", offsetof(MGPFramebufferState, Stencil), sizeof(((MGPFramebufferState*)nullptr)->Stencil)},
{"MGPFramebufferState.ReadSurface", offsetof(MGPFramebufferState, ReadSurface), sizeof(((MGPFramebufferState*)nullptr)->ReadSurface)},
{"MGPFramebufferState.DrawBuffers", offsetof(MGPFramebufferState, DrawBuffers), sizeof(((MGPFramebufferState*)nullptr)->DrawBuffers)},
{"MGPFramebufferState.Width", offsetof(MGPFramebufferState, Width), sizeof(((MGPFramebufferState*)nullptr)->Width)},
{"MGPFramebufferState.Height", offsetof(MGPFramebufferState, Height), sizeof(((MGPFramebufferState*)nullptr)->Height)},
{"MGPFramebufferState.Layers", offsetof(MGPFramebufferState, Layers), sizeof(((MGPFramebufferState*)nullptr)->Layers)},
{"MGPFramebufferState.Samples", offsetof(MGPFramebufferState, Samples), sizeof(((MGPFramebufferState*)nullptr)->Samples)},
{"MGPFramebufferState.FixedSampleLocations", offsetof(MGPFramebufferState, FixedSampleLocations), sizeof(((MGPFramebufferState*)nullptr)->FixedSampleLocations)},
{"MGPFramebufferState.IsDefault", offsetof(MGPFramebufferState, IsDefault), sizeof(((MGPFramebufferState*)nullptr)->IsDefault)},
{"MGPFramebufferState.Complete", offsetof(MGPFramebufferState, Complete), sizeof(((MGPFramebufferState*)nullptr)->Complete)},
{"MGPFramebufferState.Target", offsetof(MGPFramebufferState, Target), sizeof(((MGPFramebufferState*)nullptr)->Target)},
{"MGPFramebufferState.ContentHash", offsetof(MGPFramebufferState, ContentHash), sizeof(((MGPFramebufferState*)nullptr)->ContentHash)},
{"MGPVertexBuffer", 0, sizeof(MGPVertexBuffer)},
{"MGPVertexBuffer.Res", offsetof(MGPVertexBuffer, Res), sizeof(((MGPVertexBuffer*)nullptr)->Res)},
{"MGPVertexBuffer.Offset", offsetof(MGPVertexBuffer, Offset), sizeof(((MGPVertexBuffer*)nullptr)->Offset)},
{"MGPVertexBuffer.Stride", offsetof(MGPVertexBuffer, Stride), sizeof(((MGPVertexBuffer*)nullptr)->Stride)},
{"MGPVertexBuffer.Divisor", offsetof(MGPVertexBuffer, Divisor), sizeof(((MGPVertexBuffer*)nullptr)->Divisor)},
{"MGPVertexBuffer.BindingIndex", offsetof(MGPVertexBuffer, BindingIndex), sizeof(((MGPVertexBuffer*)nullptr)->BindingIndex)},
{"MGPVertexBuffers", 0, sizeof(MGPVertexBuffers)},
{"MGPVertexBuffers.Start", offsetof(MGPVertexBuffers, Start), sizeof(((MGPVertexBuffers*)nullptr)->Start)},
{"MGPVertexBuffers.Count", offsetof(MGPVertexBuffers, Count), sizeof(((MGPVertexBuffers*)nullptr)->Count)},
{"MGPVertexBuffers.BaseInstance", offsetof(MGPVertexBuffers, BaseInstance), sizeof(((MGPVertexBuffers*)nullptr)->BaseInstance)},
{"MGPVertexBuffers.ContentHash", offsetof(MGPVertexBuffers, ContentHash), sizeof(((MGPVertexBuffers*)nullptr)->ContentHash)},
{"MGPIndexBuffer", 0, sizeof(MGPIndexBuffer)},
{"MGPIndexBuffer.Res", offsetof(MGPIndexBuffer, Res), sizeof(((MGPIndexBuffer*)nullptr)->Res)},
{"MGPIndexBuffer.Offset", offsetof(MGPIndexBuffer, Offset), sizeof(((MGPIndexBuffer*)nullptr)->Offset)},
{"MGPIndexBuffer.IndexSize", offsetof(MGPIndexBuffer, IndexSize), sizeof(((MGPIndexBuffer*)nullptr)->IndexSize)},
{"MGPIndirectBuffers", 0, sizeof(MGPIndirectBuffers)},
{"MGPIndirectBuffers.DrawIndirect", offsetof(MGPIndirectBuffers, DrawIndirect), sizeof(((MGPIndirectBuffers*)nullptr)->DrawIndirect)},
{"MGPIndirectBuffers.Parameter", offsetof(MGPIndirectBuffers, Parameter), sizeof(((MGPIndirectBuffers*)nullptr)->Parameter)},
{"MGPBoundView", 0, sizeof(MGPBoundView)},
{"MGPBoundView.View", offsetof(MGPBoundView, View), sizeof(((MGPBoundView*)nullptr)->View)},
{"MGPBoundView.Texture", offsetof(MGPBoundView, Texture), sizeof(((MGPBoundView*)nullptr)->Texture)},
{"MGPBoundView.Unit", offsetof(MGPBoundView, Unit), sizeof(((MGPBoundView*)nullptr)->Unit)},
{"MGPSamplerViews", 0, sizeof(MGPSamplerViews)},
{"MGPSamplerViews.Start", offsetof(MGPSamplerViews, Start), sizeof(((MGPSamplerViews*)nullptr)->Start)},
{"MGPSamplerViews.Count", offsetof(MGPSamplerViews, Count), sizeof(((MGPSamplerViews*)nullptr)->Count)},
{"MGPSamplerViews.ContentHash", offsetof(MGPSamplerViews, ContentHash), sizeof(((MGPSamplerViews*)nullptr)->ContentHash)},
{"MGPSamplerStates", 0, sizeof(MGPSamplerStates)},
{"MGPSamplerStates.Start", offsetof(MGPSamplerStates, Start), sizeof(((MGPSamplerStates*)nullptr)->Start)},
{"MGPSamplerStates.Count", offsetof(MGPSamplerStates, Count), sizeof(((MGPSamplerStates*)nullptr)->Count)},
{"MGPSamplerStates.ContentHash", offsetof(MGPSamplerStates, ContentHash), sizeof(((MGPSamplerStates*)nullptr)->ContentHash)},
{"MGPImageView", 0, sizeof(MGPImageView)},
{"MGPImageView.Res", offsetof(MGPImageView, Res), sizeof(((MGPImageView*)nullptr)->Res)},
{"MGPImageView.Unit", offsetof(MGPImageView, Unit), sizeof(((MGPImageView*)nullptr)->Unit)},
{"MGPImageView.InternalFormat", offsetof(MGPImageView, InternalFormat), sizeof(((MGPImageView*)nullptr)->InternalFormat)},
{"MGPImageView.Layer", offsetof(MGPImageView, Layer), sizeof(((MGPImageView*)nullptr)->Layer)},
{"MGPImageView.Level", offsetof(MGPImageView, Level), sizeof(((MGPImageView*)nullptr)->Level)},
{"MGPImageView.Layered", offsetof(MGPImageView, Layered), sizeof(((MGPImageView*)nullptr)->Layered)},
{"MGPImageView.Access", offsetof(MGPImageView, Access), sizeof(((MGPImageView*)nullptr)->Access)},
{"MGPShaderImages", 0, sizeof(MGPShaderImages)},
{"MGPShaderImages.Start", offsetof(MGPShaderImages, Start), sizeof(((MGPShaderImages*)nullptr)->Start)},
{"MGPShaderImages.Count", offsetof(MGPShaderImages, Count), sizeof(((MGPShaderImages*)nullptr)->Count)},
{"MGPShaderImages.ContentHash", offsetof(MGPShaderImages, ContentHash), sizeof(((MGPShaderImages*)nullptr)->ContentHash)},
{"MGPBufferRange", 0, sizeof(MGPBufferRange)},
{"MGPBufferRange.Res", offsetof(MGPBufferRange, Res), sizeof(((MGPBufferRange*)nullptr)->Res)},
{"MGPBufferRange.Offset", offsetof(MGPBufferRange, Offset), sizeof(((MGPBufferRange*)nullptr)->Offset)},
{"MGPBufferRange.Size", offsetof(MGPBufferRange, Size), sizeof(((MGPBufferRange*)nullptr)->Size)},
{"MGPShaderBuffers", 0, sizeof(MGPShaderBuffers)},
{"MGPShaderBuffers.Class", offsetof(MGPShaderBuffers, Class), sizeof(((MGPShaderBuffers*)nullptr)->Class)},
{"MGPShaderBuffers.Start", offsetof(MGPShaderBuffers, Start), sizeof(((MGPShaderBuffers*)nullptr)->Start)},
{"MGPShaderBuffers.Count", offsetof(MGPShaderBuffers, Count), sizeof(((MGPShaderBuffers*)nullptr)->Count)},
{"MGPShaderBuffers.WritableMask", offsetof(MGPShaderBuffers, WritableMask), sizeof(((MGPShaderBuffers*)nullptr)->WritableMask)},
{"MGPShaderBuffers.HostSpanCount", offsetof(MGPShaderBuffers, HostSpanCount), sizeof(((MGPShaderBuffers*)nullptr)->HostSpanCount)},
{"MGPShaderBuffers.ContentHash", offsetof(MGPShaderBuffers, ContentHash), sizeof(((MGPShaderBuffers*)nullptr)->ContentHash)},
{"MGPStreamOutputTargets", 0, sizeof(MGPStreamOutputTargets)},
{"MGPStreamOutputTargets.Count", offsetof(MGPStreamOutputTargets, Count), sizeof(((MGPStreamOutputTargets*)nullptr)->Count)},
{"MGPStreamOutputTargets.Generation", offsetof(MGPStreamOutputTargets, Generation), sizeof(((MGPStreamOutputTargets*)nullptr)->Generation)},
{"MGPStreamOutputTargets.ContentHash", offsetof(MGPStreamOutputTargets, ContentHash), sizeof(((MGPStreamOutputTargets*)nullptr)->ContentHash)},
{"MGPGlobalConstants", 0, sizeof(MGPGlobalConstants)},
{"MGPGlobalConstants.ShaderCso", offsetof(MGPGlobalConstants, ShaderCso), sizeof(((MGPGlobalConstants*)nullptr)->ShaderCso)},
{"MGPGlobalConstants.Version", offsetof(MGPGlobalConstants, Version), sizeof(((MGPGlobalConstants*)nullptr)->Version)},
{"MGPGlobalConstants.Blob", offsetof(MGPGlobalConstants, Blob), sizeof(((MGPGlobalConstants*)nullptr)->Blob)},
{"MGPAttribValue", 0, sizeof(MGPAttribValue)},
{"MGPAttribValue.Location", offsetof(MGPAttribValue, Location), sizeof(((MGPAttribValue*)nullptr)->Location)},
{"MGPAttribValue.ValueClass", offsetof(MGPAttribValue, ValueClass), sizeof(((MGPAttribValue*)nullptr)->ValueClass)},
{"MGPAttribValue.FloatView", offsetof(MGPAttribValue, FloatView), sizeof(((MGPAttribValue*)nullptr)->FloatView)},
{"MGPAttribValue.IntView", offsetof(MGPAttribValue, IntView), sizeof(((MGPAttribValue*)nullptr)->IntView)},
{"MGPAttribValue.UintView", offsetof(MGPAttribValue, UintView), sizeof(((MGPAttribValue*)nullptr)->UintView)},
{"MGPVertexAttribDefaults", 0, sizeof(MGPVertexAttribDefaults)},
{"MGPVertexAttribDefaults.Mask", offsetof(MGPVertexAttribDefaults, Mask), sizeof(((MGPVertexAttribDefaults*)nullptr)->Mask)},
{"MGPVertexAttribDefaults.Count", offsetof(MGPVertexAttribDefaults, Count), sizeof(((MGPVertexAttribDefaults*)nullptr)->Count)},
{"MGPPixelPackState", 0, sizeof(MGPPixelPackState)},
{"MGPPixelPackState.Pack", offsetof(MGPPixelPackState, Pack), sizeof(((MGPPixelPackState*)nullptr)->Pack)},
{"MGPPatchState", 0, sizeof(MGPPatchState)},
{"MGPPatchState.Vertices", offsetof(MGPPatchState, Vertices), sizeof(((MGPPatchState*)nullptr)->Vertices)},
{"MGPPatchState.Outer", offsetof(MGPPatchState, Outer), sizeof(((MGPPatchState*)nullptr)->Outer)},
{"MGPPatchState.Inner", offsetof(MGPPatchState, Inner), sizeof(((MGPPatchState*)nullptr)->Inner)},
{"ResidualValueBlock", 0, sizeof(ResidualValueBlock)},
{"ResidualValueBlock.CapabilityBits", offsetof(ResidualValueBlock, CapabilityBits), sizeof(((ResidualValueBlock*)nullptr)->CapabilityBits)},
{"MGPResidualValueState", 0, sizeof(MGPResidualValueState)},
{"MGPResidualValueState.Version", offsetof(MGPResidualValueState, Version), sizeof(((MGPResidualValueState*)nullptr)->Version)},
{"MGPResidualValueState.Blob", offsetof(MGPResidualValueState, Blob), sizeof(((MGPResidualValueState*)nullptr)->Blob)},
{"MGPSubRegion", 0, sizeof(MGPSubRegion)},
{"MGPSubRegion.X", offsetof(MGPSubRegion, X), sizeof(((MGPSubRegion*)nullptr)->X)},
{"MGPSubRegion.Y", offsetof(MGPSubRegion, Y), sizeof(((MGPSubRegion*)nullptr)->Y)},
{"MGPSubRegion.Z", offsetof(MGPSubRegion, Z), sizeof(((MGPSubRegion*)nullptr)->Z)},
{"MGPSubRegion.W", offsetof(MGPSubRegion, W), sizeof(((MGPSubRegion*)nullptr)->W)},
{"MGPSubRegion.H", offsetof(MGPSubRegion, H), sizeof(((MGPSubRegion*)nullptr)->H)},
{"MGPSubRegion.D", offsetof(MGPSubRegion, D), sizeof(((MGPSubRegion*)nullptr)->D)},
{"MGPSubRegion.SrcOffset", offsetof(MGPSubRegion, SrcOffset), sizeof(((MGPSubRegion*)nullptr)->SrcOffset)},
{"MGPSubRegion.SrcRowStride", offsetof(MGPSubRegion, SrcRowStride), sizeof(((MGPSubRegion*)nullptr)->SrcRowStride)},
{"MGPSubRegion.SrcSliceStride", offsetof(MGPSubRegion, SrcSliceStride), sizeof(((MGPSubRegion*)nullptr)->SrcSliceStride)},
{"MGPSubData", 0, sizeof(MGPSubData)},
{"MGPSubData.Res", offsetof(MGPSubData, Res), sizeof(((MGPSubData*)nullptr)->Res)},
{"MGPSubData.Target", offsetof(MGPSubData, Target), sizeof(((MGPSubData*)nullptr)->Target)},
{"MGPSubData.Level", offsetof(MGPSubData, Level), sizeof(((MGPSubData*)nullptr)->Level)},
{"MGPSubData.SourceIsVerbatimLevelShadow", offsetof(MGPSubData, SourceIsVerbatimLevelShadow), sizeof(((MGPSubData*)nullptr)->SourceIsVerbatimLevelShadow)},
{"MGPSubData.HasLiveHostWrites", offsetof(MGPSubData, HasLiveHostWrites), sizeof(((MGPSubData*)nullptr)->HasLiveHostWrites)},
{"MGPSubData.UnionBox", offsetof(MGPSubData, UnionBox), sizeof(((MGPSubData*)nullptr)->UnionBox)},
{"MGPSubData.RegionCount", offsetof(MGPSubData, RegionCount), sizeof(((MGPSubData*)nullptr)->RegionCount)},
{"MGPSubData.Blob", offsetof(MGPSubData, Blob), sizeof(((MGPSubData*)nullptr)->Blob)},
{"MGPSubData.LevelWidth", offsetof(MGPSubData, LevelWidth), sizeof(((MGPSubData*)nullptr)->LevelWidth)},
{"MGPSubData.LevelHeight", offsetof(MGPSubData, LevelHeight), sizeof(((MGPSubData*)nullptr)->LevelHeight)},
{"MGPSubData.LevelDepth", offsetof(MGPSubData, LevelDepth), sizeof(((MGPSubData*)nullptr)->LevelDepth)},
{"MGPSubData.LevelPad", offsetof(MGPSubData, LevelPad), sizeof(((MGPSubData*)nullptr)->LevelPad)},
{"MGPSubDataComplete", 0, sizeof(MGPSubDataComplete)},
{"MGPSubDataComplete.Res", offsetof(MGPSubDataComplete, Res), sizeof(((MGPSubDataComplete*)nullptr)->Res)},
{"MGPSubDataComplete.Target", offsetof(MGPSubDataComplete, Target), sizeof(((MGPSubDataComplete*)nullptr)->Target)},
{"MGPSubDataComplete.FirstLevel", offsetof(MGPSubDataComplete, FirstLevel), sizeof(((MGPSubDataComplete*)nullptr)->FirstLevel)},
{"MGPSubDataComplete.LevelCount", offsetof(MGPSubDataComplete, LevelCount), sizeof(((MGPSubDataComplete*)nullptr)->LevelCount)},
{"MGPSubDataComplete.PullSerial", offsetof(MGPSubDataComplete, PullSerial), sizeof(((MGPSubDataComplete*)nullptr)->PullSerial)},
{"MGPFlushRange", 0, sizeof(MGPFlushRange)},
{"MGPFlushRange.Res", offsetof(MGPFlushRange, Res), sizeof(((MGPFlushRange*)nullptr)->Res)},
{"MGPFlushRange.Offset", offsetof(MGPFlushRange, Offset), sizeof(((MGPFlushRange*)nullptr)->Offset)},
{"MGPFlushRange.Size", offsetof(MGPFlushRange, Size), sizeof(((MGPFlushRange*)nullptr)->Size)},
{"MGPFlushRange.AccessFlags", offsetof(MGPFlushRange, AccessFlags), sizeof(((MGPFlushRange*)nullptr)->AccessFlags)},
{"MGPReadback", 0, sizeof(MGPReadback)},
{"MGPReadback.Res", offsetof(MGPReadback, Res), sizeof(((MGPReadback*)nullptr)->Res)},
{"MGPReadback.Offset", offsetof(MGPReadback, Offset), sizeof(((MGPReadback*)nullptr)->Offset)},
{"MGPReadback.Size", offsetof(MGPReadback, Size), sizeof(((MGPReadback*)nullptr)->Size)},
{"MGPCopyRegion", 0, sizeof(MGPCopyRegion)},
{"MGPCopyRegion.Src", offsetof(MGPCopyRegion, Src), sizeof(((MGPCopyRegion*)nullptr)->Src)},
{"MGPCopyRegion.Dst", offsetof(MGPCopyRegion, Dst), sizeof(((MGPCopyRegion*)nullptr)->Dst)},
{"MGPCopyRegion.SrcBox", offsetof(MGPCopyRegion, SrcBox), sizeof(((MGPCopyRegion*)nullptr)->SrcBox)},
{"MGPCopyRegion.DstX", offsetof(MGPCopyRegion, DstX), sizeof(((MGPCopyRegion*)nullptr)->DstX)},
{"MGPCopyRegion.DstY", offsetof(MGPCopyRegion, DstY), sizeof(((MGPCopyRegion*)nullptr)->DstY)},
{"MGPCopyRegion.DstZ", offsetof(MGPCopyRegion, DstZ), sizeof(((MGPCopyRegion*)nullptr)->DstZ)},
{"MGPCopyRegion.SrcTarget", offsetof(MGPCopyRegion, SrcTarget), sizeof(((MGPCopyRegion*)nullptr)->SrcTarget)},
{"MGPCopyRegion.DstTarget", offsetof(MGPCopyRegion, DstTarget), sizeof(((MGPCopyRegion*)nullptr)->DstTarget)},
{"MGPCopyRegion.SrcLevel", offsetof(MGPCopyRegion, SrcLevel), sizeof(((MGPCopyRegion*)nullptr)->SrcLevel)},
{"MGPCopyRegion.DstLevel", offsetof(MGPCopyRegion, DstLevel), sizeof(((MGPCopyRegion*)nullptr)->DstLevel)},
{"MGPCopyRegion.SrcGlName", offsetof(MGPCopyRegion, SrcGlName), sizeof(((MGPCopyRegion*)nullptr)->SrcGlName)},
{"MGPCopyRegion.DstGlName", offsetof(MGPCopyRegion, DstGlName), sizeof(((MGPCopyRegion*)nullptr)->DstGlName)},
{"MGPBlit", 0, sizeof(MGPBlit)},
{"MGPBlit.ReadFbo", offsetof(MGPBlit, ReadFbo), sizeof(((MGPBlit*)nullptr)->ReadFbo)},
{"MGPBlit.DrawFbo", offsetof(MGPBlit, DrawFbo), sizeof(((MGPBlit*)nullptr)->DrawFbo)},
{"MGPBlit.SrcX0", offsetof(MGPBlit, SrcX0), sizeof(((MGPBlit*)nullptr)->SrcX0)},
{"MGPBlit.SrcY0", offsetof(MGPBlit, SrcY0), sizeof(((MGPBlit*)nullptr)->SrcY0)},
{"MGPBlit.SrcX1", offsetof(MGPBlit, SrcX1), sizeof(((MGPBlit*)nullptr)->SrcX1)},
{"MGPBlit.SrcY1", offsetof(MGPBlit, SrcY1), sizeof(((MGPBlit*)nullptr)->SrcY1)},
{"MGPBlit.DstX0", offsetof(MGPBlit, DstX0), sizeof(((MGPBlit*)nullptr)->DstX0)},
{"MGPBlit.DstY0", offsetof(MGPBlit, DstY0), sizeof(((MGPBlit*)nullptr)->DstY0)},
{"MGPBlit.DstX1", offsetof(MGPBlit, DstX1), sizeof(((MGPBlit*)nullptr)->DstX1)},
{"MGPBlit.DstY1", offsetof(MGPBlit, DstY1), sizeof(((MGPBlit*)nullptr)->DstY1)},
{"MGPBlit.Mask", offsetof(MGPBlit, Mask), sizeof(((MGPBlit*)nullptr)->Mask)},
{"MGPBlit.Filter", offsetof(MGPBlit, Filter), sizeof(((MGPBlit*)nullptr)->Filter)},
{"MGPClear", 0, sizeof(MGPClear)},
{"MGPClear.Fbo", offsetof(MGPClear, Fbo), sizeof(((MGPClear*)nullptr)->Fbo)},
{"MGPClear.Kind", offsetof(MGPClear, Kind), sizeof(((MGPClear*)nullptr)->Kind)},
{"MGPClear.DrawBufferIndex", offsetof(MGPClear, DrawBufferIndex), sizeof(((MGPClear*)nullptr)->DrawBufferIndex)},
{"MGPClear.BufferMask", offsetof(MGPClear, BufferMask), sizeof(((MGPClear*)nullptr)->BufferMask)},
{"MGPClear.ValueClass", offsetof(MGPClear, ValueClass), sizeof(((MGPClear*)nullptr)->ValueClass)},
{"MGPClear.ColorValue", offsetof(MGPClear, ColorValue), sizeof(((MGPClear*)nullptr)->ColorValue)},
{"MGPClear.DepthValue", offsetof(MGPClear, DepthValue), sizeof(((MGPClear*)nullptr)->DepthValue)},
{"MGPClear.StencilValue", offsetof(MGPClear, StencilValue), sizeof(((MGPClear*)nullptr)->StencilValue)},
{"MGPMipPlan", 0, sizeof(MGPMipPlan)},
{"MGPMipPlan.Res", offsetof(MGPMipPlan, Res), sizeof(((MGPMipPlan*)nullptr)->Res)},
{"MGPMipPlan.Target", offsetof(MGPMipPlan, Target), sizeof(((MGPMipPlan*)nullptr)->Target)},
{"MGPMipPlan.BaseLevel", offsetof(MGPMipPlan, BaseLevel), sizeof(((MGPMipPlan*)nullptr)->BaseLevel)},
{"MGPMipPlan.LevelCount", offsetof(MGPMipPlan, LevelCount), sizeof(((MGPMipPlan*)nullptr)->LevelCount)},
{"MGPReadbackInfo", 0, sizeof(MGPReadbackInfo)},
{"MGPReadbackInfo.Res", offsetof(MGPReadbackInfo, Res), sizeof(((MGPReadbackInfo*)nullptr)->Res)},
{"MGPReadbackInfo.Box", offsetof(MGPReadbackInfo, Box), sizeof(((MGPReadbackInfo*)nullptr)->Box)},
{"MGPReadbackInfo.Format", offsetof(MGPReadbackInfo, Format), sizeof(((MGPReadbackInfo*)nullptr)->Format)},
{"MGPReadbackInfo.Type", offsetof(MGPReadbackInfo, Type), sizeof(((MGPReadbackInfo*)nullptr)->Type)},
{"MGPReadbackInfo.Target", offsetof(MGPReadbackInfo, Target), sizeof(((MGPReadbackInfo*)nullptr)->Target)},
{"MGPReadbackInfo.Level", offsetof(MGPReadbackInfo, Level), sizeof(((MGPReadbackInfo*)nullptr)->Level)},
{"MGPReadbackInfo.DstOffset", offsetof(MGPReadbackInfo, DstOffset), sizeof(((MGPReadbackInfo*)nullptr)->DstOffset)},
{"MGPReadbackInfo.DstSize", offsetof(MGPReadbackInfo, DstSize), sizeof(((MGPReadbackInfo*)nullptr)->DstSize)},
{"MGPDrawInfo", 0, sizeof(MGPDrawInfo)},
{"MGPDrawInfo.Mode", offsetof(MGPDrawInfo, Mode), sizeof(((MGPDrawInfo*)nullptr)->Mode)},
{"MGPDrawInfo.IndexSize", offsetof(MGPDrawInfo, IndexSize), sizeof(((MGPDrawInfo*)nullptr)->IndexSize)},
{"MGPDrawInfo.Flags", offsetof(MGPDrawInfo, Flags), sizeof(((MGPDrawInfo*)nullptr)->Flags)},
{"MGPDrawInfo.InstanceCount", offsetof(MGPDrawInfo, InstanceCount), sizeof(((MGPDrawInfo*)nullptr)->InstanceCount)},
{"MGPDrawInfo.StartInstance", offsetof(MGPDrawInfo, StartInstance), sizeof(((MGPDrawInfo*)nullptr)->StartInstance)},
{"MGPDrawInfo.RestartIndex", offsetof(MGPDrawInfo, RestartIndex), sizeof(((MGPDrawInfo*)nullptr)->RestartIndex)},
{"MGPDrawInfo.DrawIdOffset", offsetof(MGPDrawInfo, DrawIdOffset), sizeof(((MGPDrawInfo*)nullptr)->DrawIdOffset)},
{"MGPDrawInfo.IndexResource", offsetof(MGPDrawInfo, IndexResource), sizeof(((MGPDrawInfo*)nullptr)->IndexResource)},
{"MGPDrawInfo.MinIndex", offsetof(MGPDrawInfo, MinIndex), sizeof(((MGPDrawInfo*)nullptr)->MinIndex)},
{"MGPDrawInfo.MaxIndex", offsetof(MGPDrawInfo, MaxIndex), sizeof(((MGPDrawInfo*)nullptr)->MaxIndex)},
{"MGPDrawInfo.XfbCpuCapturedVertices", offsetof(MGPDrawInfo, XfbCpuCapturedVertices), sizeof(((MGPDrawInfo*)nullptr)->XfbCpuCapturedVertices)},
{"MGPDrawInfo.NumDraws", offsetof(MGPDrawInfo, NumDraws), sizeof(((MGPDrawInfo*)nullptr)->NumDraws)},
{"MGPDrawRange", 0, sizeof(MGPDrawRange)},
{"MGPDrawRange.Start", offsetof(MGPDrawRange, Start), sizeof(((MGPDrawRange*)nullptr)->Start)},
{"MGPDrawRange.Count", offsetof(MGPDrawRange, Count), sizeof(((MGPDrawRange*)nullptr)->Count)},
{"MGPDrawRange.IndexBias", offsetof(MGPDrawRange, IndexBias), sizeof(((MGPDrawRange*)nullptr)->IndexBias)},
{"MGPDrawIndirect", 0, sizeof(MGPDrawIndirect)},
{"MGPDrawIndirect.Buffer", offsetof(MGPDrawIndirect, Buffer), sizeof(((MGPDrawIndirect*)nullptr)->Buffer)},
{"MGPDrawIndirect.ParameterBuffer", offsetof(MGPDrawIndirect, ParameterBuffer), sizeof(((MGPDrawIndirect*)nullptr)->ParameterBuffer)},
{"MGPDrawIndirect.Offset", offsetof(MGPDrawIndirect, Offset), sizeof(((MGPDrawIndirect*)nullptr)->Offset)},
{"MGPDrawIndirect.ParameterOffset", offsetof(MGPDrawIndirect, ParameterOffset), sizeof(((MGPDrawIndirect*)nullptr)->ParameterOffset)},
{"MGPDrawIndirect.Stride", offsetof(MGPDrawIndirect, Stride), sizeof(((MGPDrawIndirect*)nullptr)->Stride)},
{"MGPDrawIndirect.DrawCount", offsetof(MGPDrawIndirect, DrawCount), sizeof(((MGPDrawIndirect*)nullptr)->DrawCount)},
{"MGPGridInfo", 0, sizeof(MGPGridInfo)},
{"MGPGridInfo.GridX", offsetof(MGPGridInfo, GridX), sizeof(((MGPGridInfo*)nullptr)->GridX)},
{"MGPGridInfo.GridY", offsetof(MGPGridInfo, GridY), sizeof(((MGPGridInfo*)nullptr)->GridY)},
{"MGPGridInfo.GridZ", offsetof(MGPGridInfo, GridZ), sizeof(((MGPGridInfo*)nullptr)->GridZ)},
{"MGPGridInfo.BlockX", offsetof(MGPGridInfo, BlockX), sizeof(((MGPGridInfo*)nullptr)->BlockX)},
{"MGPGridInfo.BlockY", offsetof(MGPGridInfo, BlockY), sizeof(((MGPGridInfo*)nullptr)->BlockY)},
{"MGPGridInfo.BlockZ", offsetof(MGPGridInfo, BlockZ), sizeof(((MGPGridInfo*)nullptr)->BlockZ)},
{"MGPGridInfo.IndirectBuffer", offsetof(MGPGridInfo, IndirectBuffer), sizeof(((MGPGridInfo*)nullptr)->IndirectBuffer)},
{"MGPGridInfo.IndirectOffset", offsetof(MGPGridInfo, IndirectOffset), sizeof(((MGPGridInfo*)nullptr)->IndirectOffset)},
{"MGPGridInfo.IsIndirect", offsetof(MGPGridInfo, IsIndirect), sizeof(((MGPGridInfo*)nullptr)->IsIndirect)},
{"MGPMemoryBarrier", 0, sizeof(MGPMemoryBarrier)},
{"MGPMemoryBarrier.Bits", offsetof(MGPMemoryBarrier, Bits), sizeof(((MGPMemoryBarrier*)nullptr)->Bits)},
{"MGPMemoryBarrier.ByRegion", offsetof(MGPMemoryBarrier, ByRegion), sizeof(((MGPMemoryBarrier*)nullptr)->ByRegion)},
{"MGPStreamOutputBegin", 0, sizeof(MGPStreamOutputBegin)},
{"MGPStreamOutputBegin.PrimitiveMode", offsetof(MGPStreamOutputBegin, PrimitiveMode), sizeof(((MGPStreamOutputBegin*)nullptr)->PrimitiveMode)},
{"MGPStreamOutputBegin.CaptureProgram", offsetof(MGPStreamOutputBegin, CaptureProgram), sizeof(((MGPStreamOutputBegin*)nullptr)->CaptureProgram)},
{"MGPStreamOutputBegin.LifetimeId", offsetof(MGPStreamOutputBegin, LifetimeId), sizeof(((MGPStreamOutputBegin*)nullptr)->LifetimeId)},
{"MGPStreamOutputBegin.Targets", offsetof(MGPStreamOutputBegin, Targets), sizeof(((MGPStreamOutputBegin*)nullptr)->Targets)},
{"MGPXfbAccounting", 0, sizeof(MGPXfbAccounting)},
{"MGPXfbAccounting.CapturedVertices", offsetof(MGPXfbAccounting, CapturedVertices), sizeof(((MGPXfbAccounting*)nullptr)->CapturedVertices)},
{"MGPXfbAccounting.PrimitivesWritten", offsetof(MGPXfbAccounting, PrimitivesWritten), sizeof(((MGPXfbAccounting*)nullptr)->PrimitivesWritten)},
{"MGPXfbAccounting.PrimitiveMode", offsetof(MGPXfbAccounting, PrimitiveMode), sizeof(((MGPXfbAccounting*)nullptr)->PrimitiveMode)},
{"MGPStreamOutputControl", 0, sizeof(MGPStreamOutputControl)},
{"MGPStreamOutputControl.Reserved", offsetof(MGPStreamOutputControl, Reserved), sizeof(((MGPStreamOutputControl*)nullptr)->Reserved)},
{"MGPFlush", 0, sizeof(MGPFlush)},
{"MGPFlush.Flags", offsetof(MGPFlush, Flags), sizeof(((MGPFlush*)nullptr)->Flags)},
{"MGPPresent", 0, sizeof(MGPPresent)},
{"MGPPresent.FrameSerial", offsetof(MGPPresent, FrameSerial), sizeof(((MGPPresent*)nullptr)->FrameSerial)},
{"MGPSwapInterval", 0, sizeof(MGPSwapInterval)},
{"MGPSwapInterval.Interval", offsetof(MGPSwapInterval, Interval), sizeof(((MGPSwapInterval*)nullptr)->Interval)},
{"MGPSurfaceInfo", 0, sizeof(MGPSurfaceInfo)},
{"MGPSurfaceInfo.Width", offsetof(MGPSurfaceInfo, Width), sizeof(((MGPSurfaceInfo*)nullptr)->Width)},
{"MGPSurfaceInfo.Height", offsetof(MGPSurfaceInfo, Height), sizeof(((MGPSurfaceInfo*)nullptr)->Height)},
{"MGPSurfaceInfo.InternalFormat", offsetof(MGPSurfaceInfo, InternalFormat), sizeof(((MGPSurfaceInfo*)nullptr)->InternalFormat)},
{"MGPSurfaceInfo.Samples", offsetof(MGPSurfaceInfo, Samples), sizeof(((MGPSurfaceInfo*)nullptr)->Samples)},
{"MGPSurfaceInfo.Layers", offsetof(MGPSurfaceInfo, Layers), sizeof(((MGPSurfaceInfo*)nullptr)->Layers)},
{"MGPSurfaceInfo.IsDefault", offsetof(MGPSurfaceInfo, IsDefault), sizeof(((MGPSurfaceInfo*)nullptr)->IsDefault)},
{"MGPImageBind", 0, sizeof(MGPImageBind)},
{"MGPImageBind.Res", offsetof(MGPImageBind, Res), sizeof(((MGPImageBind*)nullptr)->Res)},
{"MGPImageBind.Unit", offsetof(MGPImageBind, Unit), sizeof(((MGPImageBind*)nullptr)->Unit)},
{"MGPImageBind.GlName", offsetof(MGPImageBind, GlName), sizeof(((MGPImageBind*)nullptr)->GlName)},
{"MGPImageBind.Level", offsetof(MGPImageBind, Level), sizeof(((MGPImageBind*)nullptr)->Level)},
{"MGPImageBind.Layer", offsetof(MGPImageBind, Layer), sizeof(((MGPImageBind*)nullptr)->Layer)},
{"MGPImageBind.Access", offsetof(MGPImageBind, Access), sizeof(((MGPImageBind*)nullptr)->Access)},
{"MGPImageBind.Format", offsetof(MGPImageBind, Format), sizeof(((MGPImageBind*)nullptr)->Format)},
{"MGPImageBind.Layered", offsetof(MGPImageBind, Layered), sizeof(((MGPImageBind*)nullptr)->Layered)},
{"MGPPatchParameter", 0, sizeof(MGPPatchParameter)},
{"MGPPatchParameter.Pname", offsetof(MGPPatchParameter, Pname), sizeof(((MGPPatchParameter*)nullptr)->Pname)},
{"MGPPatchParameter.Value", offsetof(MGPPatchParameter, Value), sizeof(((MGPPatchParameter*)nullptr)->Value)},
{"MGPStreamOutputBind", 0, sizeof(MGPStreamOutputBind)},
{"MGPStreamOutputBind.GlName", offsetof(MGPStreamOutputBind, GlName), sizeof(((MGPStreamOutputBind*)nullptr)->GlName)},
{"MGPStreamOutputBind.LifetimeId", offsetof(MGPStreamOutputBind, LifetimeId), sizeof(((MGPStreamOutputBind*)nullptr)->LifetimeId)},
{"MGPStorageBlockBinding", 0, sizeof(MGPStorageBlockBinding)},
{"MGPStorageBlockBinding.ShaderCso", offsetof(MGPStorageBlockBinding, ShaderCso), sizeof(((MGPStorageBlockBinding*)nullptr)->ShaderCso)},
{"MGPStorageBlockBinding.GlName", offsetof(MGPStorageBlockBinding, GlName), sizeof(((MGPStorageBlockBinding*)nullptr)->GlName)},
{"MGPStorageBlockBinding.Binding", offsetof(MGPStorageBlockBinding, Binding), sizeof(((MGPStorageBlockBinding*)nullptr)->Binding)},
{"MGPStorageBlockBinding.Name", offsetof(MGPStorageBlockBinding, Name), sizeof(((MGPStorageBlockBinding*)nullptr)->Name)},
{"MGPCopyFromFramebuffer", 0, sizeof(MGPCopyFromFramebuffer)},
{"MGPCopyFromFramebuffer.Dst", offsetof(MGPCopyFromFramebuffer, Dst), sizeof(((MGPCopyFromFramebuffer*)nullptr)->Dst)},
{"MGPCopyFromFramebuffer.Target", offsetof(MGPCopyFromFramebuffer, Target), sizeof(((MGPCopyFromFramebuffer*)nullptr)->Target)},
{"MGPCopyFromFramebuffer.Level", offsetof(MGPCopyFromFramebuffer, Level), sizeof(((MGPCopyFromFramebuffer*)nullptr)->Level)},
{"MGPCopyFromFramebuffer.InternalFormat", offsetof(MGPCopyFromFramebuffer, InternalFormat), sizeof(((MGPCopyFromFramebuffer*)nullptr)->InternalFormat)},
{"MGPCopyFromFramebuffer.X", offsetof(MGPCopyFromFramebuffer, X), sizeof(((MGPCopyFromFramebuffer*)nullptr)->X)},
{"MGPCopyFromFramebuffer.Y", offsetof(MGPCopyFromFramebuffer, Y), sizeof(((MGPCopyFromFramebuffer*)nullptr)->Y)},
{"MGPCopyFromFramebuffer.Width", offsetof(MGPCopyFromFramebuffer, Width), sizeof(((MGPCopyFromFramebuffer*)nullptr)->Width)},
{"MGPCopyFromFramebuffer.Height", offsetof(MGPCopyFromFramebuffer, Height), sizeof(((MGPCopyFromFramebuffer*)nullptr)->Height)},
{"MGPCopyFromFramebuffer.XOffset", offsetof(MGPCopyFromFramebuffer, XOffset), sizeof(((MGPCopyFromFramebuffer*)nullptr)->XOffset)},
{"MGPCopyFromFramebuffer.YOffset", offsetof(MGPCopyFromFramebuffer, YOffset), sizeof(((MGPCopyFromFramebuffer*)nullptr)->YOffset)},
{"MGPCopyFromFramebuffer.SubImage", offsetof(MGPCopyFromFramebuffer, SubImage), sizeof(((MGPCopyFromFramebuffer*)nullptr)->SubImage)},
{"MGPApplierReset", 0, sizeof(MGPApplierReset)},
{"MGPApplierReset.ContextSerial", offsetof(MGPApplierReset, ContextSerial), sizeof(((MGPApplierReset*)nullptr)->ContextSerial)},
{"MGPContextValues", 0, sizeof(MGPContextValues)},
{"MGPContextValues.ActiveTextureUnit", offsetof(MGPContextValues, ActiveTextureUnit), sizeof(((MGPContextValues*)nullptr)->ActiveTextureUnit)},
{"MGPContextValues.MaxTouchedTextureUnit", offsetof(MGPContextValues, MaxTouchedTextureUnit), sizeof(((MGPContextValues*)nullptr)->MaxTouchedTextureUnit)},
{"MGPContextValues.TouchedBufferBindingPointCount", offsetof(MGPContextValues, TouchedBufferBindingPointCount), sizeof(((MGPContextValues*)nullptr)->TouchedBufferBindingPointCount)},
{"MGPContextValues.IsTransformFeedbackActive", offsetof(MGPContextValues, IsTransformFeedbackActive), sizeof(((MGPContextValues*)nullptr)->IsTransformFeedbackActive)},
{"MGPContextValues.IsTransformFeedbackPaused", offsetof(MGPContextValues, IsTransformFeedbackPaused), sizeof(((MGPContextValues*)nullptr)->IsTransformFeedbackPaused)},
{"MGPContextValues.TransformFeedbackGeneration", offsetof(MGPContextValues, TransformFeedbackGeneration), sizeof(((MGPContextValues*)nullptr)->TransformFeedbackGeneration)},
{"MGPContextValues.BoundTransformFeedbackLifetimeId", offsetof(MGPContextValues, BoundTransformFeedbackLifetimeId), sizeof(((MGPContextValues*)nullptr)->BoundTransformFeedbackLifetimeId)},
{"MGPContextValues.TransformFeedbackCapturedVertices", offsetof(MGPContextValues, TransformFeedbackCapturedVertices), sizeof(((MGPContextValues*)nullptr)->TransformFeedbackCapturedVertices)},
{"MGPProgramBindings", 0, sizeof(MGPProgramBindings)},
{"MGPProgramBindings.Cso", offsetof(MGPProgramBindings, Cso), sizeof(((MGPProgramBindings*)nullptr)->Cso)},
{"MGPProgramBindings.Signature", offsetof(MGPProgramBindings, Signature), sizeof(((MGPProgramBindings*)nullptr)->Signature)},
{"MGPProgramBindings.BlockBindingCount", offsetof(MGPProgramBindings, BlockBindingCount), sizeof(((MGPProgramBindings*)nullptr)->BlockBindingCount)},
{"MGPProgramBindings.SamplerUnitCount", offsetof(MGPProgramBindings, SamplerUnitCount), sizeof(((MGPProgramBindings*)nullptr)->SamplerUnitCount)},
{"MGPProgramBindings.StorageOverrideCount", offsetof(MGPProgramBindings, StorageOverrideCount), sizeof(((MGPProgramBindings*)nullptr)->StorageOverrideCount)},
{"MGPProgramSamplerUnit", 0, sizeof(MGPProgramSamplerUnit)},
{"MGPProgramSamplerUnit.Location", offsetof(MGPProgramSamplerUnit, Location), sizeof(((MGPProgramSamplerUnit*)nullptr)->Location)},
{"MGPProgramSamplerUnit.Unit", offsetof(MGPProgramSamplerUnit, Unit), sizeof(((MGPProgramSamplerUnit*)nullptr)->Unit)},
{"MGPProgramStorageOverride", 0, sizeof(MGPProgramStorageOverride)},
{"MGPProgramStorageOverride.Name", offsetof(MGPProgramStorageOverride, Name), sizeof(((MGPProgramStorageOverride*)nullptr)->Name)},
{"MGPProgramStorageOverride.Binding", offsetof(MGPProgramStorageOverride, Binding), sizeof(((MGPProgramStorageOverride*)nullptr)->Binding)},
{"RenderStateParameters", 0, sizeof(RenderStateParameters)},
{"RenderStateParameters.Viewports", offsetof(RenderStateParameters, Viewports), sizeof(((RenderStateParameters*)nullptr)->Viewports)},
{"RenderStateParameters.LineWidth", offsetof(RenderStateParameters, LineWidth), sizeof(((RenderStateParameters*)nullptr)->LineWidth)},
{"RenderStateParameters.PointSize", offsetof(RenderStateParameters, PointSize), sizeof(((RenderStateParameters*)nullptr)->PointSize)},
{"RenderStateParameters.PatchVertices", offsetof(RenderStateParameters, PatchVertices), sizeof(((RenderStateParameters*)nullptr)->PatchVertices)},
{"RenderStateParameters.PatchDefaultOuterLevel", offsetof(RenderStateParameters, PatchDefaultOuterLevel), sizeof(((RenderStateParameters*)nullptr)->PatchDefaultOuterLevel)},
{"RenderStateParameters.PatchDefaultInnerLevel", offsetof(RenderStateParameters, PatchDefaultInnerLevel), sizeof(((RenderStateParameters*)nullptr)->PatchDefaultInnerLevel)},
{"RenderStateParameters.PolygonOffsetFactor", offsetof(RenderStateParameters, PolygonOffsetFactor), sizeof(((RenderStateParameters*)nullptr)->PolygonOffsetFactor)},
{"RenderStateParameters.PolygonOffsetUnits", offsetof(RenderStateParameters, PolygonOffsetUnits), sizeof(((RenderStateParameters*)nullptr)->PolygonOffsetUnits)},
{"RenderStateParameters.PolygonOffsetClamp", offsetof(RenderStateParameters, PolygonOffsetClamp), sizeof(((RenderStateParameters*)nullptr)->PolygonOffsetClamp)},
{"RenderStateParameters.ClipOrigin", offsetof(RenderStateParameters, ClipOrigin), sizeof(((RenderStateParameters*)nullptr)->ClipOrigin)},
{"RenderStateParameters.ClipDepthMode", offsetof(RenderStateParameters, ClipDepthMode), sizeof(((RenderStateParameters*)nullptr)->ClipDepthMode)},
{"RenderStateParameters.BlendStates", offsetof(RenderStateParameters, BlendStates), sizeof(((RenderStateParameters*)nullptr)->BlendStates)},
{"RenderStateParameters.LogicOp", offsetof(RenderStateParameters, LogicOp), sizeof(((RenderStateParameters*)nullptr)->LogicOp)},
{"RenderStateParameters.DepthTestEnabled", offsetof(RenderStateParameters, DepthTestEnabled), sizeof(((RenderStateParameters*)nullptr)->DepthTestEnabled)},
{"RenderStateParameters.DepthFunc", offsetof(RenderStateParameters, DepthFunc), sizeof(((RenderStateParameters*)nullptr)->DepthFunc)},
{"RenderStateParameters.DepthMask", offsetof(RenderStateParameters, DepthMask), sizeof(((RenderStateParameters*)nullptr)->DepthMask)},
{"RenderStateParameters.ColorMasks", offsetof(RenderStateParameters, ColorMasks), sizeof(((RenderStateParameters*)nullptr)->ColorMasks)},
{"RenderStateParameters.FramebufferSrgbEnabled", offsetof(RenderStateParameters, FramebufferSrgbEnabled), sizeof(((RenderStateParameters*)nullptr)->FramebufferSrgbEnabled)},
{"RenderStateParameters.DepthClampEnabled", offsetof(RenderStateParameters, DepthClampEnabled), sizeof(((RenderStateParameters*)nullptr)->DepthClampEnabled)},
{"RenderStateParameters.TextureCubeMapSeamlessEnabled", offsetof(RenderStateParameters, TextureCubeMapSeamlessEnabled), sizeof(((RenderStateParameters*)nullptr)->TextureCubeMapSeamlessEnabled)},
{"RenderStateParameters.ClearColor", offsetof(RenderStateParameters, ClearColor), sizeof(((RenderStateParameters*)nullptr)->ClearColor)},
{"RenderStateParameters.ClearDepth", offsetof(RenderStateParameters, ClearDepth), sizeof(((RenderStateParameters*)nullptr)->ClearDepth)},
{"RenderStateParameters.ClearStencil", offsetof(RenderStateParameters, ClearStencil), sizeof(((RenderStateParameters*)nullptr)->ClearStencil)},
{"RenderStateParameters.BlendColor", offsetof(RenderStateParameters, BlendColor), sizeof(((RenderStateParameters*)nullptr)->BlendColor)},
{"RenderStateParameters.DepthRanges", offsetof(RenderStateParameters, DepthRanges), sizeof(((RenderStateParameters*)nullptr)->DepthRanges)},
{"RenderStateParameters.SampleCoverageValue", offsetof(RenderStateParameters, SampleCoverageValue), sizeof(((RenderStateParameters*)nullptr)->SampleCoverageValue)},
{"RenderStateParameters.SampleCoverageInvert", offsetof(RenderStateParameters, SampleCoverageInvert), sizeof(((RenderStateParameters*)nullptr)->SampleCoverageInvert)},
{"RenderStateParameters.SampleMaskValue", offsetof(RenderStateParameters, SampleMaskValue), sizeof(((RenderStateParameters*)nullptr)->SampleMaskValue)},
{"RenderStateParameters.MinSampleShadingValue", offsetof(RenderStateParameters, MinSampleShadingValue), sizeof(((RenderStateParameters*)nullptr)->MinSampleShadingValue)},
{"RenderStateParameters.StencilStates", offsetof(RenderStateParameters, StencilStates), sizeof(((RenderStateParameters*)nullptr)->StencilStates)},
{"RenderStateParameters.CullFaceEnabled", offsetof(RenderStateParameters, CullFaceEnabled), sizeof(((RenderStateParameters*)nullptr)->CullFaceEnabled)},
{"RenderStateParameters.CullFaceModeSetting", offsetof(RenderStateParameters, CullFaceModeSetting), sizeof(((RenderStateParameters*)nullptr)->CullFaceModeSetting)},
{"RenderStateParameters.FrontFaceModeSetting", offsetof(RenderStateParameters, FrontFaceModeSetting), sizeof(((RenderStateParameters*)nullptr)->FrontFaceModeSetting)},
{"RenderStateParameters.ProvokingVertexModeSetting", offsetof(RenderStateParameters, ProvokingVertexModeSetting), sizeof(((RenderStateParameters*)nullptr)->ProvokingVertexModeSetting)},
{"RenderStateParameters.LineSmoothHint", offsetof(RenderStateParameters, LineSmoothHint), sizeof(((RenderStateParameters*)nullptr)->LineSmoothHint)},
{"RenderStateParameters.PolygonSmoothHint", offsetof(RenderStateParameters, PolygonSmoothHint), sizeof(((RenderStateParameters*)nullptr)->PolygonSmoothHint)},
{"RenderStateParameters.TextureCompressionHint", offsetof(RenderStateParameters, TextureCompressionHint), sizeof(((RenderStateParameters*)nullptr)->TextureCompressionHint)},
{"RenderStateParameters.FragmentShaderDerivativeHint", offsetof(RenderStateParameters, FragmentShaderDerivativeHint), sizeof(((RenderStateParameters*)nullptr)->FragmentShaderDerivativeHint)},
{"RenderStateParameters.PointFadeThresholdSize", offsetof(RenderStateParameters, PointFadeThresholdSize), sizeof(((RenderStateParameters*)nullptr)->PointFadeThresholdSize)},
{"RenderStateParameters.PointSpriteCoordOrigin", offsetof(RenderStateParameters, PointSpriteCoordOrigin), sizeof(((RenderStateParameters*)nullptr)->PointSpriteCoordOrigin)},
{"RenderStateParameters.ClampReadColor", offsetof(RenderStateParameters, ClampReadColor), sizeof(((RenderStateParameters*)nullptr)->ClampReadColor)},
{"RenderStateParameters.PolygonModeFront", offsetof(RenderStateParameters, PolygonModeFront), sizeof(((RenderStateParameters*)nullptr)->PolygonModeFront)},
{"RenderStateParameters.PolygonModeBack", offsetof(RenderStateParameters, PolygonModeBack), sizeof(((RenderStateParameters*)nullptr)->PolygonModeBack)},
{"RenderStateParameters.PrimitiveRestartIndex", offsetof(RenderStateParameters, PrimitiveRestartIndex), sizeof(((RenderStateParameters*)nullptr)->PrimitiveRestartIndex)},
{"RenderStateParameters.ColorLogicOpEnabled", offsetof(RenderStateParameters, ColorLogicOpEnabled), sizeof(((RenderStateParameters*)nullptr)->ColorLogicOpEnabled)},
{"RenderStateParameters.DebugOutputEnabled", offsetof(RenderStateParameters, DebugOutputEnabled), sizeof(((RenderStateParameters*)nullptr)->DebugOutputEnabled)},
{"RenderStateParameters.DebugOutputSynchronousEnabled", offsetof(RenderStateParameters, DebugOutputSynchronousEnabled), sizeof(((RenderStateParameters*)nullptr)->DebugOutputSynchronousEnabled)},
{"RenderStateParameters.DitherEnabled", offsetof(RenderStateParameters, DitherEnabled), sizeof(((RenderStateParameters*)nullptr)->DitherEnabled)},
{"RenderStateParameters.LineSmoothEnabled", offsetof(RenderStateParameters, LineSmoothEnabled), sizeof(((RenderStateParameters*)nullptr)->LineSmoothEnabled)},
{"RenderStateParameters.MultisampleEnabled", offsetof(RenderStateParameters, MultisampleEnabled), sizeof(((RenderStateParameters*)nullptr)->MultisampleEnabled)},
{"RenderStateParameters.PolygonOffsetFillEnabled", offsetof(RenderStateParameters, PolygonOffsetFillEnabled), sizeof(((RenderStateParameters*)nullptr)->PolygonOffsetFillEnabled)},
{"RenderStateParameters.PolygonOffsetLineEnabled", offsetof(RenderStateParameters, PolygonOffsetLineEnabled), sizeof(((RenderStateParameters*)nullptr)->PolygonOffsetLineEnabled)},
{"RenderStateParameters.PolygonOffsetPointEnabled", offsetof(RenderStateParameters, PolygonOffsetPointEnabled), sizeof(((RenderStateParameters*)nullptr)->PolygonOffsetPointEnabled)},
{"RenderStateParameters.PolygonSmoothEnabled", offsetof(RenderStateParameters, PolygonSmoothEnabled), sizeof(((RenderStateParameters*)nullptr)->PolygonSmoothEnabled)},
{"RenderStateParameters.PrimitiveRestartEnabled", offsetof(RenderStateParameters, PrimitiveRestartEnabled), sizeof(((RenderStateParameters*)nullptr)->PrimitiveRestartEnabled)},
{"RenderStateParameters.PrimitiveRestartFixedIndexEnabled", offsetof(RenderStateParameters, PrimitiveRestartFixedIndexEnabled), sizeof(((RenderStateParameters*)nullptr)->PrimitiveRestartFixedIndexEnabled)},
{"RenderStateParameters.RasterizerDiscardEnabled", offsetof(RenderStateParameters, RasterizerDiscardEnabled), sizeof(((RenderStateParameters*)nullptr)->RasterizerDiscardEnabled)},
{"RenderStateParameters.SampleAlphaToCoverageEnabled", offsetof(RenderStateParameters, SampleAlphaToCoverageEnabled), sizeof(((RenderStateParameters*)nullptr)->SampleAlphaToCoverageEnabled)},
{"RenderStateParameters.SampleAlphaToOneEnabled", offsetof(RenderStateParameters, SampleAlphaToOneEnabled), sizeof(((RenderStateParameters*)nullptr)->SampleAlphaToOneEnabled)},
{"RenderStateParameters.SampleCoverageEnabled", offsetof(RenderStateParameters, SampleCoverageEnabled), sizeof(((RenderStateParameters*)nullptr)->SampleCoverageEnabled)},
{"RenderStateParameters.SampleMaskEnabled", offsetof(RenderStateParameters, SampleMaskEnabled), sizeof(((RenderStateParameters*)nullptr)->SampleMaskEnabled)},
{"RenderStateParameters.SampleShadingEnabled", offsetof(RenderStateParameters, SampleShadingEnabled), sizeof(((RenderStateParameters*)nullptr)->SampleShadingEnabled)},
{"RenderStateParameters.StencilTestEnabled", offsetof(RenderStateParameters, StencilTestEnabled), sizeof(((RenderStateParameters*)nullptr)->StencilTestEnabled)},
{"RenderStateParameters.ProgramPointSizeEnabled", offsetof(RenderStateParameters, ProgramPointSizeEnabled), sizeof(((RenderStateParameters*)nullptr)->ProgramPointSizeEnabled)},
{"RenderStateParameters.ScissorTestEnabledMask", offsetof(RenderStateParameters, ScissorTestEnabledMask), sizeof(((RenderStateParameters*)nullptr)->ScissorTestEnabledMask)},
{"RenderStateParameters.ScissorBoxes", offsetof(RenderStateParameters, ScissorBoxes), sizeof(((RenderStateParameters*)nullptr)->ScissorBoxes)},
{"RenderStateParameters.ScissorBoxWrittenMask", offsetof(RenderStateParameters, ScissorBoxWrittenMask), sizeof(((RenderStateParameters*)nullptr)->ScissorBoxWrittenMask)},
{"RenderStateParameters.ClipDistanceEnabledMask", offsetof(RenderStateParameters, ClipDistanceEnabledMask), sizeof(((RenderStateParameters*)nullptr)->ClipDistanceEnabledMask)},
{"PixelStoreParameters", 0, sizeof(PixelStoreParameters)},
{"PixelStoreParameters.SwapBytes", offsetof(PixelStoreParameters, SwapBytes), sizeof(((PixelStoreParameters*)nullptr)->SwapBytes)},
{"PixelStoreParameters.LSBFirst", offsetof(PixelStoreParameters, LSBFirst), sizeof(((PixelStoreParameters*)nullptr)->LSBFirst)},
{"PixelStoreParameters.RowLength", offsetof(PixelStoreParameters, RowLength), sizeof(((PixelStoreParameters*)nullptr)->RowLength)},
{"PixelStoreParameters.ImageHeight", offsetof(PixelStoreParameters, ImageHeight), sizeof(((PixelStoreParameters*)nullptr)->ImageHeight)},
{"PixelStoreParameters.SkipPixels", offsetof(PixelStoreParameters, SkipPixels), sizeof(((PixelStoreParameters*)nullptr)->SkipPixels)},
{"PixelStoreParameters.SkipRows", offsetof(PixelStoreParameters, SkipRows), sizeof(((PixelStoreParameters*)nullptr)->SkipRows)},
{"PixelStoreParameters.SkipImages", offsetof(PixelStoreParameters, SkipImages), sizeof(((PixelStoreParameters*)nullptr)->SkipImages)},
{"PixelStoreParameters.Alignment", offsetof(PixelStoreParameters, Alignment), sizeof(((PixelStoreParameters*)nullptr)->Alignment)},
{"SamplerParameters", 0, sizeof(SamplerParameters)},
{"SamplerParameters.wrapS", offsetof(SamplerParameters, wrapS), sizeof(((SamplerParameters*)nullptr)->wrapS)},
{"SamplerParameters.wrapT", offsetof(SamplerParameters, wrapT), sizeof(((SamplerParameters*)nullptr)->wrapT)},
{"SamplerParameters.wrapR", offsetof(SamplerParameters, wrapR), sizeof(((SamplerParameters*)nullptr)->wrapR)},
{"SamplerParameters.minFilter", offsetof(SamplerParameters, minFilter), sizeof(((SamplerParameters*)nullptr)->minFilter)},
{"SamplerParameters.magFilter", offsetof(SamplerParameters, magFilter), sizeof(((SamplerParameters*)nullptr)->magFilter)},
{"SamplerParameters.mipmapMode", offsetof(SamplerParameters, mipmapMode), sizeof(((SamplerParameters*)nullptr)->mipmapMode)},
{"SamplerParameters.minLod", offsetof(SamplerParameters, minLod), sizeof(((SamplerParameters*)nullptr)->minLod)},
{"SamplerParameters.maxLod", offsetof(SamplerParameters, maxLod), sizeof(((SamplerParameters*)nullptr)->maxLod)},
{"SamplerParameters.lodBias", offsetof(SamplerParameters, lodBias), sizeof(((SamplerParameters*)nullptr)->lodBias)},
{"SamplerParameters.maxAnisotropy", offsetof(SamplerParameters, maxAnisotropy), sizeof(((SamplerParameters*)nullptr)->maxAnisotropy)},
{"SamplerParameters.compareFunc", offsetof(SamplerParameters, compareFunc), sizeof(((SamplerParameters*)nullptr)->compareFunc)},
{"SamplerParameters.compareMode", offsetof(SamplerParameters, compareMode), sizeof(((SamplerParameters*)nullptr)->compareMode)},
{"SamplerParameters.borderColor", offsetof(SamplerParameters, borderColor), sizeof(((SamplerParameters*)nullptr)->borderColor)},
{"SamplerParameters.borderColorI", offsetof(SamplerParameters, borderColorI), sizeof(((SamplerParameters*)nullptr)->borderColorI)},
{"SamplerParameters.borderColorUI", offsetof(SamplerParameters, borderColorUI), sizeof(((SamplerParameters*)nullptr)->borderColorUI)},
{"SamplerParameters.borderColorForm", offsetof(SamplerParameters, borderColorForm), sizeof(((SamplerParameters*)nullptr)->borderColorForm)},
{"PerBufferBlendState", 0, sizeof(PerBufferBlendState)},
{"PerBufferBlendState.Enabled", offsetof(PerBufferBlendState, Enabled), sizeof(((PerBufferBlendState*)nullptr)->Enabled)},
{"PerBufferBlendState.SrcFactorRGB", offsetof(PerBufferBlendState, SrcFactorRGB), sizeof(((PerBufferBlendState*)nullptr)->SrcFactorRGB)},
{"PerBufferBlendState.DstFactorRGB", offsetof(PerBufferBlendState, DstFactorRGB), sizeof(((PerBufferBlendState*)nullptr)->DstFactorRGB)},
{"PerBufferBlendState.SrcFactorAlpha", offsetof(PerBufferBlendState, SrcFactorAlpha), sizeof(((PerBufferBlendState*)nullptr)->SrcFactorAlpha)},
{"PerBufferBlendState.DstFactorAlpha", offsetof(PerBufferBlendState, DstFactorAlpha), sizeof(((PerBufferBlendState*)nullptr)->DstFactorAlpha)},
{"PerBufferBlendState.ColorEquation", offsetof(PerBufferBlendState, ColorEquation), sizeof(((PerBufferBlendState*)nullptr)->ColorEquation)},
{"PerBufferBlendState.AlphaEquation", offsetof(PerBufferBlendState, AlphaEquation), sizeof(((PerBufferBlendState*)nullptr)->AlphaEquation)},
{"StencilFaceState", 0, sizeof(StencilFaceState)},
{"StencilFaceState.Func", offsetof(StencilFaceState, Func), sizeof(((StencilFaceState*)nullptr)->Func)},
{"StencilFaceState.Ref", offsetof(StencilFaceState, Ref), sizeof(((StencilFaceState*)nullptr)->Ref)},
{"StencilFaceState.ValueMask", offsetof(StencilFaceState, ValueMask), sizeof(((StencilFaceState*)nullptr)->ValueMask)},
{"StencilFaceState.WriteMask", offsetof(StencilFaceState, WriteMask), sizeof(((StencilFaceState*)nullptr)->WriteMask)},
{"StencilFaceState.FailOp", offsetof(StencilFaceState, FailOp), sizeof(((StencilFaceState*)nullptr)->FailOp)},
{"StencilFaceState.PassDepthFailOp", offsetof(StencilFaceState, PassDepthFailOp), sizeof(((StencilFaceState*)nullptr)->PassDepthFailOp)},
{"StencilFaceState.PassDepthPassOp", offsetof(StencilFaceState, PassDepthPassOp), sizeof(((StencilFaceState*)nullptr)->PassDepthPassOp)},
{"DynamicBackendParameters", 0, sizeof(DynamicBackendParameters)},
{"DynamicBackendParameters.UniformBufferOffsetAlignment", offsetof(DynamicBackendParameters, UniformBufferOffsetAlignment), sizeof(((DynamicBackendParameters*)nullptr)->UniformBufferOffsetAlignment)},
{"DynamicBackendParameters.ShaderStorageBufferOffsetAlignment", offsetof(DynamicBackendParameters, ShaderStorageBufferOffsetAlignment), sizeof(((DynamicBackendParameters*)nullptr)->ShaderStorageBufferOffsetAlignment)},
{"DynamicBackendParameters.MaxTextureMaxAnisotropy", offsetof(DynamicBackendParameters, MaxTextureMaxAnisotropy), sizeof(((DynamicBackendParameters*)nullptr)->MaxTextureMaxAnisotropy)},
{"DynamicBackendParameters.AliasedLineWidthRangeMin", offsetof(DynamicBackendParameters, AliasedLineWidthRangeMin), sizeof(((DynamicBackendParameters*)nullptr)->AliasedLineWidthRangeMin)},
{"DynamicBackendParameters.AliasedLineWidthRangeMax", offsetof(DynamicBackendParameters, AliasedLineWidthRangeMax), sizeof(((DynamicBackendParameters*)nullptr)->AliasedLineWidthRangeMax)},
{"DynamicBackendParameters.SmoothLineWidthRangeMin", offsetof(DynamicBackendParameters, SmoothLineWidthRangeMin), sizeof(((DynamicBackendParameters*)nullptr)->SmoothLineWidthRangeMin)},
{"DynamicBackendParameters.SmoothLineWidthRangeMax", offsetof(DynamicBackendParameters, SmoothLineWidthRangeMax), sizeof(((DynamicBackendParameters*)nullptr)->SmoothLineWidthRangeMax)},
{"DynamicBackendParameters.SmoothLineWidthGranularity", offsetof(DynamicBackendParameters, SmoothLineWidthGranularity), sizeof(((DynamicBackendParameters*)nullptr)->SmoothLineWidthGranularity)},
{"DynamicBackendParameters.PointSizeRangeMin", offsetof(DynamicBackendParameters, PointSizeRangeMin), sizeof(((DynamicBackendParameters*)nullptr)->PointSizeRangeMin)},
{"DynamicBackendParameters.PointSizeRangeMax", offsetof(DynamicBackendParameters, PointSizeRangeMax), sizeof(((DynamicBackendParameters*)nullptr)->PointSizeRangeMax)},
{"DynamicBackendParameters.PointSizeGranularity", offsetof(DynamicBackendParameters, PointSizeGranularity), sizeof(((DynamicBackendParameters*)nullptr)->PointSizeGranularity)},
{"DynamicBackendParameters.Max3DTextureSize", offsetof(DynamicBackendParameters, Max3DTextureSize), sizeof(((DynamicBackendParameters*)nullptr)->Max3DTextureSize)},
{"DynamicBackendParameters.MaxArrayTextureLayers", offsetof(DynamicBackendParameters, MaxArrayTextureLayers), sizeof(((DynamicBackendParameters*)nullptr)->MaxArrayTextureLayers)},
{"DynamicBackendParameters.MaxCubeMapTextureSize", offsetof(DynamicBackendParameters, MaxCubeMapTextureSize), sizeof(((DynamicBackendParameters*)nullptr)->MaxCubeMapTextureSize)},
{"DynamicBackendParameters.MaxFramebufferWidth", offsetof(DynamicBackendParameters, MaxFramebufferWidth), sizeof(((DynamicBackendParameters*)nullptr)->MaxFramebufferWidth)},
{"DynamicBackendParameters.MaxFramebufferHeight", offsetof(DynamicBackendParameters, MaxFramebufferHeight), sizeof(((DynamicBackendParameters*)nullptr)->MaxFramebufferHeight)},
{"DynamicBackendParameters.MaxFramebufferLayers", offsetof(DynamicBackendParameters, MaxFramebufferLayers), sizeof(((DynamicBackendParameters*)nullptr)->MaxFramebufferLayers)},
{"DynamicBackendParameters.MaxRenderbufferSize", offsetof(DynamicBackendParameters, MaxRenderbufferSize), sizeof(((DynamicBackendParameters*)nullptr)->MaxRenderbufferSize)},
{"DynamicBackendParameters.MaxTextureSize", offsetof(DynamicBackendParameters, MaxTextureSize), sizeof(((DynamicBackendParameters*)nullptr)->MaxTextureSize)},
{"DynamicBackendParameters.MaxColorTextureSamples", offsetof(DynamicBackendParameters, MaxColorTextureSamples), sizeof(((DynamicBackendParameters*)nullptr)->MaxColorTextureSamples)},
{"DynamicBackendParameters.MaxDepthTextureSamples", offsetof(DynamicBackendParameters, MaxDepthTextureSamples), sizeof(((DynamicBackendParameters*)nullptr)->MaxDepthTextureSamples)},
{"DynamicBackendParameters.MaxFramebufferSamples", offsetof(DynamicBackendParameters, MaxFramebufferSamples), sizeof(((DynamicBackendParameters*)nullptr)->MaxFramebufferSamples)},
{"DynamicBackendParameters.MaxIntegerSamples", offsetof(DynamicBackendParameters, MaxIntegerSamples), sizeof(((DynamicBackendParameters*)nullptr)->MaxIntegerSamples)},
{"DynamicBackendParameters.MaxSamples", offsetof(DynamicBackendParameters, MaxSamples), sizeof(((DynamicBackendParameters*)nullptr)->MaxSamples)},
{"DynamicBackendParameters.MaxSampleMaskWords", offsetof(DynamicBackendParameters, MaxSampleMaskWords), sizeof(((DynamicBackendParameters*)nullptr)->MaxSampleMaskWords)},
{"DynamicBackendParameters.MaxPatchVertices", offsetof(DynamicBackendParameters, MaxPatchVertices), sizeof(((DynamicBackendParameters*)nullptr)->MaxPatchVertices)},
{"DynamicBackendParameters.MaxTessGenLevel", offsetof(DynamicBackendParameters, MaxTessGenLevel), sizeof(((DynamicBackendParameters*)nullptr)->MaxTessGenLevel)},
{"DynamicBackendParameters.MinProgramTextureGatherOffset", offsetof(DynamicBackendParameters, MinProgramTextureGatherOffset), sizeof(((DynamicBackendParameters*)nullptr)->MinProgramTextureGatherOffset)},
{"DynamicBackendParameters.MaxProgramTextureGatherOffset", offsetof(DynamicBackendParameters, MaxProgramTextureGatherOffset), sizeof(((DynamicBackendParameters*)nullptr)->MaxProgramTextureGatherOffset)},
{"DynamicBackendParameters.MaxTextureImageUnits", offsetof(DynamicBackendParameters, MaxTextureImageUnits), sizeof(((DynamicBackendParameters*)nullptr)->MaxTextureImageUnits)},
{"DynamicBackendParameters.MaxVertexTextureImageUnits", offsetof(DynamicBackendParameters, MaxVertexTextureImageUnits), sizeof(((DynamicBackendParameters*)nullptr)->MaxVertexTextureImageUnits)},
{"DynamicBackendParameters.MaxComputeTextureImageUnits", offsetof(DynamicBackendParameters, MaxComputeTextureImageUnits), sizeof(((DynamicBackendParameters*)nullptr)->MaxComputeTextureImageUnits)},
{"DynamicBackendParameters.MaxCombinedTextureImageUnits", offsetof(DynamicBackendParameters, MaxCombinedTextureImageUnits), sizeof(((DynamicBackendParameters*)nullptr)->MaxCombinedTextureImageUnits)},
{"DynamicBackendParameters.MaxVertexAttribs", offsetof(DynamicBackendParameters, MaxVertexAttribs), sizeof(((DynamicBackendParameters*)nullptr)->MaxVertexAttribs)},
{"DynamicBackendParameters.MaxComputeShaderStorageBlocks", offsetof(DynamicBackendParameters, MaxComputeShaderStorageBlocks), sizeof(((DynamicBackendParameters*)nullptr)->MaxComputeShaderStorageBlocks)},
{"DynamicBackendParameters.MaxCombinedShaderStorageBlocks", offsetof(DynamicBackendParameters, MaxCombinedShaderStorageBlocks), sizeof(((DynamicBackendParameters*)nullptr)->MaxCombinedShaderStorageBlocks)},
{"DynamicBackendParameters.MaxVertexShaderStorageBlocks", offsetof(DynamicBackendParameters, MaxVertexShaderStorageBlocks), sizeof(((DynamicBackendParameters*)nullptr)->MaxVertexShaderStorageBlocks)},
{"DynamicBackendParameters.MaxTessControlShaderStorageBlocks", offsetof(DynamicBackendParameters, MaxTessControlShaderStorageBlocks), sizeof(((DynamicBackendParameters*)nullptr)->MaxTessControlShaderStorageBlocks)},
{"DynamicBackendParameters.MaxTessEvaluationShaderStorageBlocks", offsetof(DynamicBackendParameters, MaxTessEvaluationShaderStorageBlocks), sizeof(((DynamicBackendParameters*)nullptr)->MaxTessEvaluationShaderStorageBlocks)},
{"DynamicBackendParameters.MaxGeometryShaderStorageBlocks", offsetof(DynamicBackendParameters, MaxGeometryShaderStorageBlocks), sizeof(((DynamicBackendParameters*)nullptr)->MaxGeometryShaderStorageBlocks)},
{"DynamicBackendParameters.MaxFragmentShaderStorageBlocks", offsetof(DynamicBackendParameters, MaxFragmentShaderStorageBlocks), sizeof(((DynamicBackendParameters*)nullptr)->MaxFragmentShaderStorageBlocks)},
{"DynamicBackendParameters.MaxComputeUniformBlocks", offsetof(DynamicBackendParameters, MaxComputeUniformBlocks), sizeof(((DynamicBackendParameters*)nullptr)->MaxComputeUniformBlocks)},
{"DynamicBackendParameters.MaxComputeWorkGroupInvocations", offsetof(DynamicBackendParameters, MaxComputeWorkGroupInvocations), sizeof(((DynamicBackendParameters*)nullptr)->MaxComputeWorkGroupInvocations)},
{"DynamicBackendParameters.MaxComputeWorkGroupCount", offsetof(DynamicBackendParameters, MaxComputeWorkGroupCount), sizeof(((DynamicBackendParameters*)nullptr)->MaxComputeWorkGroupCount)},
{"DynamicBackendParameters.MaxComputeWorkGroupSize", offsetof(DynamicBackendParameters, MaxComputeWorkGroupSize), sizeof(((DynamicBackendParameters*)nullptr)->MaxComputeWorkGroupSize)},
{"DynamicBackendParameters.MaxShaderStorageBufferBindings", offsetof(DynamicBackendParameters, MaxShaderStorageBufferBindings), sizeof(((DynamicBackendParameters*)nullptr)->MaxShaderStorageBufferBindings)},
{"DynamicBackendParameters.MaxTextureBufferSize", offsetof(DynamicBackendParameters, MaxTextureBufferSize), sizeof(((DynamicBackendParameters*)nullptr)->MaxTextureBufferSize)},
{"DynamicBackendParameters.TextureBufferOffsetAlignment", offsetof(DynamicBackendParameters, TextureBufferOffsetAlignment), sizeof(((DynamicBackendParameters*)nullptr)->TextureBufferOffsetAlignment)},
{"DynamicBackendParameters.MaxUniformBufferBindings", offsetof(DynamicBackendParameters, MaxUniformBufferBindings), sizeof(((DynamicBackendParameters*)nullptr)->MaxUniformBufferBindings)},
{"DynamicBackendParameters.MaxUniformBlockSize", offsetof(DynamicBackendParameters, MaxUniformBlockSize), sizeof(((DynamicBackendParameters*)nullptr)->MaxUniformBlockSize)},
{"DynamicBackendParameters.MaxImageUnits", offsetof(DynamicBackendParameters, MaxImageUnits), sizeof(((DynamicBackendParameters*)nullptr)->MaxImageUnits)},
{"DynamicBackendParameters.MaxCombinedImageUniforms", offsetof(DynamicBackendParameters, MaxCombinedImageUniforms), sizeof(((DynamicBackendParameters*)nullptr)->MaxCombinedImageUniforms)},
{"DynamicBackendParameters.MaxVertexImageUniforms", offsetof(DynamicBackendParameters, MaxVertexImageUniforms), sizeof(((DynamicBackendParameters*)nullptr)->MaxVertexImageUniforms)},
{"DynamicBackendParameters.MaxGeometryImageUniforms", offsetof(DynamicBackendParameters, MaxGeometryImageUniforms), sizeof(((DynamicBackendParameters*)nullptr)->MaxGeometryImageUniforms)},
{"DynamicBackendParameters.MaxFragmentImageUniforms", offsetof(DynamicBackendParameters, MaxFragmentImageUniforms), sizeof(((DynamicBackendParameters*)nullptr)->MaxFragmentImageUniforms)},
{"DynamicBackendParameters.MaxComputeImageUniforms", offsetof(DynamicBackendParameters, MaxComputeImageUniforms), sizeof(((DynamicBackendParameters*)nullptr)->MaxComputeImageUniforms)},
{"DynamicBackendParameters.MaxDrawBuffers", offsetof(DynamicBackendParameters, MaxDrawBuffers), sizeof(((DynamicBackendParameters*)nullptr)->MaxDrawBuffers)},
{"DynamicBackendParameters.MaxColorAttachments", offsetof(DynamicBackendParameters, MaxColorAttachments), sizeof(((DynamicBackendParameters*)nullptr)->MaxColorAttachments)},
{"DynamicBackendParameters.MaxClipDistances", offsetof(DynamicBackendParameters, MaxClipDistances), sizeof(((DynamicBackendParameters*)nullptr)->MaxClipDistances)},
{"DynamicBackendParameters.MaxCullDistances", offsetof(DynamicBackendParameters, MaxCullDistances), sizeof(((DynamicBackendParameters*)nullptr)->MaxCullDistances)},
{"DynamicBackendParameters.MaxCombinedClipAndCullDistances", offsetof(DynamicBackendParameters, MaxCombinedClipAndCullDistances), sizeof(((DynamicBackendParameters*)nullptr)->MaxCombinedClipAndCullDistances)},
{"DynamicBackendParameters.MaxViewports", offsetof(DynamicBackendParameters, MaxViewports), sizeof(((DynamicBackendParameters*)nullptr)->MaxViewports)},
{"DynamicBackendParameters.LayerProvokingVertex", offsetof(DynamicBackendParameters, LayerProvokingVertex), sizeof(((DynamicBackendParameters*)nullptr)->LayerProvokingVertex)},
{"DynamicBackendParameters.ViewportIndexProvokingVertex", offsetof(DynamicBackendParameters, ViewportIndexProvokingVertex), sizeof(((DynamicBackendParameters*)nullptr)->ViewportIndexProvokingVertex)},
{"DynamicBackendParameters.MaxViewportWidth", offsetof(DynamicBackendParameters, MaxViewportWidth), sizeof(((DynamicBackendParameters*)nullptr)->MaxViewportWidth)},
{"DynamicBackendParameters.MaxViewportHeight", offsetof(DynamicBackendParameters, MaxViewportHeight), sizeof(((DynamicBackendParameters*)nullptr)->MaxViewportHeight)},
{"DynamicBackendParameters.ViewportBoundsRangeMin", offsetof(DynamicBackendParameters, ViewportBoundsRangeMin), sizeof(((DynamicBackendParameters*)nullptr)->ViewportBoundsRangeMin)},
{"DynamicBackendParameters.ViewportBoundsRangeMax", offsetof(DynamicBackendParameters, ViewportBoundsRangeMax), sizeof(((DynamicBackendParameters*)nullptr)->ViewportBoundsRangeMax)},
{"DynamicBackendParameters.ViewportSubpixelBits", offsetof(DynamicBackendParameters, ViewportSubpixelBits), sizeof(((DynamicBackendParameters*)nullptr)->ViewportSubpixelBits)},
{"DynamicBackendParameters.MinFragmentInterpolationOffset", offsetof(DynamicBackendParameters, MinFragmentInterpolationOffset), sizeof(((DynamicBackendParameters*)nullptr)->MinFragmentInterpolationOffset)},
{"DynamicBackendParameters.MaxFragmentInterpolationOffset", offsetof(DynamicBackendParameters, MaxFragmentInterpolationOffset), sizeof(((DynamicBackendParameters*)nullptr)->MaxFragmentInterpolationOffset)},
{"DynamicBackendParameters.FragmentInterpolationOffsetBits", offsetof(DynamicBackendParameters, FragmentInterpolationOffsetBits), sizeof(((DynamicBackendParameters*)nullptr)->FragmentInterpolationOffsetBits)},
{"DynamicBackendParameters.SupportsWideLines", offsetof(DynamicBackendParameters, SupportsWideLines), sizeof(((DynamicBackendParameters*)nullptr)->SupportsWideLines)},
{"DynamicBackendParameters.SupportsDistinctDepthStencilAttachments", offsetof(DynamicBackendParameters, SupportsDistinctDepthStencilAttachments), sizeof(((DynamicBackendParameters*)nullptr)->SupportsDistinctDepthStencilAttachments)},
{"DynamicBackendParameters.PerLayerFramebufferAttachmentTargets", offsetof(DynamicBackendParameters, PerLayerFramebufferAttachmentTargets), sizeof(((DynamicBackendParameters*)nullptr)->PerLayerFramebufferAttachmentTargets)},
{"DynamicBackendParameters.SupportsShaderFloat64", offsetof(DynamicBackendParameters, SupportsShaderFloat64), sizeof(((DynamicBackendParameters*)nullptr)->SupportsShaderFloat64)},
{"DynamicBackendParameters.SupportsFloat64VertexAttributes", offsetof(DynamicBackendParameters, SupportsFloat64VertexAttributes), sizeof(((DynamicBackendParameters*)nullptr)->SupportsFloat64VertexAttributes)},
{"DynamicBackendParameters.SupportsTessellationPointSize", offsetof(DynamicBackendParameters, SupportsTessellationPointSize), sizeof(((DynamicBackendParameters*)nullptr)->SupportsTessellationPointSize)},
{"DynamicBackendParameters.SupportsGeometryPointSize", offsetof(DynamicBackendParameters, SupportsGeometryPointSize), sizeof(((DynamicBackendParameters*)nullptr)->SupportsGeometryPointSize)},
{"DynamicBackendParameters.MaxShaderStorageBlockSize", offsetof(DynamicBackendParameters, MaxShaderStorageBlockSize), sizeof(((DynamicBackendParameters*)nullptr)->MaxShaderStorageBlockSize)},
{"DynamicBackendParameters.SubgroupSize", offsetof(DynamicBackendParameters, SubgroupSize), sizeof(((DynamicBackendParameters*)nullptr)->SubgroupSize)},
{"DynamicBackendParameters.SubgroupSupportedStages", offsetof(DynamicBackendParameters, SubgroupSupportedStages), sizeof(((DynamicBackendParameters*)nullptr)->SubgroupSupportedStages)},
{"DynamicBackendParameters.SubgroupSupportedFeatures", offsetof(DynamicBackendParameters, SubgroupSupportedFeatures), sizeof(((DynamicBackendParameters*)nullptr)->SubgroupSupportedFeatures)},
{"DynamicBackendParameters.SubgroupQuadOperationsInAllStages", offsetof(DynamicBackendParameters, SubgroupQuadOperationsInAllStages), sizeof(((DynamicBackendParameters*)nullptr)->SubgroupQuadOperationsInAllStages)},
{"DynamicBackendParameters.GpuVendor", offsetof(DynamicBackendParameters, GpuVendor), sizeof(((DynamicBackendParameters*)nullptr)->GpuVendor)},
{"MGHostSpan", 0, sizeof(MGHostSpan)},
{"MGHostSpan.Ptr", offsetof(MGHostSpan, Ptr), sizeof(((MGHostSpan*)nullptr)->Ptr)},
{"MGHostSpan.Seg", offsetof(MGHostSpan, Seg), sizeof(((MGHostSpan*)nullptr)->Seg)},
{"MGHostSpan.Size", offsetof(MGHostSpan, Size), sizeof(((MGHostSpan*)nullptr)->Size)},
{"MGHostSpan.Offset", offsetof(MGHostSpan, Offset), sizeof(((MGHostSpan*)nullptr)->Offset)},
{"MGPVertexAttribWire", 0, sizeof(MGPVertexAttribWire)},
{"MGPVertexAttribWire.Offset", offsetof(MGPVertexAttribWire, Offset), sizeof(((MGPVertexAttribWire*)nullptr)->Offset)},
{"MGPVertexAttribWire.Stride", offsetof(MGPVertexAttribWire, Stride), sizeof(((MGPVertexAttribWire*)nullptr)->Stride)},
{"MGPVertexAttribWire.Type", offsetof(MGPVertexAttribWire, Type), sizeof(((MGPVertexAttribWire*)nullptr)->Type)},
{"MGPVertexAttribWire.Size", offsetof(MGPVertexAttribWire, Size), sizeof(((MGPVertexAttribWire*)nullptr)->Size)},
{"MGPVertexAttribWire.Enabled", offsetof(MGPVertexAttribWire, Enabled), sizeof(((MGPVertexAttribWire*)nullptr)->Enabled)},
{"MGPVertexAttribWire.Normalized", offsetof(MGPVertexAttribWire, Normalized), sizeof(((MGPVertexAttribWire*)nullptr)->Normalized)},
{"MGPVertexAttribWire.IsInteger", offsetof(MGPVertexAttribWire, IsInteger), sizeof(((MGPVertexAttribWire*)nullptr)->IsInteger)},
{"MGPVertexAttribWire.IsLong", offsetof(MGPVertexAttribWire, IsLong), sizeof(((MGPVertexAttribWire*)nullptr)->IsLong)},
{"MGPVertexAttribWire.IsBgra", offsetof(MGPVertexAttribWire, IsBgra), sizeof(((MGPVertexAttribWire*)nullptr)->IsBgra)},
{"MGPVertexAttribWire.BindingIndex", offsetof(MGPVertexAttribWire, BindingIndex), sizeof(((MGPVertexAttribWire*)nullptr)->BindingIndex)},
{"MGPVertexBindingPointWire", 0, sizeof(MGPVertexBindingPointWire)},
{"MGPVertexBindingPointWire.Offset", offsetof(MGPVertexBindingPointWire, Offset), sizeof(((MGPVertexBindingPointWire*)nullptr)->Offset)},
{"MGPVertexBindingPointWire.Stride", offsetof(MGPVertexBindingPointWire, Stride), sizeof(((MGPVertexBindingPointWire*)nullptr)->Stride)},
{"MGPVertexBindingPointWire.Divisor", offsetof(MGPVertexBindingPointWire, Divisor), sizeof(((MGPVertexBindingPointWire*)nullptr)->Divisor)},
{"MGPipeHandle", 0, sizeof(MGPipeHandle)},
{"MGPipeHandle.Slot", offsetof(MGPipeHandle, Slot), sizeof(((MGPipeHandle*)nullptr)->Slot)},
{"MGPipeHandle.Gen", offsetof(MGPipeHandle, Gen), sizeof(((MGPipeHandle*)nullptr)->Gen)},
{"MGPWireRecHeader", 0, sizeof(MGPWireRecHeader)},
{"MGPWireRecHeader.Op", offsetof(MGPWireRecHeader, Op), sizeof(((MGPWireRecHeader*)nullptr)->Op)},
{"MGPWireRecHeader.Flags", offsetof(MGPWireRecHeader, Flags), sizeof(((MGPWireRecHeader*)nullptr)->Flags)},
{"MGPWireRecHeader.Size", offsetof(MGPWireRecHeader, Size), sizeof(((MGPWireRecHeader*)nullptr)->Size)},
};
struct WireCallLayout { const char* Name; const char* Payload; Uint64 Op; Uint64 Flags; Uint64 Wait; };
inline constexpr WireCallLayout kMGPipeWireCallLayouts[] = {
{"GetCaps", "MGPCaps", static_cast<Uint64>(MGPWireOp::GetCaps), static_cast<Uint64>(kReplySlot | kHasBlob), static_cast<Uint64>(kWaitReply)},
{"ResourceCreate", "MGPResourceDesc", static_cast<Uint64>(MGPWireOp::ResourceCreate), static_cast<Uint64>(kReplySlot), static_cast<Uint64>(kWaitReply)},
{"ResourceRespecify", "MGPResourceDesc", static_cast<Uint64>(MGPWireOp::ResourceRespecify), static_cast<Uint64>(kNeedsAck | kReplySlot), static_cast<Uint64>(kWaitReply)},
{"ResourceDestroy", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::ResourceDestroy), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"MapPersistent", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::MapPersistent), static_cast<Uint64>(kReplySlot | kOptional), static_cast<Uint64>(kWaitReply)},
{"UnmapPersistent", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::UnmapPersistent), static_cast<Uint64>(kOptional), static_cast<Uint64>(kWaitNone)},
{"FenceCreate", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::FenceCreate), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"FenceStatus", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::FenceStatus), static_cast<Uint64>(kReplySlot), static_cast<Uint64>(kWaitReply)},
{"FenceWait", "MGPFenceWait", static_cast<Uint64>(MGPWireOp::FenceWait), static_cast<Uint64>(kReplySlot), static_cast<Uint64>(kWaitReply)},
{"FenceDestroy", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::FenceDestroy), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"QueryCreate", "MGPQueryDesc", static_cast<Uint64>(MGPWireOp::QueryCreate), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"QueryBegin", "MGPQueryDesc", static_cast<Uint64>(MGPWireOp::QueryBegin), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"QueryEnd", "MGPQueryDesc", static_cast<Uint64>(MGPWireOp::QueryEnd), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"QueryAvailable", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::QueryAvailable), static_cast<Uint64>(kReplySlot), static_cast<Uint64>(kWaitReply)},
{"QueryResult", "MGPQueryResultRequest", static_cast<Uint64>(MGPWireOp::QueryResult), static_cast<Uint64>(kReplySlot), static_cast<Uint64>(kWaitReply)},
{"QueryDestroy", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::QueryDestroy), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"CreateRenderState", "MGPRenderStateDesc", static_cast<Uint64>(MGPWireOp::CreateRenderState), static_cast<Uint64>(kHasBlob), static_cast<Uint64>(kWaitNone)},
{"BindRenderState", "MGPBindRenderState", static_cast<Uint64>(MGPWireOp::BindRenderState), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"DeleteRenderState", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::DeleteRenderState), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"CreateVertexElements", "MGPVertexElements", static_cast<Uint64>(MGPWireOp::CreateVertexElements), static_cast<Uint64>(kHasBlob), static_cast<Uint64>(kWaitNone)},
{"BindVertexElements", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::BindVertexElements), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"DeleteVertexElements", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::DeleteVertexElements), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"CreateSamplerState", "MGPSamplerDesc", static_cast<Uint64>(MGPWireOp::CreateSamplerState), static_cast<Uint64>(kHasBlob), static_cast<Uint64>(kWaitNone)},
{"DeleteSamplerState", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::DeleteSamplerState), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"CreateSamplerView", "MGPSamplerView", static_cast<Uint64>(MGPWireOp::CreateSamplerView), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"DeleteSamplerView", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::DeleteSamplerView), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"CreateShaderState", "MGPProgramDesc", static_cast<Uint64>(MGPWireOp::CreateShaderState), static_cast<Uint64>(kHasBlob), static_cast<Uint64>(kWaitNone)},
{"BindShaderState", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::BindShaderState), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"DeleteShaderState", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::DeleteShaderState), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"SetDynamicState", "MGPDynamicState", static_cast<Uint64>(MGPWireOp::SetDynamicState), static_cast<Uint64>(kHasBlob), static_cast<Uint64>(kWaitNone)},
{"SetFramebufferState", "MGPFramebufferState", static_cast<Uint64>(MGPWireOp::SetFramebufferState), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"SetVertexBuffers", "MGPVertexBuffers", static_cast<Uint64>(MGPWireOp::SetVertexBuffers), static_cast<Uint64>(kVarTail), static_cast<Uint64>(kWaitNone)},
{"SetIndexBuffer", "MGPIndexBuffer", static_cast<Uint64>(MGPWireOp::SetIndexBuffer), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"SetIndirectBuffers", "MGPIndirectBuffers", static_cast<Uint64>(MGPWireOp::SetIndirectBuffers), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"SetSamplerViews", "MGPSamplerViews", static_cast<Uint64>(MGPWireOp::SetSamplerViews), static_cast<Uint64>(kVarTail), static_cast<Uint64>(kWaitNone)},
{"BindSamplerStates", "MGPSamplerStates", static_cast<Uint64>(MGPWireOp::BindSamplerStates), static_cast<Uint64>(kVarTail), static_cast<Uint64>(kWaitNone)},
{"SetShaderImages", "MGPShaderImages", static_cast<Uint64>(MGPWireOp::SetShaderImages), static_cast<Uint64>(kVarTail), static_cast<Uint64>(kWaitNone)},
{"SetShaderBuffers", "MGPShaderBuffers", static_cast<Uint64>(MGPWireOp::SetShaderBuffers), static_cast<Uint64>(kVarTail | kHostSpan), static_cast<Uint64>(kWaitNone)},
{"SetStreamOutputTargets", "MGPStreamOutputTargets", static_cast<Uint64>(MGPWireOp::SetStreamOutputTargets), static_cast<Uint64>(kVarTail), static_cast<Uint64>(kWaitNone)},
{"SetGlobalConstants", "MGPGlobalConstants", static_cast<Uint64>(MGPWireOp::SetGlobalConstants), static_cast<Uint64>(kHasBlob), static_cast<Uint64>(kWaitNone)},
{"SetVertexAttribDefaults", "MGPVertexAttribDefaults", static_cast<Uint64>(MGPWireOp::SetVertexAttribDefaults), static_cast<Uint64>(kVarTail), static_cast<Uint64>(kWaitNone)},
{"SetPixelPackState", "MGPPixelPackState", static_cast<Uint64>(MGPWireOp::SetPixelPackState), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"SetPatchState", "MGPPatchState", static_cast<Uint64>(MGPWireOp::SetPatchState), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"SetDrawProgram", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::SetDrawProgram), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"SetDispatchProgram", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::SetDispatchProgram), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"SetResidualValueState", "MGPResidualValueState", static_cast<Uint64>(MGPWireOp::SetResidualValueState), static_cast<Uint64>(kHasBlob), static_cast<Uint64>(kWaitNone)},
{"SetTextureParams", "MGPTextureParams", static_cast<Uint64>(MGPWireOp::SetTextureParams), static_cast<Uint64>(kReplySlot), static_cast<Uint64>(kWaitReply)},
{"ResourceSubData", "MGPSubData", static_cast<Uint64>(MGPWireOp::ResourceSubData), static_cast<Uint64>(kHasBlob | kVarTail | kReplySlot), static_cast<Uint64>(kWaitReply)},
{"BufferSubDataResident", "MGPSubData", static_cast<Uint64>(MGPWireOp::BufferSubDataResident), static_cast<Uint64>(kHasBlob | kOptional), static_cast<Uint64>(kWaitNone)},
{"ResourceSubDataComplete", "MGPSubDataComplete", static_cast<Uint64>(MGPWireOp::ResourceSubDataComplete), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"ResourceFlushRange", "MGPFlushRange", static_cast<Uint64>(MGPWireOp::ResourceFlushRange), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"ResourceReadback", "MGPReadback", static_cast<Uint64>(MGPWireOp::ResourceReadback), static_cast<Uint64>(kReplySlot), static_cast<Uint64>(kWaitReply)},
{"ResourceCopyRegion", "MGPCopyRegion", static_cast<Uint64>(MGPWireOp::ResourceCopyRegion), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"GenerateMipmap", "MGPMipPlan", static_cast<Uint64>(MGPWireOp::GenerateMipmap), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"GetTextureImage", "MGPReadbackInfo", static_cast<Uint64>(MGPWireOp::GetTextureImage), static_cast<Uint64>(kReplySlot), static_cast<Uint64>(kWaitReply)},
{"Blit", "MGPBlit", static_cast<Uint64>(MGPWireOp::Blit), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"Clear", "MGPClear", static_cast<Uint64>(MGPWireOp::Clear), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"ReadPixels", "MGPReadbackInfo", static_cast<Uint64>(MGPWireOp::ReadPixels), static_cast<Uint64>(kReplySlot), static_cast<Uint64>(kWaitReply)},
{"DrawVbo", "MGPDrawInfo", static_cast<Uint64>(MGPWireOp::DrawVbo), static_cast<Uint64>(kHostSpan | kVarTail), static_cast<Uint64>(kWaitNone)},
{"LaunchGrid", "MGPGridInfo", static_cast<Uint64>(MGPWireOp::LaunchGrid), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"MemoryBarrier", "MGPMemoryBarrier", static_cast<Uint64>(MGPWireOp::MemoryBarrier), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"BeginStreamOutput", "MGPStreamOutputBegin", static_cast<Uint64>(MGPWireOp::BeginStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"EndStreamOutput", "MGPXfbAccounting", static_cast<Uint64>(MGPWireOp::EndStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"PauseStreamOutput", "MGPStreamOutputControl", static_cast<Uint64>(MGPWireOp::PauseStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"ResumeStreamOutput", "MGPStreamOutputControl", static_cast<Uint64>(MGPWireOp::ResumeStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"Flush", "MGPFlush", static_cast<Uint64>(MGPWireOp::Flush), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"Present", "MGPPresent", static_cast<Uint64>(MGPWireOp::Present), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitPresent)},
{"SetSwapInterval", "MGPSwapInterval", static_cast<Uint64>(MGPWireOp::SetSwapInterval), static_cast<Uint64>(kOptional), static_cast<Uint64>(kWaitNone)},
{"QueryTimestamp", "MGPTimestampRequest", static_cast<Uint64>(MGPWireOp::QueryTimestamp), static_cast<Uint64>(kReplySlot), static_cast<Uint64>(kWaitReply)},
{"QueryCounter", "MGPQueryDesc", static_cast<Uint64>(MGPWireOp::QueryCounter), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"FenceWaitServer", "MGPFenceWait", static_cast<Uint64>(MGPWireOp::FenceWaitServer), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"BindShaderImage", "MGPImageBind", static_cast<Uint64>(MGPWireOp::BindShaderImage), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"PatchParameter", "MGPPatchParameter", static_cast<Uint64>(MGPWireOp::PatchParameter), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"BindStreamOutput", "MGPStreamOutputBind", static_cast<Uint64>(MGPWireOp::BindStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"SetStorageBlockBinding", "MGPStorageBlockBinding", static_cast<Uint64>(MGPWireOp::SetStorageBlockBinding), static_cast<Uint64>(kHasBlob), static_cast<Uint64>(kWaitApplied)},
{"CopyFramebufferToTexture", "MGPCopyFromFramebuffer", static_cast<Uint64>(MGPWireOp::CopyFramebufferToTexture), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"ApplierReset", "MGPApplierReset", static_cast<Uint64>(MGPWireOp::ApplierReset), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitApplied)},
{"ObjectDeath", "MGPHandleOnly", static_cast<Uint64>(MGPWireOp::ObjectDeath), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"SetContextValues", "MGPContextValues", static_cast<Uint64>(MGPWireOp::SetContextValues), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
{"SetProgramBindings", "MGPProgramBindings", static_cast<Uint64>(MGPWireOp::SetProgramBindings), static_cast<Uint64>(kVarTail | kHostSpan), static_cast<Uint64>(kWaitNone)},
{"DeleteStreamOutput", "MGPStreamOutputBind", static_cast<Uint64>(MGPWireOp::DeleteStreamOutput), static_cast<Uint64>(kNone), static_cast<Uint64>(kWaitNone)},
};
+18 -29
View File
@@ -35,6 +35,7 @@
#include "CapsCodec.h"
#include "Protocol/mg_protocol_base.h"
#include <MG_Pipe/PipeWireLayout.h>
#include <MGGitHash.h>
#include <MG_Util/Debug/Log.h>
@@ -461,43 +462,31 @@ namespace MobileGL::MG_Remote {
// ---------------------------------------------------------------------------------
Transport::AbiFingerprintInputs CapsAbiFingerprintInputs() {
// MGPCaps has only a COMPOSITIONAL size assertion (MGPipeTypes.h:145-146) because
// DynamicBackendParameters still carries SizeT and GLenum members - P0.5's fixed-width
// rewrite did not happen and P5 does not do it either (table 0's ABI row; the rewrite
// is P7's account). So the caps block's literal size IS ABI-dependent, and this
// fingerprint is what turns that from a latent hazard into a named refusal.
//
// The git stamp is in it because two builds of the same sizes can still disagree about
// a FIELD ORDER, which no sizeof can see; P6's spawn is same-machine and same-binary,
// so it inherits this unchanged rather than needing a looser rule.
Transport::AbiFingerprintInputs inputs;
inputs.DynamicParamsSize = sizeof(MG_Backend::DynamicBackendParameters);
inputs.CapsSize = sizeof(MG_Pipe::MGPCaps);
inputs.FunctionTableSize = sizeof(MG_Backend::GLFunctionsTable);
inputs.FormatCapabilityTargets = static_cast<Uint64>(MG_Backend::kFormatCapabilityTargetCount);
inputs.FormatCapabilityFormats = static_cast<Uint64>(MG_Backend::kFormatCapabilityFormatCount);
inputs.MemberLayout = MG_Pipe::kMGPipeWireMemberLayoutDigest;
inputs.CatalogueLayout = MG_Pipe::kMGPipeWireCatalogueDigest;
inputs.RenderStateLayout = MG_Pipe::WireRenderStateDigest();
inputs.FormatCapabilityTargets = MG_Backend::kFormatCapabilityTargetCount;
inputs.FormatCapabilityFormats = MG_Backend::kFormatCapabilityFormatCount;
inputs.FormatCapabilitiesCodecVersion = kFormatCapabilitiesCodecVersion;
inputs.RendererInfoCodecVersion = kRendererInfoCodecVersion;
inputs.OpCount = static_cast<Uint64>(MG_Pipe::MGPWireOp::kOpCount);
// The declared protocol ABI, carried over from s1's version of this function at
// integration (ID-33). The sizeofs above catch a struct that changed shape; this
// catches a peer that changed the PROTOCOL while every struct stayed the same size,
// which is the one break the rest of the mix is blind to.
inputs.AbiVersion =
static_cast<Uint32>(MOBILEGL_ABI_VERSION(MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR));
// MOBILEGL_BUILD_STAMP_VALUE, NOT GIT_COMMIT_HASH_SHORT (CONTRACT-P6 4.2). The old
// macro is application-visible through glGetString(GL_VERSION) and may not move, and it
// came from an execute_process with no RESULT_VARIABLE - so a tree git could not read
// produced "" and every such build agreed with every other. The new pair separates the
// VALUE from whether the build could name a commit at all.
inputs.BuildStamp = MOBILEGL_BUILD_STAMP_VALUE;
inputs.BuildStampPresent = MOBILEGL_BUILD_STAMP_PRESENT;
inputs.AbiVersion = MOBILEGL_ABI_VERSION(MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR);
inputs.PointerBits = sizeof(void*) * 8;
const Uint32 endian = 1;
inputs.LittleEndian = *reinterpret_cast<const Uint8*>(&endian) == 1 ? 1u : 0u;
return inputs;
}
// ONE implementation, deliberately a one-liner: the wave-1 review (ID-46 finding 6) found a
// second hand-rolled FNV loop here while the mixer under Transport/ had no production caller,
// so the sensitivity test could not see this function change. Now it starts from here.
Uint64 CapsAbiFingerprint() { return Transport::MixAbiFingerprint(CapsAbiFingerprintInputs()); }
Uint64 WireFingerprint() {
static const Uint64 fingerprint = Transport::MixAbiFingerprint(CapsAbiFingerprintInputs());
return fingerprint;
}
// Compatibility spelling for existing wire tests and helper peers.
Uint64 CapsAbiFingerprint() { return WireFingerprint(); }
const char* BuildFingerprint() { return MOBILEGL_BUILD_STAMP_VALUE; }
Bool BuildFingerprintPresent() { return MOBILEGL_BUILD_STAMP_PRESENT != 0; }
} // namespace MobileGL::MG_Remote
+6 -20
View File
@@ -20,13 +20,7 @@
// RendererInfo -> RendererInfo (MG_Util/Types.h:317): three Strings, an
// Optional<String>, and a Vector<GLExtension> inside GLInfo.
//
// THE ABI RULING (table 0). MGPCaps has only a COMPOSITIONAL size assertion
// (MGPipeTypes.h:145-146) because DynamicBackendParameters still carries SizeT and GLenum
// members - P0.5's fixed-width rewrite did not happen. P5 does NOT rewrite it. Instead the
// handshake asserts that both peers agree on sizeof(DynamicBackendParameters),
// sizeof(MGPCaps) and the build fingerprint, and Fatal{AbiMismatch} otherwise. P6's spawn is
// same-machine and same-binary, so it inherits this unchanged; the fixed-width rewrite is on
// P7's account.
// P6.5: wire layout and build identity are independent handshake facts.
#pragma once
#include <Includes.h>
@@ -90,19 +84,11 @@ namespace MobileGL::MG_Remote {
Bool EncodeRendererInfo(const RendererInfo& info, Vector<Uint8>& out);
Bool DecodeRendererInfo(const void* bytes, Uint64 size, RendererInfo& out);
// ---- the ABI assertion the handshake carries ----------------------------------------
//
// The inputs, as this build sees them: sizeof(DynamicBackendParameters), sizeof(MGPCaps),
// sizeof(GLFunctionsTable), the format-capability table's extents, the two caps-blob codec
// versions, MGPWireOp::kOpCount, the protocol ABI version and the compile-time git stamp.
// Public so that the sensitivity control can pin every one of them to the real value AND
// perturb them one at a time through the same mixer the handshake uses.
// Wire compatibility never includes a git stamp or local function-pointer tables.
Transport::AbiFingerprintInputs CapsAbiFingerprintInputs();
// What Hello/Welcome carry and compare: EXACTLY Transport::MixAbiFingerprint(
// CapsAbiFingerprintInputs()) - one implementation, no second hash (ID-46 finding 6). A
// mismatch is Fatal{AbiMismatch} and never a downgrade, because every alternative silently
// reads one struct as another.
Uint64 CapsAbiFingerprint();
Uint64 WireFingerprint();
Uint64 CapsAbiFingerprint(); // legacy spelling of WireFingerprint
const char* BuildFingerprint();
Bool BuildFingerprintPresent();
} // namespace MobileGL::MG_Remote
+270 -117
View File
@@ -11,6 +11,8 @@
#include "ClientSession.h"
#include <MG_Remote/FatalFunnel.h>
#include <MG_Remote/Handshake.h>
#include <MG_Remote/Transport/LinkMetrics.h>
#include "../CapsCodec.h"
#include "../Protocol/generated/protocol_generated.h"
@@ -109,39 +111,6 @@ namespace MobileGL::MG_Remote::Client {
return ::MobileGL::Wire::GetCtrlEnvelope(bytes.data());
}
// CONTRACT-P6 4.3: THE DECLARED PROTOCOL VERSION, WHICH THIS SIDE NEVER READ.
//
// Hello has carried abiMajor/abiMinor since P5 and the SERVER checks them, but the
// client only ever compared the fingerprint - so the asymmetry was silent in the one
// direction that matters for spawn, where the peer is a SEPARATE IMAGE the operator
// chose with MOBILEGL_IPC_SERVER_PATH.
//
// The fingerprint mixes AbiVersion and would catch a mismatch anyway, but it catches it
// as "struct shapes" - which sends the reader looking for a layout change that is not
// there. Checking the version FIRST turns the same failure into the sentence that
// explains it. Both handshake sites call this; there are two, and only one of them used
// to be reachable, which is how the gap survived review.
[[noreturn]] void FatalProtocolVersionMismatch(unsigned theirMajor, unsigned theirMinor,
const char* theirStamp) {
SessionFail(MGFatalFamily::AbiMismatch, "MGPipe: Fatal{AbiMismatch, \"protocol version\"} ours=%u.%u theirs=%u.%u "
"ourBuild=%s theirBuild=%s - the peer speaks a different MGPipe protocol. "
"Under spawn this is most often a stale MOBILEGL_IPC_SERVER_PATH pointing at "
"a libMobileGLServer.so built from another tree.",
static_cast<unsigned>(MOBILEGL_PROTOCOL_ABI_MAJOR),
static_cast<unsigned>(MOBILEGL_PROTOCOL_ABI_MINOR), theirMajor, theirMinor,
GIT_COMMIT_HASH_SHORT, theirStamp == nullptr ? "?" : theirStamp);
}
[[noreturn]] void FatalAbiMismatch(const char* what, Uint64 ours, Uint64 theirs,
const char* theirStamp) {
SessionFail(MGFatalFamily::AbiMismatch, "MGPipe: Fatal{AbiMismatch, \"%s\"} ours=%llu theirs=%llu ourBuild=%s "
"theirBuild=%s - never a downgrade: the caps block's size is ABI-dependent "
"and every field past the first difference would be read at the wrong offset",
what, static_cast<unsigned long long>(ours),
static_cast<unsigned long long>(theirs), GIT_COMMIT_HASH_SHORT,
theirStamp == nullptr ? "?" : theirStamp);
}
// Guarded the same way ServerSession's helpers are: MG_Config::Ipc only exists
// behind MOBILEGL_BUILD_DISAGGREGATED (Config.h), and this file is only compiled
// there today - but the guard is what keeps that true if the source list ever
@@ -531,6 +500,11 @@ namespace MobileGL::MG_Remote::Client {
return *instance;
}
ClientSession::ClientSession() {
m_link = Transport::CreateSharedLink(Transport::TransportRoleTag::ClientProducer);
m_cmd = &m_link->CommandsOut(); m_events = &m_link->EventsIn();
}
ClientSession::~ClientSession() {
if (m_started) {
Stop();
@@ -580,13 +554,19 @@ namespace MobileGL::MG_Remote::Client {
// ---- 2. Hello. Sent before the server accepts: InProcessTransport queues whole
// messages, so one thread can drive both halves of the handshake in order.
const Uint64 fingerprint = CapsAbiFingerprint();
const auto dial = ::MobileGL::Wire::DialMode::No;
const auto dataPlane = ::MobileGL::Wire::DataPlane::SharedSegments;
const Uint64 fingerprint = WireFingerprint();
{
::flatbuffers::FlatBufferBuilder builder(512);
auto stamp = builder.CreateString(GIT_COMMIT_HASH_SHORT);
auto stamp = builder.CreateString(BuildFingerprint());
auto terms = ::MobileGL::Wire::CreateLinkTerms(builder, dataPlane);
const char* tokenText = std::getenv("MOBILEGL_IPC_TOKEN");
auto token = builder.CreateString(tokenText == nullptr ? "" : tokenText);
auto hello = ::MobileGL::Wire::CreateHello(
builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR, stamp,
/*backendType=*/0u, /*pid=*/SelfProcessId(), /*configBlob=*/0, fingerprint);
static_cast<Uint32>(MG_Config::ActiveBackendType), SelfProcessId(), 0,
fingerprint, fingerprint, terms, token, dial);
auto root = ::MobileGL::Wire::CreateCtrlEnvelope(
builder, ::MobileGL::Wire::CtrlMsg::Hello, hello.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root);
@@ -617,6 +597,10 @@ namespace MobileGL::MG_Remote::Client {
return received;
}
const ::MobileGL::Wire::CtrlEnvelope* envelope = ParseEnvelope(frame);
if (LogPeerRefusal(envelope)) {
Stop();
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
// msg_as_Welcome() IS PART OF THE GUARD - flatbuffers' Verifier::VerifyTable is
// `return !table || table->Verify(*this)`, so a NULL union member verifies while
// msg_type() still reports Welcome. See ServerSession::Accept for the same guard.
@@ -634,16 +618,32 @@ namespace MobileGL::MG_Remote::Client {
// 4.3: the pid the SERVER stated, taken before any check can abort, so a Fatal line
// can name it too.
m_peerServerPid = static_cast<std::uint32_t>(welcome->serverPid());
// The VERSION before the fingerprint: both would fire, and only one names the cause.
if (welcome->abiMajor() != MOBILEGL_PROTOCOL_ABI_MAJOR ||
welcome->abiMinor() != MOBILEGL_PROTOCOL_ABI_MINOR) {
FatalProtocolVersionMismatch(static_cast<unsigned>(welcome->abiMajor()),
static_cast<unsigned>(welcome->abiMinor()),
theirStamp);
const MobileGLResult compatible = ValidatePeerHandshake(*m_transport,
welcome->abiMajor(), welcome->abiMinor(), welcome->wireFingerprint(), theirStamp, dial);
if (compatible != MOBILEGL_OK) {
Stop();
return compatible;
}
if (welcome->abiFingerprint() != fingerprint) {
FatalAbiMismatch("struct shapes", fingerprint, welcome->abiFingerprint(),
theirStamp);
const auto* terms = welcome->linkTerms();
if (terms == nullptr || terms->dataPlane() != dataPlane ||
terms->wireForm() != ::MobileGL::Wire::WireForm::StructImage ||
terms->cmdWindowBytes() < Transport::kMinRingCapacity ||
(terms->cmdWindowBytes() & (terms->cmdWindowBytes() - 1)) != 0 ||
terms->stageWindowBytes() == 0 || terms->maxReplyBytes() == 0 ||
terms->maxReplyBytes() > 0xFFFFFFFFull - sizeof(Transport::ReplySlotHeader) ||
terms->eventWindowBytes() < Transport::kMinRingCapacity ||
(terms->eventWindowBytes() & (terms->eventWindowBytes() - 1)) != 0) {
const auto result = RefuseHandshake(*m_transport, ::MobileGL::Wire::RefuseCode::LinkTerms,
"invalid server link terms");
Stop();
return result;
}
if (welcome->backendType() != static_cast<Uint32>(MG_Config::ActiveBackendType)) {
const auto result = RefuseHandshake(*m_transport, ::MobileGL::Wire::RefuseCode::Backend,
"server backend differs from request", static_cast<Uint32>(MG_Config::ActiveBackendType),
welcome->backendType());
Stop();
return result;
}
// The four SegmentRefs are what a spawn client MAPS (P6). Under inproc the mapping
// already exists, so what they are good for here is the cross-check that the two
@@ -675,7 +675,7 @@ namespace MobileGL::MG_Remote::Client {
// under the client role; under `spawn` it becomes ShmSegment::Adopt of the fds the
// server passed by SCM_RIGHTS, which is why nothing below this line knows which it was.
const MobileGLResult attached =
m_shm.AttachInProcess(server.Shm(), Transport::MemoryRole::Client);
m_link->Memory().AttachInProcess(server.Shm(), Transport::MemoryRole::Client);
if (attached != MOBILEGL_OK) {
Stop();
return attached;
@@ -690,6 +690,35 @@ namespace MobileGL::MG_Remote::Client {
if (m_started) {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
const std::string configured(MG_Config::Ipc.Control.c_str());
if (configured != "fork") {
const bool tcp = configured.compare(0, 6, "tcp://") == 0;
const bool unixSocket = configured.compare(0, 5, "unix:") == 0;
const std::string data(MG_Config::Ipc.Data.c_str());
if ((!tcp && !unixSocket) ||
(data != "auto" && data != (tcp ? "stream" : "shm"))) {
MGLOG_E("MG_Remote: Refuse{ProtocolMismatch} unsupported control/data pair %s/%s",
configured.c_str(), data.c_str());
return MOBILEGL_ERR_UNSUPPORTED;
}
m_connectDial = true;
const auto endpoint = tcp ? configured : configured.substr(5);
std::unique_ptr<Transport::SocketTransport> socket;
const auto connected = Transport::SocketTransport::ConnectTo(endpoint, kSpawnConnectTimeoutMs, socket);
if (connected != MOBILEGL_OK) return connected;
const auto started = StartOverSocket(std::move(socket));
if (started == MOBILEGL_OK) {
MGLOG_I("MG_Remote client: control=%s data=%s server=%s pid=%u dial=connect",
tcp ? "tcp" : "unix", tcp ? "stream" : "shm",
tcp ? endpoint.c_str() + 6 : endpoint.c_str(), m_peerServerPid);
}
return started;
}
if (MG_Config::Ipc.Data != "auto" && MG_Config::Ipc.Data != "shm") {
MGLOG_E("MG_Remote: Refuse{ProtocolMismatch} fork requires shm data in P6.5 wave one");
return MOBILEGL_ERR_UNSUPPORTED;
}
m_connectDial = false;
// A rendezvous nobody else can collide with. Independent processes need
// a NAME; making it per-process-per-instant is what stops two runs on
// one machine - which `ctest -j` produces by construction - finding each
@@ -755,13 +784,20 @@ namespace MobileGL::MG_Remote::Client {
// ---- Hello. The same bytes as inproc; the fingerprint is still compared
// for equality, because under Dial == Fork the child IS the same binary and
// comparing it catches a stale MOBILEGL_IPC_SERVER_PATH (CONTRACT-P6 4.1).
const Uint64 fingerprint = CapsAbiFingerprint();
const auto dial = m_connectDial ? ::MobileGL::Wire::DialMode::Connect : ::MobileGL::Wire::DialMode::Fork;
const auto dataPlane = m_socketTransport->IsTcp() ? ::MobileGL::Wire::DataPlane::Stream
: ::MobileGL::Wire::DataPlane::SharedSegments;
const Uint64 fingerprint = WireFingerprint();
{
::flatbuffers::FlatBufferBuilder builder(512);
auto stamp = builder.CreateString(GIT_COMMIT_HASH_SHORT);
auto stamp = builder.CreateString(BuildFingerprint());
auto terms = ::MobileGL::Wire::CreateLinkTerms(builder, dataPlane);
const char* tokenText = std::getenv("MOBILEGL_IPC_TOKEN");
auto token = builder.CreateString(tokenText == nullptr ? "" : tokenText);
auto hello = ::MobileGL::Wire::CreateHello(
builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR, stamp,
/*backendType=*/0u, /*pid=*/SelfProcessId(), /*configBlob=*/0, fingerprint);
static_cast<Uint32>(MG_Config::ActiveBackendType), SelfProcessId(), 0,
fingerprint, fingerprint, terms, token, dial);
auto root = ::MobileGL::Wire::CreateCtrlEnvelope(
builder, ::MobileGL::Wire::CtrlMsg::Hello, hello.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root);
@@ -778,6 +814,8 @@ namespace MobileGL::MG_Remote::Client {
// own mapping, which a6 showed is vacuous against a real peer. Here the wire
// values are the only values, which is what that check has to become on
// every lane (CONTRACT-P6 4.4).
Transport::SessionSegmentSizes negotiatedSizes;
bool stream = false;
Uint64 announced[4] = {0, 0, 0, 0};
const Uint32 replySlotCount = Transport::kDefaultReplySlotCount;
{
@@ -791,6 +829,10 @@ namespace MobileGL::MG_Remote::Client {
return received;
}
const ::MobileGL::Wire::CtrlEnvelope* envelope = ParseEnvelope(frame);
if (LogPeerRefusal(envelope)) {
Stop();
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
if (envelope == nullptr || envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::Welcome ||
envelope->msg_as_Welcome() == nullptr) {
MGLOG_E("MG_Remote client: the spawned server first frame is not a verifiable "
@@ -805,17 +847,39 @@ namespace MobileGL::MG_Remote::Client {
// 4.3: the pid the SERVER stated, taken before any check can abort, so a Fatal line
// can name it too.
m_peerServerPid = static_cast<std::uint32_t>(welcome->serverPid());
// The VERSION before the fingerprint: both would fire, and only one names the cause.
if (welcome->abiMajor() != MOBILEGL_PROTOCOL_ABI_MAJOR ||
welcome->abiMinor() != MOBILEGL_PROTOCOL_ABI_MINOR) {
FatalProtocolVersionMismatch(static_cast<unsigned>(welcome->abiMajor()),
static_cast<unsigned>(welcome->abiMinor()),
theirStamp);
const MobileGLResult compatible = ValidatePeerHandshake(*m_transport,
welcome->abiMajor(), welcome->abiMinor(), welcome->wireFingerprint(), theirStamp, dial);
if (compatible != MOBILEGL_OK) {
Stop();
return compatible;
}
if (welcome->abiFingerprint() != fingerprint) {
FatalAbiMismatch("struct shapes", fingerprint, welcome->abiFingerprint(),
theirStamp);
const auto* terms = welcome->linkTerms();
if (terms == nullptr || terms->dataPlane() != dataPlane ||
terms->wireForm() != ::MobileGL::Wire::WireForm::StructImage ||
terms->cmdWindowBytes() < Transport::kMinRingCapacity ||
(terms->cmdWindowBytes() & (terms->cmdWindowBytes() - 1)) != 0 ||
terms->stageWindowBytes() == 0 || terms->maxReplyBytes() == 0 ||
terms->maxReplyBytes() > 0xFFFFFFFFull - sizeof(Transport::ReplySlotHeader) ||
terms->eventWindowBytes() < Transport::kMinRingCapacity ||
(terms->eventWindowBytes() & (terms->eventWindowBytes() - 1)) != 0) {
const auto result = RefuseHandshake(*m_transport, ::MobileGL::Wire::RefuseCode::LinkTerms,
"invalid server link terms");
Stop();
return result;
}
if (welcome->backendType() != static_cast<Uint32>(MG_Config::ActiveBackendType)) {
const auto result = RefuseHandshake(*m_transport, ::MobileGL::Wire::RefuseCode::Backend,
"server backend differs from request", static_cast<Uint32>(MG_Config::ActiveBackendType),
welcome->backendType());
Stop();
return result;
}
stream = terms->dataPlane() == ::MobileGL::Wire::DataPlane::Stream;
negotiatedSizes.CmdRingBytes = terms->cmdWindowBytes();
negotiatedSizes.StageBytes = terms->stageWindowBytes();
negotiatedSizes.EventRingBytes = terms->eventWindowBytes();
negotiatedSizes.ReplyBytes = (terms->maxReplyBytes() + sizeof(Transport::ReplySlotHeader)) *
negotiatedSizes.ReplySlotCount;
const ::MobileGL::Wire::SegmentRef* refs[4] = {welcome->cmdRing(), welcome->stageRing(),
welcome->replyPool(),
welcome->eventRing()};
@@ -830,6 +894,14 @@ namespace MobileGL::MG_Remote::Client {
}
}
if (stream) {
const auto attached = AttachStreamLink(m_socketTransport->TakeDataFd(), negotiatedSizes);
if (attached != MOBILEGL_OK) { Stop(); return attached; }
m_controlInbox = std::make_unique<Transport::ControlInbox>(*m_transport);
Server::ServerLoopInstance().SetRemoteControlSink(&RemoteControlSinkThunk, this);
return FinishStartup(&m_link->ConsumerBell(), &m_link->ProducerBell(), false);
}
// ---- the four descriptors. THIS is the step inproc does not have, and the
// reason SCM_RIGHTS is in this design at all: the segments are anonymous -
// ASharedMemory_create has no filesystem name and memfd / shm_open+unlink
@@ -894,14 +966,14 @@ namespace MobileGL::MG_Remote::Client {
// ---- adopt and map. The same Adopt + Map every inproc run has
// exercised since P5; only the origin of the descriptor is new.
const MobileGLResult adopted = m_shm.AdoptFromDescriptors(
const MobileGLResult adopted = m_link->Memory().AdoptFromDescriptors(
fds, announced, replySlotCount, Transport::MemoryRole::Client);
if (adopted != MOBILEGL_OK) {
closeAll();
Stop();
return adopted;
}
// The four segment descriptors now belong to m_shm. The two bells
// The four segment descriptors now belong to m_link->Memory(). The two bells
// belong to the doorbells, which own and close them.
//
// WHICH BELL IS WHICH IS THE SESSION'S KNOWLEDGE, exactly as under
@@ -947,6 +1019,46 @@ namespace MobileGL::MG_Remote::Client {
return static_cast<ClientSession*>(user)->RunRemoteSurfaceControlFrame(frame);
}
MobileGLResult ClientSession::ReceiveControlFrame(std::vector<Uint8>& frame, Uint32 timeoutMs) {
return m_controlInbox ? m_controlInbox->Receive(frame, timeoutMs)
: ReceiveEnvelope(*m_transport, frame, timeoutMs);
}
void ClientSession::SyncPeerLog() {
if (!m_started || !m_controlInbox || !m_transport || DeviceLost()) return;
WaitForApplyToCatchUp("SyncPeerLog");
const std::lock_guard<std::mutex> lock(m_remoteControlMutex);
const auto seq = ++m_logFlushSeq;
flatbuffers::FlatBufferBuilder builder(64);
auto flush = ::MobileGL::Wire::CreateLogFlush(builder, seq, false);
auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(builder, ::MobileGL::Wire::CtrlMsg::LogFlush, flush.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope);
if (m_transport->SendFrame({builder.GetBufferPointer(), builder.GetSize()}) != MOBILEGL_OK) {
LatchDeviceLost("control stream closed during LogFlush");
return;
}
for (;;) {
std::vector<Uint8> bytes;
const auto received = ReceiveControlFrame(bytes, kRemoteControlTimeoutMs);
if (received == MOBILEGL_ERR_TRANSPORT_CLOSED) {
LatchDeviceLost("control stream closed during LogFlush");
return;
}
if (received != MOBILEGL_OK)
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, LogFlush} no acknowledgement rc=%d", received);
const auto* reply = ParseEnvelope(bytes);
if (!reply)
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, LogFlush} malformed acknowledgement");
if (const auto* ack = reply->msg_as_LogFlush()) {
if (!ack->ack() || ack->seq() != seq) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, LogFlush} reply sequence mismatch");
}
return;
}
if (const auto* caps = reply->msg_as_CapsSnapshot()) AdoptCapsSnapshot(caps);
}
}
MobileGLResult ClientSession::RunRemoteSurfaceControlFrame(Server::SurfaceControlFrame& frame) {
if (m_transport == nullptr) {
return MOBILEGL_ERR_NOT_INITIALIZED;
@@ -974,6 +1086,15 @@ namespace MobileGL::MG_Remote::Client {
// caller-serialised on the server's m_callerMutex under inproc; the same
// discipline has to hold here or two replies race for one correlation.
const std::lock_guard<std::mutex> lock(m_remoteControlMutex);
// Control and data are independent ordered streams. Finish the old
// command prefix before a surface operation can change its EGL tuple.
if (m_started && m_link && !m_link->Capabilities().PublishIsDelivery) {
const auto submitted = m_producer.LastPublishedSeq();
m_producer.PublishAndNotify(submitted);
if (WaitForAppliedBudget(m_producer, *m_events, submitted, kRemoteControlTimeoutMs) !=
Transport::SessionWait::Reached) return MOBILEGL_ERR_TRANSPORT_CLOSED;
DrainEventRing(*m_events);
}
const MobileGLResult sent = m_transport->SendFrame(
MobileGLByteSpan{builder.GetBufferPointer(), builder.GetSize()});
if (sent != MOBILEGL_OK) {
@@ -986,7 +1107,7 @@ namespace MobileGL::MG_Remote::Client {
for (;;) {
std::vector<Uint8> reply;
const MobileGLResult received =
ReceiveEnvelope(*m_transport, reply, kRemoteControlTimeoutMs);
ReceiveControlFrame(reply, kRemoteControlTimeoutMs);
if (received != MOBILEGL_OK) {
// `dl` (D5b): THE EXPIRY IS NOT FATAL AND IT IS NOT ONE ANSWER. The transport
// already distinguishes the two states this wait can end in, and the contract
@@ -1063,6 +1184,10 @@ namespace MobileGL::MG_Remote::Client {
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
MG_Remote::DecodeWireSurfaceReply(*wire, &frame);
if (m_link) {
const auto delivered = m_link->WaitForEventDelivery(frame.eventHead, kRemoteControlTimeoutMs);
if (delivered != MOBILEGL_OK) return delivered;
}
return MOBILEGL_OK;
}
}
@@ -1081,9 +1206,7 @@ namespace MobileGL::MG_Remote::Client {
MobileGLResult ClientSession::FinishStartup(Transport::Doorbell* peerBell,
Transport::Doorbell* selfBell,
bool startApplyThreadHere) {
Transport::RingControl* control = m_shm.CmdControl();
m_cmd = Transport::RingProducer(control, m_shm.CmdRingBase(), m_shm.CmdRingCapacity(),
Transport::RingCursorSet::Cmd);
m_link->InitializeEndpoints();
// NO STAGE RING. SEG_STAGE is package w1's encoder-local LINEAR ALLOCATOR:
// a staged byte run carries no RingRecordHeader, nothing consumes SEG_STAGE,
// and the allocator reclaims on retiredSeq. A RingProducer over
@@ -1091,23 +1214,22 @@ namespace MobileGL::MG_Remote::Client {
// two tails, so FreeBytes() would fall to zero the first time the head
// lapped the capacity and never recover - a guaranteed hang. See
// RingControl's stage triple in Ring.h.
if (!m_cmd.Valid()) {
if (!m_cmd->Valid()) {
Stop();
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// PeerDoorbell() is the bell the SERVER parks on and this side rings; SelfDoorbell() is
// this side's own. Which is which is the session's knowledge, not the transport's.
m_producer.Attach(control, &m_cmd, peerBell, selfBell, SpinUsFromConfig());
m_link->BindDoorbells(peerBell, selfBell);
peerBell = &m_link->ConsumerBell(); selfBell = &m_link->ProducerBell();
m_producer.Attach(*m_link, SpinUsFromConfig());
m_replies = Transport::ReplySlotPool(m_shm.ReplyBase(), m_shm.ReplyBytes(),
m_shm.ReplySlotCount());
m_events = Transport::EventRingConsumer(m_shm.EventControl(), control,
m_shm.EventRingBase(), m_shm.EventRingCapacity(),
m_shm.EventSegmentBase());
// P5e (ra, §2.6): the drain's other half. A server parked on a full SEG_EVENT is woken
// by the drain that emptied it, not by the next thing this client happens to publish.
m_events.SetServerDoorbell(peerBell);
if (!m_replies.Valid() || !m_events.Valid()) {
m_events->SetServerDoorbell(peerBell);
m_events->SetLink(m_link->Capabilities().PublishIsDelivery ? nullptr : m_link.get());
if (MaxReplyBytes() == 0 || !m_events->Valid()) {
Stop();
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
@@ -1128,21 +1250,9 @@ namespace MobileGL::MG_Remote::Client {
//
// The Install calls themselves are package w1's and are named-Fatal stubs until w1
// lands; see ServerSession::Accept for why they are called anyway.
m_segments.Install(Wire::kSegCmd,
Wire::SegmentView{m_shm.CmdRingBase(), m_shm.CmdRingCapacity()});
m_segments.Install(Wire::kSegStage,
Wire::SegmentView{m_shm.StageBase(), m_shm.StageBytes()});
m_segments.Install(Wire::kSegReply,
Wire::SegmentView{m_shm.ReplyBase(), m_shm.ReplyBytes()});
m_segments.Install(Wire::kSegEvent,
Wire::SegmentView{m_shm.EventSegmentBase(),
m_shm.AnnouncedSize(Transport::SessionSegmentSlot::Event)});
// nullptr for the stage producer, and that is the honest value: c0's
// signature predates w1's ruling that SEG_STAGE is a linear allocator, and
// the encoder reaches its bytes through the SegmentTable above. Handing it
// a live RingProducer over a cursor triple nobody consumes would be the
// half-wired shape this session exists not to have.
m_encoder = Wire::PipeWireEncoder(control, &m_cmd, nullptr, &m_segments);
m_segments.AttachLink(m_link.get());
m_encoder = Wire::PipeWireEncoder(m_link.get(), &m_segments);
m_encoder.SetLink(m_link->Capabilities().PublishIsDelivery ? nullptr : m_link.get());
m_encoder.SetStageRetirementDoorbell(m_producer.SelfDoorbell());
// P5e (ra, §2.6): the stage bell is a client wait and every client wait drains. The
// encoder owns the park; the session owns SEG_EVENT, so the session lends it a drain.
@@ -1171,6 +1281,7 @@ namespace MobileGL::MG_Remote::Client {
// to be true before the pointer that grants access to it is published. `Stop()` takes
// them down in the mirror order (m_started = false, then g_active = nullptr).
m_started = true;
Transport::LinkMetricsBegin();
g_active = this;
// D1c: THE SESSION-LIVE AND APPLY-THREAD FACTS, stated where they become true.
//
@@ -1239,6 +1350,8 @@ namespace MobileGL::MG_Remote::Client {
// monolith adapter runs is a defined no-op rather than a use-after-free.
UninstallClientWireTables();
if (!m_started) {
m_controlInbox.reset();
if (m_socketTransport) m_socketTransport->Shutdown();
// Start's own failure paths land here with a half-built session. FIVE of them are
// reached AFTER ServerSession::Accept has already returned OK, so tearing down
// only the client half is not enough and gets three things wrong at once: the
@@ -1249,7 +1362,9 @@ namespace MobileGL::MG_Remote::Client {
// still point at. The server closes FIRST, in the same order the started path
// gets right, and only then do the transports go.
m_producer.Detach();
m_shm.Close();
m_link = Transport::CreateSharedLink(Transport::TransportRoleTag::ClientProducer);
m_cmd = &m_link->CommandsOut(); m_events = &m_link->EventsIn();
m_link->Memory().Close();
Server::ServerSessionInstance().Close();
m_clientTransport.reset();
m_serverTransport.reset();
@@ -1264,7 +1379,7 @@ namespace MobileGL::MG_Remote::Client {
//
// 1. publish and let the server drain. Bounded: a lost record must be a red lane, not
// a hung exit.
Transport::RingControl* control = m_shm.CmdControl();
Transport::RingControl* control = m_link->Memory().CmdControl();
if (control != nullptr) {
// The PRODUCER's own last-published seq, not RingControl::submittedSeq. Ring.h:72-77
// permits submittedSeq to be published lazily and Ring.h:243 encourages batching
@@ -1291,6 +1406,7 @@ namespace MobileGL::MG_Remote::Client {
if (m_transport != nullptr) {
m_transport->Shutdown();
}
m_controlInbox.reset();
// 3. JOIN, bounded - package v1's ServerLoop::Stop, which also destroys the server's
// private BackendObject on that thread before it exits.
@@ -1300,12 +1416,14 @@ namespace MobileGL::MG_Remote::Client {
// referenced by an unapplied record is a use-after-free the join is what prevents.
LogMemory("teardown");
LogWireLedger();
Transport::LinkMetricsEnd();
m_producer.Detach();
m_encoder = Wire::PipeWireEncoder();
m_events = Transport::EventRingConsumer();
m_replies = Transport::ReplySlotPool();
m_cmd = Transport::RingProducer();
m_shm.Close();
*m_events = Transport::EventRingConsumer();
*m_cmd = Transport::RingProducer();
m_link = Transport::CreateSharedLink(Transport::TransportRoleTag::ClientProducer);
m_cmd = &m_link->CommandsOut(); m_events = &m_link->EventsIn();
m_link->Memory().Close();
Server::ServerSessionInstance().Close();
m_clientTransport.reset();
m_serverTransport.reset();
@@ -1412,6 +1530,10 @@ namespace MobileGL::MG_Remote::Client {
Wire::WireOpName(op));
}
const Bool rowCarriesReplySlot =
(MG_Pipe::MGPipeCallFlagsFor(op) & static_cast<Uint32>(MG_Pipe::kReplySlot)) != 0;
const Bool ownsReplySlot = rowCarriesReplySlot && wantReply;
const auto replyMetricStart = Transport::LinkMetricsBeginReply(ownsReplySlot);
Uint64 seq = m_encoder.EncodeRecord(op, payload, payloadBytes, tails, tailCount);
if (seq == Wire::kInvalidSeq) {
// EncodeRecord already refused individually oversized records. This one fits,
@@ -1419,7 +1541,7 @@ namespace MobileGL::MG_Remote::Client {
// including the wrap pad. Prior EmitAndWait calls have published every record.
Wire::WireRecordLayout layout{};
Wire::MGPipeWireRecordLayout(op, payload, layout);
const Uint64 toEnd = m_cmd.Capacity() - m_cmd.LocalHead() % m_cmd.Capacity();
const Uint64 toEnd = m_cmd->Capacity() - m_cmd->LocalHead() % m_cmd->Capacity();
const Uint64 needed = layout.TotalBytes + (toEnd < layout.TotalBytes ? toEnd : 0);
const BarrierWaitScope waiting;
const auto wait = m_producer.WaitForCmdSpace(needed, kBarrierTimeoutMs);
@@ -1434,7 +1556,7 @@ namespace MobileGL::MG_Remote::Client {
// here without draining is the other half of the flow-control deadlock: the server
// stops applying on a full event ring, so retiredSeq stops moving, so this wait
// never completes. One drain on the way out closes it.
DrainEventRing(m_events);
DrainEventRing(*m_events);
seq = m_encoder.EncodeRecord(op, payload, payloadBytes, tails, tailCount);
if (seq == Wire::kInvalidSeq) {
SessionFail(MGFatalFamily::RingOverrun, "MGPipe: Fatal{RingOverrun, \"SEG_CMD\"} - %s refused after sufficient "
@@ -1446,6 +1568,7 @@ namespace MobileGL::MG_Remote::Client {
// SessionProducer's one job and RingTest.cpp:446's pin. Notify-then-publish loses the
// wakeup.
m_producer.PublishAndNotify(seq);
if (op == MG_Pipe::MGPWireOp::Present) Transport::LinkMetricsPresent();
// Does this row own a reply slot? kMGPipeCallFlags IS THE SINGLE SOURCE OF TRUTH
// (R-16 / ID-31) - fourteen rows now, because the four Bool acceptance entry points
@@ -1456,9 +1579,7 @@ namespace MobileGL::MG_Remote::Client {
// The catalogue still says which rows CARRY a slot; a caller that passes false says it
// will not read this one (resource_subdata's buffer half, §2.5 - the only such caller).
// The server posts either way, so the wire is unchanged and the row keeps its flag.
const Bool rowCarriesReplySlot =
(MG_Pipe::MGPipeCallFlagsFor(op) & static_cast<Uint32>(MG_Pipe::kReplySlot)) != 0;
const Bool ownsReplySlot = rowCarriesReplySlot && wantReply;
// ---- THE P5e WAIT RULE (CONTRACT-P5E §2.3) ---------------------------------------
//
@@ -1564,7 +1685,7 @@ namespace MobileGL::MG_Remote::Client {
const BarrierWaitScope waiting;
const Uint64 waitBudgetMs = AppliedWaitBudgetMs(op, payload);
const Transport::SessionWait wait =
WaitForAppliedBudget(m_producer, m_events, seq, waitBudgetMs);
WaitForAppliedBudget(m_producer, *m_events, seq, waitBudgetMs);
if (wait == Transport::SessionWait::ShutDown) {
// `dl`: SHUTDOWN IS TWO DIFFERENT EVENTS AND ONLY ONE IS A DEVICE LOSS. Stop() kills
// this same bell to wake a parked applier, so an orderly exit arrives here too - and
@@ -1599,12 +1720,14 @@ namespace MobileGL::MG_Remote::Client {
static_cast<unsigned long long>(waitBudgetMs));
}
Transport::LinkMetricsReplyApplied(replyMetricStart);
// THE REVERSE CHANNEL IS DRAINED HERE, and here is the only place it can be: under the
// barrier this is the one instant at which the apply thread is known not to be inside
// the applier, and MGPipeClientOnGpuWritten / OnBufferWriteback write frontend objects.
// It is what gives AwaitBufferWriteback something to have waited FOR: b1's third state
// clears when the writeback lands, and the writeback lands on this ring.
DrainEventRing(m_events);
DrainEventRing(*m_events);
if (!ownsReplySlot) {
// P5e (ra, §2.3). A barriered record's apply has returned and this thread is the
@@ -1716,7 +1839,7 @@ namespace MobileGL::MG_Remote::Client {
if (target == 0) return;
const BarrierWaitScope waiting;
const Transport::SessionWait wait =
WaitForAppliedBudget(m_producer, m_events, target, kBarrierTimeoutMs);
WaitForAppliedBudget(m_producer, *m_events, target, kBarrierTimeoutMs);
if (wait == Transport::SessionWait::ShutDown) {
// `dl`: the same two-events distinction the verb barrier makes. Only a peer that
// HUNG UP is a device loss; Stop() reaches this path too.
@@ -1741,7 +1864,7 @@ namespace MobileGL::MG_Remote::Client {
// on: the server blocks only on a full event ring, the client blocks only on
// watermarks the server advances, and every client wait drains - so at most one side
// is parked at any instant.
DrainEventRing(m_events);
DrainEventRing(*m_events);
ReleaseFillPins();
#else
(void)why;
@@ -1791,7 +1914,7 @@ namespace MobileGL::MG_Remote::Client {
Transport::Watermark::Reached(control->Progress.presentAckSerial, awaited)) {
break;
}
DrainEventRing(m_events);
DrainEventRing(*m_events);
}
if (wait == Transport::SessionWait::TimedOut) {
SessionFail(MGFatalFamily::PresentCreditTimeout, "MGPipe: Fatal{PresentCreditTimeout} - present %llu waited %u ms for "
@@ -1803,7 +1926,7 @@ namespace MobileGL::MG_Remote::Client {
}
// ShutDown is teardown and returns: the present did not happen, the same answer
// the barrier gives on a dead doorbell.
if (wait == Transport::SessionWait::Reached) DrainEventRing(m_events);
if (wait == Transport::SessionWait::Reached) DrainEventRing(*m_events);
}
m_presentsSent = serial;
return serial;
@@ -1935,9 +2058,9 @@ namespace MobileGL::MG_Remote::Client {
// drains them would otherwise hold the GL thread here for ever, and a frame backlog
// deeper than this is a finding rather than a steady state.
for (Uint32 guard = 0; guard < kMaxControlFramesPerPump; ++guard) {
if (m_transport->PeekFrameSize() == 0) break;
if ((m_controlInbox ? m_controlInbox->Peek() : m_transport->PeekFrameSize()) == 0) break;
std::vector<Uint8> frame;
if (ReceiveEnvelope(*m_transport, frame, 0) != MOBILEGL_OK) break;
if (ReceiveControlFrame(frame, 0) != MOBILEGL_OK) break;
const ::MobileGL::Wire::CtrlEnvelope* envelope = ParseEnvelope(frame);
if (envelope == nullptr) {
MGLOG_E("MG_Remote client: an unverifiable control frame (%llu bytes) was dropped",
@@ -1965,6 +2088,22 @@ namespace MobileGL::MG_Remote::Client {
return adopted;
}
MobileGLResult ClientSession::AttachStreamLink(int fd, const Transport::SessionSegmentSizes& sizes) {
std::unique_ptr<Transport::ILink> link;
const auto attached = Transport::CreateStreamLink(fd, sizes, Transport::TransportRoleTag::ClientProducer, link);
if (attached != MOBILEGL_OK) return attached;
AttachDataLink(std::move(link));
return MOBILEGL_OK;
}
void ClientSession::AttachDataLink(std::unique_ptr<Transport::ILink> link) {
m_link = std::move(link);
m_cmd = &m_link->CommandsOut(); m_events = &m_link->EventsIn();
m_producer.SetLink(m_link->Capabilities().PublishIsDelivery ? nullptr : m_link.get());
m_encoder.SetLink(m_link->Capabilities().PublishIsDelivery ? nullptr : m_link.get());
m_events->SetLink(m_link->Capabilities().PublishIsDelivery ? nullptr : m_link.get());
}
Transport::SessionProducer& ClientSession::Producer() { return m_producer; }
Transport::SessionWait ClientSession::WaitForApplied(Uint64 seq, Uint32 timeoutMs) {
@@ -1973,38 +2112,48 @@ namespace MobileGL::MG_Remote::Client {
Bool ClientSession::ReadReply(Uint64 seq, void* outBytes, Uint64 outCapacity, Int32* outStatus,
Uint64* outSize) {
return m_replies.Read(seq, outBytes, outCapacity, outStatus, outSize);
const void* bytes = nullptr; Uint64 size = 0; Int32 status = 0;
if (m_link->ReadReply(seq, &status, &bytes, &size) != MOBILEGL_OK) return false;
if (outStatus) *outStatus = status;
if (outSize) *outSize = size;
if (size > outCapacity || (size && !outBytes)) return false;
if (size) std::memcpy(outBytes, bytes, static_cast<SizeT>(size));
return true;
}
Uint32 ClientSession::MaxReplyBytes() const { return m_replies.MaxReplyBytes(); }
Uint32 ClientSession::MaxReplyBytes() const { return static_cast<Uint32>(m_link->Capabilities().MaxReplyBytes); }
Bool ClientSession::ReplyCanHold(Uint64 bytes) const { return m_replies.CanHold(bytes); }
Bool ClientSession::ReplyCanHold(Uint64 bytes) const { return bytes <= MaxReplyBytes(); }
// ID-47. Forwarded verbatim so that the message, the boundary and the abort are the pool's
// and are pinned once, in SessionTest, rather than re-derived per caller.
void ClientSession::RequireReadPixelsReplyFits(Uint32 width, Uint32 height, Uint32 format,
Uint32 type, Uint64 bytes) const {
m_replies.RequireReadPixelsFits(width, height, format, type, bytes);
if (!ReplyCanHold(bytes)) {
SessionFail(MGFatalFamily::ReplyTooLarge,
"MGPipe: Fatal{ReplyTooLarge, \"ReadPixels %ux%u 0x%04X/0x%04X %llu > %u\"}",
width, height, format, type, static_cast<unsigned long long>(bytes), MaxReplyBytes());
}
}
Transport::EventRingConsumer& ClientSession::Events() { return m_events; }
Transport::EventRingConsumer& ClientSession::Events() { return *m_events; }
Uint32 ClientSession::DrainPublishedEvents() {
// Not gated on m_started on purpose: PumpControlPlane's own pre-start call during
// Start() has its ring-consumer twin here, and DrainEventRing's Valid() check is
// the whole guard either case needs.
return DrainEventRing(m_events);
return DrainEventRing(*m_events);
}
Uint64 ClientSession::EventRingCapacityBytes() const { return m_shm.EventRingCapacity(); }
Uint64 ClientSession::EventRingCapacityBytes() const { return m_link->Memory().EventRingCapacity(); }
Uint64 ClientSession::StageCapacityBytes() const { return m_shm.StageBytes(); }
Uint64 ClientSession::StageCapacityBytes() const { return m_link->Memory().StageBytes(); }
Wire::SegmentTable& ClientSession::Segments() { return m_segments; }
Transport::RingControl* ClientSession::Control() { return m_shm.CmdControl(); }
Transport::RingControl* ClientSession::Control() { return m_link->Memory().CmdControl(); }
Transport::SessionSegments& ClientSession::Shm() { return m_shm; }
Transport::SessionSegments& ClientSession::Shm() { return m_link->Memory(); }
Transport::ITransport* ClientSession::Control_Plane() { return m_transport; }
@@ -2071,3 +2220,7 @@ namespace MobileGL::MG_Remote::Client {
#undef MGP5_C0_STUB
} // namespace MobileGL::MG_Remote::Client
extern "C" __attribute__((visibility("default"))) void MGPipeSyncPeerLog() {
if (auto* session = MobileGL::MG_Remote::Client::ClientSession::Active()) session->SyncPeerLog();
}
+15 -4
View File
@@ -33,6 +33,8 @@
// defect P4a paid two contract corrections for, and "always accept" is ID-39's 66 lost uploads.
#pragma once
#include "../Transport/ILink.h"
#include "../Transport/ControlInbox.h"
#include <Includes.h>
#include <atomic> // the device-lost latch is read off the GL thread
@@ -96,7 +98,13 @@ namespace MobileGL::MG_Remote::Client {
// the server states its own pid, which IS ours, and that is the honest answer there.
std::uint32_t PeerServerPid() const { return m_peerServerPid; }
ClientSession();
~ClientSession();
MobileGLResult AttachStreamLink(int dataFd, const Transport::SessionSegmentSizes& sizes);
void AttachDataLink(std::unique_ptr<Transport::ILink> link);
Transport::ILink* DataLink() const { return m_link.get(); }
bool ConnectDial() const { return m_connectDial; }
void SyncPeerLog();
// Builds the four segments, performs Hello/Welcome, takes the first CapsSnapshot, and
// - for TransportMode::InProcess - starts the server role's apply thread. Returns a
@@ -407,6 +415,11 @@ namespace MobileGL::MG_Remote::Client {
void LogWireLedger() const;
private:
MobileGLResult ReceiveControlFrame(std::vector<Uint8>& frame, Uint32 timeoutMs);
std::unique_ptr<Transport::ControlInbox> m_controlInbox;
std::unique_ptr<Transport::ILink> m_link;
bool m_connectDial = false;
std::uint64_t m_logFlushSeq = 0;
// P5e (ra): the latch, taken from PumpControlPlane after every adoption. Private
// because "when may run-ahead start" is this class's own rule and not a caller's.
void LatchRunAheadFromCaps();
@@ -439,11 +452,9 @@ namespace MobileGL::MG_Remote::Client {
std::unique_ptr<Transport::InProcessTransport> m_clientTransport;
std::unique_ptr<Transport::InProcessTransport> m_serverTransport;
Transport::SessionSegments m_shm;
Transport::RingProducer m_cmd;
Transport::RingProducer* m_cmd = nullptr;
Transport::SessionProducer m_producer;
Transport::EventRingConsumer m_events;
Transport::ReplySlotPool m_replies;
Transport::EventRingConsumer* m_events = nullptr;
Transport::ITransport* m_transport = nullptr;
Bool m_started = false;
// Written by whichever thread first notices the hangup - the GL thread in the barrier,
+64
View File
@@ -0,0 +1,64 @@
// P6.5 handshake policy, shared by both roles. Rejections are returned, never aborted.
#pragma once
#include "CapsCodec.h"
#include "Protocol/generated/protocol_generated.h"
#include "Transport/ITransport.h"
#include <MG_Util/Debug/Log.h>
#include <cstdlib>
#include <cstring>
namespace MobileGL::MG_Remote {
inline MobileGLResult RefuseHandshake(Transport::ITransport& transport,
::MobileGL::Wire::RefuseCode code,
const char* detail, Uint64 expected = 0,
Uint64 actual = 0, const char* peerValue = nullptr) {
MGLOG_E("MGPipe: Refuse{%s} %s expected=%llu actual=%llu peer=%s",
::MobileGL::Wire::EnumNameRefuseCode(code), detail,
static_cast<unsigned long long>(expected), static_cast<unsigned long long>(actual),
peerValue == nullptr ? "" : peerValue);
::flatbuffers::FlatBufferBuilder builder(256);
auto refusal = ::MobileGL::Wire::CreateRefuseDirect(builder, code, detail, expected, actual, peerValue);
auto envelope = ::MobileGL::Wire::CreateCtrlEnvelope(builder, ::MobileGL::Wire::CtrlMsg::Refuse,
refusal.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope);
transport.SendFrame({builder.GetBufferPointer(), builder.GetSize()});
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
inline bool LogPeerRefusal(const ::MobileGL::Wire::CtrlEnvelope* envelope) {
if (envelope == nullptr || envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::Refuse) return false;
const auto* refusal = envelope->msg_as_Refuse();
if (refusal == nullptr) return false;
MGLOG_E("MGPipe: peer Refuse{%s} %s expected=%llu actual=%llu",
::MobileGL::Wire::EnumNameRefuseCode(refusal->code()),
refusal->detail() == nullptr ? "" : refusal->detail()->c_str(),
static_cast<unsigned long long>(refusal->expected()),
static_cast<unsigned long long>(refusal->actual()));
return true;
}
inline MobileGLResult ValidatePeerHandshake(Transport::ITransport& transport,
Uint32 major, Uint32 minor, Uint64 fingerprint,
const char* stamp, ::MobileGL::Wire::DialMode dial) {
using ::MobileGL::Wire::RefuseCode;
if (major != MOBILEGL_PROTOCOL_ABI_MAJOR || minor != MOBILEGL_PROTOCOL_ABI_MINOR)
return RefuseHandshake(transport, RefuseCode::ProtocolVersion, "protocol version",
MOBILEGL_ABI_VERSION(MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR),
MOBILEGL_ABI_VERSION(major, minor));
if (fingerprint != WireFingerprint())
return RefuseHandshake(transport, RefuseCode::WireFingerprint, "wire layout",
WireFingerprint(), fingerprint);
const bool sameBuild = BuildFingerprintPresent() && stamp != nullptr && stamp[0] != '\0' &&
std::strcmp(stamp, BuildFingerprint()) == 0;
const char* required = std::getenv("MOBILEGL_IPC_REQUIRE_SAME_BUILD");
const bool requireSame = dial == ::MobileGL::Wire::DialMode::Fork ||
(required != nullptr && std::strcmp(required, "1") == 0);
if (!sameBuild && requireSame)
return RefuseHandshake(transport, RefuseCode::BuildFingerprint, "build identity", 0, 0,
stamp == nullptr ? "<missing>" : stamp);
if (!sameBuild && dial == ::MobileGL::Wire::DialMode::Connect)
MGLOG_W("MGPipe: compatible wire with different build: local=%s peer=%s",
BuildFingerprint(), stamp == nullptr ? "<missing>" : stamp);
return MOBILEGL_OK;
}
} // namespace MobileGL::MG_Remote
@@ -190,7 +190,7 @@ namespace MobileGL::MG_Remote {
void EncodeSurfaceReplyFrame(const SurfaceControlFrame& frame,
flatbuffers::FlatBufferBuilder* builder) {
const auto reply = ::MobileGL::Wire::CreateSurfaceReply(*builder, frame.seq, frame.ok, frame.eglMajor,
frame.eglMinor, /*defaultFb=*/0);
frame.eglMinor, /*defaultFb=*/0, frame.eventHead);
const auto envelope =
::MobileGL::Wire::CreateCtrlEnvelope(*builder, ::MobileGL::Wire::CtrlMsg::SurfaceReply, reply.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(*builder, envelope);
@@ -201,6 +201,7 @@ namespace MobileGL::MG_Remote {
frame->ok = reply.ok();
frame->eglMajor = reply.eglMajor();
frame->eglMinor = reply.eglMinor();
frame->eventHead = reply.eventHead();
}
MobileGLResult ServerApplyWireSurfaceOp(const ::MobileGL::Wire::SurfaceOp& op, SurfaceControlFrame* replyOut) {
@@ -32,6 +32,15 @@ namespace Wire {
struct SegmentRef;
struct SegmentRefBuilder;
struct LinkTerms;
struct LinkTermsBuilder;
struct Refuse;
struct RefuseBuilder;
struct LogFlush;
struct LogFlushBuilder;
struct Hello;
struct HelloBuilder;
@@ -113,6 +122,147 @@ inline const char *EnumNameSegmentKind(SegmentKind e) {
return EnumNamesSegmentKind()[index];
}
enum class DataPlane : uint8_t {
SharedSegments = 0,
Stream = 1,
MIN = SharedSegments,
MAX = Stream
};
inline const DataPlane (&EnumValuesDataPlane())[2] {
static const DataPlane values[] = {
DataPlane::SharedSegments,
DataPlane::Stream
};
return values;
}
inline const char * const *EnumNamesDataPlane() {
static const char * const names[3] = {
"SharedSegments",
"Stream",
nullptr
};
return names;
}
inline const char *EnumNameDataPlane(DataPlane e) {
if (::flatbuffers::IsOutRange(e, DataPlane::SharedSegments, DataPlane::Stream)) return "";
const size_t index = static_cast<size_t>(e);
return EnumNamesDataPlane()[index];
}
enum class WireForm : uint8_t {
StructImage = 0,
MIN = StructImage,
MAX = StructImage
};
inline const WireForm (&EnumValuesWireForm())[1] {
static const WireForm values[] = {
WireForm::StructImage
};
return values;
}
inline const char * const *EnumNamesWireForm() {
static const char * const names[2] = {
"StructImage",
nullptr
};
return names;
}
inline const char *EnumNameWireForm(WireForm e) {
if (::flatbuffers::IsOutRange(e, WireForm::StructImage, WireForm::StructImage)) return "";
const size_t index = static_cast<size_t>(e);
return EnumNamesWireForm()[index];
}
enum class DialMode : uint8_t {
No = 0,
Fork = 1,
Connect = 2,
MIN = No,
MAX = Connect
};
inline const DialMode (&EnumValuesDialMode())[3] {
static const DialMode values[] = {
DialMode::No,
DialMode::Fork,
DialMode::Connect
};
return values;
}
inline const char * const *EnumNamesDialMode() {
static const char * const names[4] = {
"No",
"Fork",
"Connect",
nullptr
};
return names;
}
inline const char *EnumNameDialMode(DialMode e) {
if (::flatbuffers::IsOutRange(e, DialMode::No, DialMode::Connect)) return "";
const size_t index = static_cast<size_t>(e);
return EnumNamesDialMode()[index];
}
enum class RefuseCode : uint32_t {
None = 0,
ProtocolVersion = 1,
WireFingerprint = 2,
BuildFingerprint = 3,
Authentication = 4,
LinkTerms = 5,
Backend = 6,
Busy = 7,
MalformedHello = 8,
MIN = None,
MAX = MalformedHello
};
inline const RefuseCode (&EnumValuesRefuseCode())[9] {
static const RefuseCode values[] = {
RefuseCode::None,
RefuseCode::ProtocolVersion,
RefuseCode::WireFingerprint,
RefuseCode::BuildFingerprint,
RefuseCode::Authentication,
RefuseCode::LinkTerms,
RefuseCode::Backend,
RefuseCode::Busy,
RefuseCode::MalformedHello
};
return values;
}
inline const char * const *EnumNamesRefuseCode() {
static const char * const names[10] = {
"None",
"ProtocolVersion",
"WireFingerprint",
"BuildFingerprint",
"Authentication",
"LinkTerms",
"Backend",
"Busy",
"MalformedHello",
nullptr
};
return names;
}
inline const char *EnumNameRefuseCode(RefuseCode e) {
if (::flatbuffers::IsOutRange(e, RefuseCode::None, RefuseCode::MalformedHello)) return "";
const size_t index = static_cast<size_t>(e);
return EnumNamesRefuseCode()[index];
}
enum class SurfaceOpKind : uint8_t {
None = 0,
InitializeDisplay = 1,
@@ -350,11 +500,13 @@ enum class CtrlMsg : uint8_t {
AuxRequest = 8,
Fatal = 9,
LogLine = 10,
Refuse = 11,
LogFlush = 12,
MIN = NONE,
MAX = LogLine
MAX = LogFlush
};
inline const CtrlMsg (&EnumValuesCtrlMsg())[11] {
inline const CtrlMsg (&EnumValuesCtrlMsg())[13] {
static const CtrlMsg values[] = {
CtrlMsg::NONE,
CtrlMsg::Hello,
@@ -366,13 +518,15 @@ inline const CtrlMsg (&EnumValuesCtrlMsg())[11] {
CtrlMsg::ResyncDone,
CtrlMsg::AuxRequest,
CtrlMsg::Fatal,
CtrlMsg::LogLine
CtrlMsg::LogLine,
CtrlMsg::Refuse,
CtrlMsg::LogFlush
};
return values;
}
inline const char * const *EnumNamesCtrlMsg() {
static const char * const names[12] = {
static const char * const names[14] = {
"NONE",
"Hello",
"Welcome",
@@ -384,13 +538,15 @@ inline const char * const *EnumNamesCtrlMsg() {
"AuxRequest",
"Fatal",
"LogLine",
"Refuse",
"LogFlush",
nullptr
};
return names;
}
inline const char *EnumNameCtrlMsg(CtrlMsg e) {
if (::flatbuffers::IsOutRange(e, CtrlMsg::NONE, CtrlMsg::LogLine)) return "";
if (::flatbuffers::IsOutRange(e, CtrlMsg::NONE, CtrlMsg::LogFlush)) return "";
const size_t index = static_cast<size_t>(e);
return EnumNamesCtrlMsg()[index];
}
@@ -439,6 +595,14 @@ template<> struct CtrlMsgTraits<MobileGL::Wire::LogLine> {
static const CtrlMsg enum_value = CtrlMsg::LogLine;
};
template<> struct CtrlMsgTraits<MobileGL::Wire::Refuse> {
static const CtrlMsg enum_value = CtrlMsg::Refuse;
};
template<> struct CtrlMsgTraits<MobileGL::Wire::LogFlush> {
static const CtrlMsg enum_value = CtrlMsg::LogFlush;
};
template <bool B = false>
bool VerifyCtrlMsg(::flatbuffers::VerifierTemplate<B> &verifier, const void *obj, CtrlMsg type);
template <bool B = false>
@@ -538,6 +702,270 @@ inline ::flatbuffers::Offset<SegmentRef> CreateSegmentRefDirect(
name__);
}
struct LinkTerms FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
typedef LinkTermsBuilder Builder;
struct Traits;
enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
VT_DATAPLANE = 4,
VT_WIREFORM = 6,
VT_MAXREPLYBYTES = 8,
VT_CMDWINDOWBYTES = 10,
VT_STAGEWINDOWBYTES = 12,
VT_EVENTWINDOWBYTES = 14
};
MobileGL::Wire::DataPlane dataPlane() const {
return static_cast<MobileGL::Wire::DataPlane>(GetField<uint8_t>(VT_DATAPLANE, 0));
}
MobileGL::Wire::WireForm wireForm() const {
return static_cast<MobileGL::Wire::WireForm>(GetField<uint8_t>(VT_WIREFORM, 0));
}
uint64_t maxReplyBytes() const {
return GetField<uint64_t>(VT_MAXREPLYBYTES, 0);
}
uint64_t cmdWindowBytes() const {
return GetField<uint64_t>(VT_CMDWINDOWBYTES, 0);
}
uint64_t stageWindowBytes() const {
return GetField<uint64_t>(VT_STAGEWINDOWBYTES, 0);
}
uint64_t eventWindowBytes() const {
return GetField<uint64_t>(VT_EVENTWINDOWBYTES, 0);
}
template <bool B = false>
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
return VerifyTableStart(verifier) &&
VerifyField<uint8_t>(verifier, VT_DATAPLANE, 1) &&
VerifyField<uint8_t>(verifier, VT_WIREFORM, 1) &&
VerifyField<uint64_t>(verifier, VT_MAXREPLYBYTES, 8) &&
VerifyField<uint64_t>(verifier, VT_CMDWINDOWBYTES, 8) &&
VerifyField<uint64_t>(verifier, VT_STAGEWINDOWBYTES, 8) &&
VerifyField<uint64_t>(verifier, VT_EVENTWINDOWBYTES, 8) &&
verifier.EndTable();
}
};
struct LinkTermsBuilder {
typedef LinkTerms Table;
::flatbuffers::FlatBufferBuilder &fbb_;
::flatbuffers::uoffset_t start_;
void add_dataPlane(MobileGL::Wire::DataPlane dataPlane) {
fbb_.AddElement<uint8_t>(LinkTerms::VT_DATAPLANE, static_cast<uint8_t>(dataPlane), 0);
}
void add_wireForm(MobileGL::Wire::WireForm wireForm) {
fbb_.AddElement<uint8_t>(LinkTerms::VT_WIREFORM, static_cast<uint8_t>(wireForm), 0);
}
void add_maxReplyBytes(uint64_t maxReplyBytes) {
fbb_.AddElement<uint64_t>(LinkTerms::VT_MAXREPLYBYTES, maxReplyBytes, 0);
}
void add_cmdWindowBytes(uint64_t cmdWindowBytes) {
fbb_.AddElement<uint64_t>(LinkTerms::VT_CMDWINDOWBYTES, cmdWindowBytes, 0);
}
void add_stageWindowBytes(uint64_t stageWindowBytes) {
fbb_.AddElement<uint64_t>(LinkTerms::VT_STAGEWINDOWBYTES, stageWindowBytes, 0);
}
void add_eventWindowBytes(uint64_t eventWindowBytes) {
fbb_.AddElement<uint64_t>(LinkTerms::VT_EVENTWINDOWBYTES, eventWindowBytes, 0);
}
explicit LinkTermsBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
: fbb_(_fbb) {
start_ = fbb_.StartTable();
}
::flatbuffers::Offset<LinkTerms> Finish() {
const auto end = fbb_.EndTable(start_);
auto o = ::flatbuffers::Offset<LinkTerms>(end);
return o;
}
};
inline ::flatbuffers::Offset<LinkTerms> CreateLinkTerms(
::flatbuffers::FlatBufferBuilder &_fbb,
MobileGL::Wire::DataPlane dataPlane = MobileGL::Wire::DataPlane::SharedSegments,
MobileGL::Wire::WireForm wireForm = MobileGL::Wire::WireForm::StructImage,
uint64_t maxReplyBytes = 0,
uint64_t cmdWindowBytes = 0,
uint64_t stageWindowBytes = 0,
uint64_t eventWindowBytes = 0) {
LinkTermsBuilder builder_(_fbb);
builder_.add_eventWindowBytes(eventWindowBytes);
builder_.add_stageWindowBytes(stageWindowBytes);
builder_.add_cmdWindowBytes(cmdWindowBytes);
builder_.add_maxReplyBytes(maxReplyBytes);
builder_.add_wireForm(wireForm);
builder_.add_dataPlane(dataPlane);
return builder_.Finish();
}
struct LinkTerms::Traits {
using type = LinkTerms;
static auto constexpr Create = CreateLinkTerms;
};
struct Refuse FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
typedef RefuseBuilder Builder;
struct Traits;
enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
VT_CODE = 4,
VT_DETAIL = 6,
VT_EXPECTED = 8,
VT_ACTUAL = 10,
VT_PEERVALUE = 12
};
MobileGL::Wire::RefuseCode code() const {
return static_cast<MobileGL::Wire::RefuseCode>(GetField<uint32_t>(VT_CODE, 0));
}
const ::flatbuffers::String *detail() const {
return GetPointer<const ::flatbuffers::String *>(VT_DETAIL);
}
uint64_t expected() const {
return GetField<uint64_t>(VT_EXPECTED, 0);
}
uint64_t actual() const {
return GetField<uint64_t>(VT_ACTUAL, 0);
}
const ::flatbuffers::String *peerValue() const {
return GetPointer<const ::flatbuffers::String *>(VT_PEERVALUE);
}
template <bool B = false>
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
return VerifyTableStart(verifier) &&
VerifyField<uint32_t>(verifier, VT_CODE, 4) &&
VerifyOffset(verifier, VT_DETAIL) &&
verifier.VerifyString(detail()) &&
VerifyField<uint64_t>(verifier, VT_EXPECTED, 8) &&
VerifyField<uint64_t>(verifier, VT_ACTUAL, 8) &&
VerifyOffset(verifier, VT_PEERVALUE) &&
verifier.VerifyString(peerValue()) &&
verifier.EndTable();
}
};
struct RefuseBuilder {
typedef Refuse Table;
::flatbuffers::FlatBufferBuilder &fbb_;
::flatbuffers::uoffset_t start_;
void add_code(MobileGL::Wire::RefuseCode code) {
fbb_.AddElement<uint32_t>(Refuse::VT_CODE, static_cast<uint32_t>(code), 0);
}
void add_detail(::flatbuffers::Offset<::flatbuffers::String> detail) {
fbb_.AddOffset(Refuse::VT_DETAIL, detail);
}
void add_expected(uint64_t expected) {
fbb_.AddElement<uint64_t>(Refuse::VT_EXPECTED, expected, 0);
}
void add_actual(uint64_t actual) {
fbb_.AddElement<uint64_t>(Refuse::VT_ACTUAL, actual, 0);
}
void add_peerValue(::flatbuffers::Offset<::flatbuffers::String> peerValue) {
fbb_.AddOffset(Refuse::VT_PEERVALUE, peerValue);
}
explicit RefuseBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
: fbb_(_fbb) {
start_ = fbb_.StartTable();
}
::flatbuffers::Offset<Refuse> Finish() {
const auto end = fbb_.EndTable(start_);
auto o = ::flatbuffers::Offset<Refuse>(end);
return o;
}
};
inline ::flatbuffers::Offset<Refuse> CreateRefuse(
::flatbuffers::FlatBufferBuilder &_fbb,
MobileGL::Wire::RefuseCode code = MobileGL::Wire::RefuseCode::None,
::flatbuffers::Offset<::flatbuffers::String> detail = 0,
uint64_t expected = 0,
uint64_t actual = 0,
::flatbuffers::Offset<::flatbuffers::String> peerValue = 0) {
RefuseBuilder builder_(_fbb);
builder_.add_actual(actual);
builder_.add_expected(expected);
builder_.add_peerValue(peerValue);
builder_.add_detail(detail);
builder_.add_code(code);
return builder_.Finish();
}
struct Refuse::Traits {
using type = Refuse;
static auto constexpr Create = CreateRefuse;
};
inline ::flatbuffers::Offset<Refuse> CreateRefuseDirect(
::flatbuffers::FlatBufferBuilder &_fbb,
MobileGL::Wire::RefuseCode code = MobileGL::Wire::RefuseCode::None,
const char *detail = nullptr,
uint64_t expected = 0,
uint64_t actual = 0,
const char *peerValue = nullptr) {
auto detail__ = detail ? _fbb.CreateString(detail) : 0;
auto peerValue__ = peerValue ? _fbb.CreateString(peerValue) : 0;
return MobileGL::Wire::CreateRefuse(
_fbb,
code,
detail__,
expected,
actual,
peerValue__);
}
struct LogFlush FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
typedef LogFlushBuilder Builder;
struct Traits;
enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE {
VT_SEQ = 4,
VT_ACK = 6
};
uint64_t seq() const {
return GetField<uint64_t>(VT_SEQ, 0);
}
bool ack() const {
return GetField<uint8_t>(VT_ACK, 0) != 0;
}
template <bool B = false>
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
return VerifyTableStart(verifier) &&
VerifyField<uint64_t>(verifier, VT_SEQ, 8) &&
VerifyField<uint8_t>(verifier, VT_ACK, 1) &&
verifier.EndTable();
}
};
struct LogFlushBuilder {
typedef LogFlush Table;
::flatbuffers::FlatBufferBuilder &fbb_;
::flatbuffers::uoffset_t start_;
void add_seq(uint64_t seq) {
fbb_.AddElement<uint64_t>(LogFlush::VT_SEQ, seq, 0);
}
void add_ack(bool ack) {
fbb_.AddElement<uint8_t>(LogFlush::VT_ACK, static_cast<uint8_t>(ack), 0);
}
explicit LogFlushBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
: fbb_(_fbb) {
start_ = fbb_.StartTable();
}
::flatbuffers::Offset<LogFlush> Finish() {
const auto end = fbb_.EndTable(start_);
auto o = ::flatbuffers::Offset<LogFlush>(end);
return o;
}
};
inline ::flatbuffers::Offset<LogFlush> CreateLogFlush(
::flatbuffers::FlatBufferBuilder &_fbb,
uint64_t seq = 0,
bool ack = false) {
LogFlushBuilder builder_(_fbb);
builder_.add_seq(seq);
builder_.add_ack(ack);
return builder_.Finish();
}
struct LogFlush::Traits {
using type = LogFlush;
static auto constexpr Create = CreateLogFlush;
};
struct Hello FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
typedef HelloBuilder Builder;
struct Traits;
@@ -548,7 +976,11 @@ struct Hello FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
VT_BACKENDTYPE = 10,
VT_PID = 12,
VT_CONFIGBLOB = 14,
VT_ABIFINGERPRINT = 16
VT_ABIFINGERPRINT = 16,
VT_WIREFINGERPRINT = 18,
VT_LINKTERMS = 20,
VT_TOKEN = 22,
VT_DIALMODE = 24
};
uint32_t abiMajor() const {
return GetField<uint32_t>(VT_ABIMAJOR, 0);
@@ -571,6 +1003,18 @@ struct Hello FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
uint64_t abiFingerprint() const {
return GetField<uint64_t>(VT_ABIFINGERPRINT, 0);
}
uint64_t wireFingerprint() const {
return GetField<uint64_t>(VT_WIREFINGERPRINT, 0);
}
const MobileGL::Wire::LinkTerms *linkTerms() const {
return GetPointer<const MobileGL::Wire::LinkTerms *>(VT_LINKTERMS);
}
const ::flatbuffers::String *token() const {
return GetPointer<const ::flatbuffers::String *>(VT_TOKEN);
}
MobileGL::Wire::DialMode dialMode() const {
return static_cast<MobileGL::Wire::DialMode>(GetField<uint8_t>(VT_DIALMODE, 0));
}
template <bool B = false>
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
return VerifyTableStart(verifier) &&
@@ -583,6 +1027,12 @@ struct Hello FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
VerifyOffset(verifier, VT_CONFIGBLOB) &&
verifier.VerifyVector(configBlob()) &&
VerifyField<uint64_t>(verifier, VT_ABIFINGERPRINT, 8) &&
VerifyField<uint64_t>(verifier, VT_WIREFINGERPRINT, 8) &&
VerifyOffset(verifier, VT_LINKTERMS) &&
verifier.VerifyTable(linkTerms()) &&
VerifyOffset(verifier, VT_TOKEN) &&
verifier.VerifyString(token()) &&
VerifyField<uint8_t>(verifier, VT_DIALMODE, 1) &&
verifier.EndTable();
}
};
@@ -612,6 +1062,18 @@ struct HelloBuilder {
void add_abiFingerprint(uint64_t abiFingerprint) {
fbb_.AddElement<uint64_t>(Hello::VT_ABIFINGERPRINT, abiFingerprint, 0);
}
void add_wireFingerprint(uint64_t wireFingerprint) {
fbb_.AddElement<uint64_t>(Hello::VT_WIREFINGERPRINT, wireFingerprint, 0);
}
void add_linkTerms(::flatbuffers::Offset<MobileGL::Wire::LinkTerms> linkTerms) {
fbb_.AddOffset(Hello::VT_LINKTERMS, linkTerms);
}
void add_token(::flatbuffers::Offset<::flatbuffers::String> token) {
fbb_.AddOffset(Hello::VT_TOKEN, token);
}
void add_dialMode(MobileGL::Wire::DialMode dialMode) {
fbb_.AddElement<uint8_t>(Hello::VT_DIALMODE, static_cast<uint8_t>(dialMode), 0);
}
explicit HelloBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
: fbb_(_fbb) {
start_ = fbb_.StartTable();
@@ -631,15 +1093,23 @@ inline ::flatbuffers::Offset<Hello> CreateHello(
uint32_t backendType = 0,
uint32_t pid = 0,
::flatbuffers::Offset<::flatbuffers::Vector<uint8_t>> configBlob = 0,
uint64_t abiFingerprint = 0) {
uint64_t abiFingerprint = 0,
uint64_t wireFingerprint = 0,
::flatbuffers::Offset<MobileGL::Wire::LinkTerms> linkTerms = 0,
::flatbuffers::Offset<::flatbuffers::String> token = 0,
MobileGL::Wire::DialMode dialMode = MobileGL::Wire::DialMode::No) {
HelloBuilder builder_(_fbb);
builder_.add_wireFingerprint(wireFingerprint);
builder_.add_abiFingerprint(abiFingerprint);
builder_.add_token(token);
builder_.add_linkTerms(linkTerms);
builder_.add_configBlob(configBlob);
builder_.add_pid(pid);
builder_.add_backendType(backendType);
builder_.add_buildFingerprint(buildFingerprint);
builder_.add_abiMinor(abiMinor);
builder_.add_abiMajor(abiMajor);
builder_.add_dialMode(dialMode);
return builder_.Finish();
}
@@ -656,9 +1126,14 @@ inline ::flatbuffers::Offset<Hello> CreateHelloDirect(
uint32_t backendType = 0,
uint32_t pid = 0,
const std::vector<uint8_t> *configBlob = nullptr,
uint64_t abiFingerprint = 0) {
uint64_t abiFingerprint = 0,
uint64_t wireFingerprint = 0,
::flatbuffers::Offset<MobileGL::Wire::LinkTerms> linkTerms = 0,
const char *token = nullptr,
MobileGL::Wire::DialMode dialMode = MobileGL::Wire::DialMode::No) {
auto buildFingerprint__ = buildFingerprint ? _fbb.CreateString(buildFingerprint) : 0;
auto configBlob__ = configBlob ? _fbb.CreateVector<uint8_t>(*configBlob) : 0;
auto token__ = token ? _fbb.CreateString(token) : 0;
return MobileGL::Wire::CreateHello(
_fbb,
abiMajor,
@@ -667,7 +1142,11 @@ inline ::flatbuffers::Offset<Hello> CreateHelloDirect(
backendType,
pid,
configBlob__,
abiFingerprint);
abiFingerprint,
wireFingerprint,
linkTerms,
token__,
dialMode);
}
struct Welcome FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
@@ -682,7 +1161,10 @@ struct Welcome FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
VT_REPLYPOOL = 14,
VT_EVENTRING = 16,
VT_BUILDFINGERPRINT = 18,
VT_ABIFINGERPRINT = 20
VT_ABIFINGERPRINT = 20,
VT_WIREFINGERPRINT = 22,
VT_LINKTERMS = 24,
VT_BACKENDTYPE = 26
};
uint32_t abiMajor() const {
return GetField<uint32_t>(VT_ABIMAJOR, 0);
@@ -711,6 +1193,15 @@ struct Welcome FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
uint64_t abiFingerprint() const {
return GetField<uint64_t>(VT_ABIFINGERPRINT, 0);
}
uint64_t wireFingerprint() const {
return GetField<uint64_t>(VT_WIREFINGERPRINT, 0);
}
const MobileGL::Wire::LinkTerms *linkTerms() const {
return GetPointer<const MobileGL::Wire::LinkTerms *>(VT_LINKTERMS);
}
uint32_t backendType() const {
return GetField<uint32_t>(VT_BACKENDTYPE, 0);
}
template <bool B = false>
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
return VerifyTableStart(verifier) &&
@@ -728,6 +1219,10 @@ struct Welcome FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
VerifyOffset(verifier, VT_BUILDFINGERPRINT) &&
verifier.VerifyString(buildFingerprint()) &&
VerifyField<uint64_t>(verifier, VT_ABIFINGERPRINT, 8) &&
VerifyField<uint64_t>(verifier, VT_WIREFINGERPRINT, 8) &&
VerifyOffset(verifier, VT_LINKTERMS) &&
verifier.VerifyTable(linkTerms()) &&
VerifyField<uint32_t>(verifier, VT_BACKENDTYPE, 4) &&
verifier.EndTable();
}
};
@@ -763,6 +1258,15 @@ struct WelcomeBuilder {
void add_abiFingerprint(uint64_t abiFingerprint) {
fbb_.AddElement<uint64_t>(Welcome::VT_ABIFINGERPRINT, abiFingerprint, 0);
}
void add_wireFingerprint(uint64_t wireFingerprint) {
fbb_.AddElement<uint64_t>(Welcome::VT_WIREFINGERPRINT, wireFingerprint, 0);
}
void add_linkTerms(::flatbuffers::Offset<MobileGL::Wire::LinkTerms> linkTerms) {
fbb_.AddOffset(Welcome::VT_LINKTERMS, linkTerms);
}
void add_backendType(uint32_t backendType) {
fbb_.AddElement<uint32_t>(Welcome::VT_BACKENDTYPE, backendType, 0);
}
explicit WelcomeBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
: fbb_(_fbb) {
start_ = fbb_.StartTable();
@@ -784,9 +1288,15 @@ inline ::flatbuffers::Offset<Welcome> CreateWelcome(
::flatbuffers::Offset<MobileGL::Wire::SegmentRef> replyPool = 0,
::flatbuffers::Offset<MobileGL::Wire::SegmentRef> eventRing = 0,
::flatbuffers::Offset<::flatbuffers::String> buildFingerprint = 0,
uint64_t abiFingerprint = 0) {
uint64_t abiFingerprint = 0,
uint64_t wireFingerprint = 0,
::flatbuffers::Offset<MobileGL::Wire::LinkTerms> linkTerms = 0,
uint32_t backendType = 0) {
WelcomeBuilder builder_(_fbb);
builder_.add_wireFingerprint(wireFingerprint);
builder_.add_abiFingerprint(abiFingerprint);
builder_.add_backendType(backendType);
builder_.add_linkTerms(linkTerms);
builder_.add_buildFingerprint(buildFingerprint);
builder_.add_eventRing(eventRing);
builder_.add_replyPool(replyPool);
@@ -813,7 +1323,10 @@ inline ::flatbuffers::Offset<Welcome> CreateWelcomeDirect(
::flatbuffers::Offset<MobileGL::Wire::SegmentRef> replyPool = 0,
::flatbuffers::Offset<MobileGL::Wire::SegmentRef> eventRing = 0,
const char *buildFingerprint = nullptr,
uint64_t abiFingerprint = 0) {
uint64_t abiFingerprint = 0,
uint64_t wireFingerprint = 0,
::flatbuffers::Offset<MobileGL::Wire::LinkTerms> linkTerms = 0,
uint32_t backendType = 0) {
auto buildFingerprint__ = buildFingerprint ? _fbb.CreateString(buildFingerprint) : 0;
return MobileGL::Wire::CreateWelcome(
_fbb,
@@ -825,7 +1338,10 @@ inline ::flatbuffers::Offset<Welcome> CreateWelcomeDirect(
replyPool,
eventRing,
buildFingerprint__,
abiFingerprint);
abiFingerprint,
wireFingerprint,
linkTerms,
backendType);
}
struct CapsSnapshot FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
@@ -1211,7 +1727,8 @@ struct SurfaceReply FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
VT_OK = 6,
VT_EGLMAJOR = 8,
VT_EGLMINOR = 10,
VT_DEFAULTFB = 12
VT_DEFAULTFB = 12,
VT_EVENTHEAD = 14
};
uint64_t seq() const {
return GetField<uint64_t>(VT_SEQ, 0);
@@ -1228,6 +1745,9 @@ struct SurfaceReply FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
const MobileGL::Wire::DefaultFramebufferInfo *defaultFb() const {
return GetPointer<const MobileGL::Wire::DefaultFramebufferInfo *>(VT_DEFAULTFB);
}
uint64_t eventHead() const {
return GetField<uint64_t>(VT_EVENTHEAD, 0);
}
template <bool B = false>
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
return VerifyTableStart(verifier) &&
@@ -1237,6 +1757,7 @@ struct SurfaceReply FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
VerifyField<int32_t>(verifier, VT_EGLMINOR, 4) &&
VerifyOffset(verifier, VT_DEFAULTFB) &&
verifier.VerifyTable(defaultFb()) &&
VerifyField<uint64_t>(verifier, VT_EVENTHEAD, 8) &&
verifier.EndTable();
}
};
@@ -1260,6 +1781,9 @@ struct SurfaceReplyBuilder {
void add_defaultFb(::flatbuffers::Offset<MobileGL::Wire::DefaultFramebufferInfo> defaultFb) {
fbb_.AddOffset(SurfaceReply::VT_DEFAULTFB, defaultFb);
}
void add_eventHead(uint64_t eventHead) {
fbb_.AddElement<uint64_t>(SurfaceReply::VT_EVENTHEAD, eventHead, 0);
}
explicit SurfaceReplyBuilder(::flatbuffers::FlatBufferBuilder &_fbb)
: fbb_(_fbb) {
start_ = fbb_.StartTable();
@@ -1277,8 +1801,10 @@ inline ::flatbuffers::Offset<SurfaceReply> CreateSurfaceReply(
bool ok = false,
int32_t eglMajor = 0,
int32_t eglMinor = 0,
::flatbuffers::Offset<MobileGL::Wire::DefaultFramebufferInfo> defaultFb = 0) {
::flatbuffers::Offset<MobileGL::Wire::DefaultFramebufferInfo> defaultFb = 0,
uint64_t eventHead = 0) {
SurfaceReplyBuilder builder_(_fbb);
builder_.add_eventHead(eventHead);
builder_.add_seq(seq);
builder_.add_defaultFb(defaultFb);
builder_.add_eglMinor(eglMinor);
@@ -1656,6 +2182,12 @@ struct CtrlEnvelope FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table {
const MobileGL::Wire::LogLine *msg_as_LogLine() const {
return msg_type() == MobileGL::Wire::CtrlMsg::LogLine ? static_cast<const MobileGL::Wire::LogLine *>(msg()) : nullptr;
}
const MobileGL::Wire::Refuse *msg_as_Refuse() const {
return msg_type() == MobileGL::Wire::CtrlMsg::Refuse ? static_cast<const MobileGL::Wire::Refuse *>(msg()) : nullptr;
}
const MobileGL::Wire::LogFlush *msg_as_LogFlush() const {
return msg_type() == MobileGL::Wire::CtrlMsg::LogFlush ? static_cast<const MobileGL::Wire::LogFlush *>(msg()) : nullptr;
}
template <bool B = false>
bool Verify(::flatbuffers::VerifierTemplate<B> &verifier) const {
return VerifyTableStart(verifier) &&
@@ -1706,6 +2238,14 @@ template<> inline const MobileGL::Wire::LogLine *CtrlEnvelope::msg_as<MobileGL::
return msg_as_LogLine();
}
template<> inline const MobileGL::Wire::Refuse *CtrlEnvelope::msg_as<MobileGL::Wire::Refuse>() const {
return msg_as_Refuse();
}
template<> inline const MobileGL::Wire::LogFlush *CtrlEnvelope::msg_as<MobileGL::Wire::LogFlush>() const {
return msg_as_LogFlush();
}
struct CtrlEnvelopeBuilder {
typedef CtrlEnvelope Table;
::flatbuffers::FlatBufferBuilder &fbb_;
@@ -1788,6 +2328,14 @@ inline bool VerifyCtrlMsg(::flatbuffers::VerifierTemplate<B> &verifier, const vo
auto ptr = reinterpret_cast<const MobileGL::Wire::LogLine *>(obj);
return verifier.VerifyTable(ptr);
}
case CtrlMsg::Refuse: {
auto ptr = reinterpret_cast<const MobileGL::Wire::Refuse *>(obj);
return verifier.VerifyTable(ptr);
}
case CtrlMsg::LogFlush: {
auto ptr = reinterpret_cast<const MobileGL::Wire::LogFlush *>(obj);
return verifier.VerifyTable(ptr);
}
default: return true;
}
}
+54 -12
View File
@@ -56,6 +56,47 @@ table SegmentRef {
// Handshake
// ---------------------------------------------------------------------------
// P6.5: independent control and data-plane choices. Values are append-only.
enum DataPlane : ubyte { SharedSegments = 0, Stream = 1 }
enum WireForm : ubyte { StructImage = 0 }
enum DialMode : ubyte { No = 0, Fork = 1, Connect = 2 }
// Window sizes are stated by the server; the client's values are proposals only.
table LinkTerms {
dataPlane: DataPlane;
wireForm: WireForm;
maxReplyBytes: ulong;
cmdWindowBytes: ulong;
stageWindowBytes: ulong;
eventWindowBytes: ulong;
}
enum RefuseCode : uint {
None = 0,
ProtocolVersion = 1,
WireFingerprint = 2,
BuildFingerprint = 3,
Authentication = 4,
LinkTerms = 5,
Backend = 6,
Busy = 7,
MalformedHello = 8,
}
// A handshake rejection is a normal session outcome, never a process abort.
table Refuse {
code: RefuseCode;
detail: string;
expected: ulong;
actual: ulong;
peerValue: string;
}
table LogFlush {
seq: ulong;
ack: bool;
}
table Hello {
abiMajor: uint;
abiMinor: uint;
@@ -63,15 +104,12 @@ table Hello {
backendType: uint;
pid: uint;
configBlob: [ubyte];
// CONTRACT-P5 table 0, "ABI agreement": MGPCaps has only a COMPOSITIONAL size
// assertion, because DynamicBackendParameters still carries SizeT and GLenum.
// So the two peers assert they were built from the same struct shapes instead
// of rewriting them fixed-width (that is P7's account). This is
// MG_Remote::CapsAbiFingerprint(): sizeof(DynamicBackendParameters),
// sizeof(MGPCaps), sizeof(GLFunctionsTable), the protocol ABI version and the
// build's git stamp, mixed. A mismatch is Fatal{AbiMismatch} and NEVER a
// downgrade - every alternative silently reads one struct as another.
// Retained legacy slot. P6.5 peers compare wireFingerprint instead.
abiFingerprint: ulong;
wireFingerprint: ulong;
linkTerms: LinkTerms;
token: string;
dialMode: DialMode;
}
table Welcome {
@@ -82,12 +120,12 @@ table Welcome {
stageRing: SegmentRef;
replyPool: SegmentRef;
eventRing: SegmentRef;
// The server's half of the assertion above. The string is carried beside the
// mixed value only so a mismatch can name both builds in the Fatal line; the
// COMPARISON is on abiFingerprint, which also covers the three sizeofs the
// string cannot.
buildFingerprint: string;
// Retained legacy slot; do not reuse or renumber it.
abiFingerprint: ulong;
wireFingerprint: ulong;
linkTerms: LinkTerms;
backendType: uint;
}
// ---------------------------------------------------------------------------
@@ -221,6 +259,8 @@ table SurfaceReply {
eglMajor: int;
eglMinor: int;
defaultFb: DefaultFramebufferInfo;
// P6.5: all preceding data-plane events must arrive before this reply is observed.
eventHead: ulong;
}
// ---------------------------------------------------------------------------
@@ -306,6 +346,8 @@ union CtrlMsg {
AuxRequest,
Fatal,
LogLine,
Refuse,
LogFlush,
}
table CtrlEnvelope {
+15
View File
@@ -62,6 +62,12 @@ namespace MobileGL::MG_Remote::Server {
// The view is rebuilt per call rather than stored, so that this body does not change
// ReplyPool's four members and therefore does not touch v1's header at all.
void ReplyPool::PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) {
if (m_link) {
const auto result = m_link->PostReply(seq, status, bytes, size);
if (result == MOBILEGL_ERR_BUFFER_TOO_SMALL)
SessionFail(MGFatalFamily::ReplyTooLarge, "MGPipe: Fatal{ReplyTooLarge, stream reply}");
return;
}
Transport::ReplySlotPool pool(m_base, m_size, m_slots);
// Fatal inside Post when the answer does not fit a slot: P5 does not chunk replies,
// and the client knows an answer's size before it emits the record.
@@ -1192,6 +1198,15 @@ namespace MobileGL::MG_Remote::Server {
PipeApplier::PipeApplier(Wire::SegmentTable* segments, ReplyPool* replies)
: m_segments(segments), m_replies(replies) {}
void PipeApplier::Attach(Transport::ILink* link, MG_Backend::BackendObject* backend) {
if (!link || !link->Attached() || !m_segments)
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, PipeApplier::Attach missing link}");
m_verbs.SetBackend(backend);
m_decoder = Wire::PipeWireDecoder(link, m_segments, m_replies);
m_decoder.SetVerbSink(&m_verbs);
MG_Pipe::MGPipeServerBlockNoteIdentity(); m_attached = true;
}
void PipeApplier::Attach(Transport::RingControl* control, MG_Backend::BackendObject* backend) {
if (control == nullptr || m_segments == nullptr) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"PipeApplier::Attach\"} - no control "
+3
View File
@@ -89,6 +89,7 @@ namespace MobileGL::MG_Remote::Server {
ReplyPool() = default;
ReplyPool(void* base, Uint64 sizeBytes, Uint32 slotCount, Uint32 slotBytes);
void SetLink(Transport::ILink* link) { m_link = link; }
void PostReply(Uint64 seq, Int32 status, const void* bytes, Uint64 size) override;
// A reply larger than one slot is Fatal rather than chunked: P5's only large answer is
@@ -97,6 +98,7 @@ namespace MobileGL::MG_Remote::Server {
Uint32 SlotBytes() const;
private:
Transport::ILink* m_link = nullptr;
Uint8* m_base = nullptr;
Uint64 m_size = 0;
Uint32 m_slots = 0;
@@ -350,6 +352,7 @@ namespace MobileGL::MG_Remote::Server {
// CALLED ON THE APPLY THREAD, ONCE, BEFORE THE FIRST RECORD. PipeWireDecoder is "not
// thread safe: one decoder on the apply thread, by construction", and its constructor
// installs the process-wide apply hook.
void Attach(Transport::ILink* link, MG_Backend::BackendObject* backend);
void Attach(Transport::RingControl* control, MG_Backend::BackendObject* backend);
void Detach();
Bool Attached() const;
+12 -10
View File
@@ -372,10 +372,10 @@ namespace MobileGL::MG_Remote::Server {
// The decoder is built HERE, on this thread, because PipeWireDecoder is "not thread
// safe: one decoder on the apply thread, by construction" and its constructor installs
// the process-wide apply hook.
session.Applier().Attach(&session.Control(), m_backend.get());
session.Applier().Attach(session.DataLink(), m_backend.get());
Transport::RingControl& control = session.Control();
Transport::Doorbell& bell = session.ConsumerDoorbell();
const auto signals = session.DataLink()->Signals();
Transport::Doorbell& bell = session.DataLink()->ConsumerBell();
Transport::RingConsumer& ring = session.CommandRing();
const Uint32 spinUs = SpinUsFromConfig();
@@ -400,16 +400,16 @@ namespace MobileGL::MG_Remote::Server {
// this bell (that pairing is the whole of the flow-control protocol; a cleared flag
// with no bell is the lost wakeup the forward direction's publish-then-ring order
// exists to prevent).
const auto ready = [this, &control, &ring] {
const auto ready = [this, signals, &ring] {
if (m_stopRequested.load(std::memory_order_acquire) || ControlIsPending()) return true;
if (control.eventRingFull.load(std::memory_order_acquire) != 0) return false;
return control.cmdHead.load(std::memory_order_acquire) != ring.LocalTail();
if (signals.EventRingFull->load(std::memory_order_acquire) != 0) return false;
return signals.CmdHead->load(std::memory_order_acquire) != ring.LocalTail();
};
for (;;) {
PumpControlRequest();
if (m_stopRequested.load(std::memory_order_acquire)) break;
if (control.eventRingFull.load(std::memory_order_acquire) == 0) DrainRing();
if (signals.EventRingFull->load(std::memory_order_acquire) == 0) DrainRing();
if (m_stopRequested.load(std::memory_order_acquire)) break;
if (ready()) continue;
@@ -436,7 +436,9 @@ namespace MobileGL::MG_Remote::Server {
MG_Util::PipeStats::PublishGauge(Gauge::ServerWaits, waits);
MG_Util::PipeStats::PublishGauge(Gauge::ServerParks, ParkBlockCount());
}
const bool woke = bell.Wait(control.consumerParked, ready, spinUs,
// Flush-on-idle publishes the last reply record even below the batch threshold.
session.FlushDataProgress();
const bool woke = bell.Wait(*signals.ConsumerParked, ready, spinUs,
Transport::kWaitForever, &m_parkBlocks);
if (!woke) {
// Wait returns false only on a dead bell or an expired deadline, and this park
@@ -735,7 +737,7 @@ namespace MobileGL::MG_Remote::Server {
// it atomically, so the apply thread cannot observe the request until this side is
// waiting, and the doorbell remembers the notify regardless.
if (m_session != nullptr) {
m_session->ConsumerDoorbell().Notify();
m_session->DataLink()->ConsumerBell().Notify();
}
m_controlDone.wait(lock, [this] { return m_controlFinished; });
frame = m_controlFrame; // the reply half, written by the dispatch
@@ -786,7 +788,7 @@ namespace MobileGL::MG_Remote::Server {
// The bell may already be dead - ClientSession::Stop calls transport->Shutdown()
// first, which is what table 3's step 2 requires - and Notify on a dead bell is
// harmless. Ringing anyway covers the paths that Stop without a Kill.
m_session->ConsumerDoorbell().Notify();
m_session->DataLink()->ConsumerBell().Notify();
}
// THE JOIN IS BOUNDED. std::thread::join has no deadline, so a lost wakeup would wedge
+175 -353
View File
@@ -1,316 +1,117 @@
// MobileGL - MobileGL/MG_Remote/Server/ServerMain.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
// MobileGL - separate-process server and TCP session supervisor.
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The server process's entry point. Package `sm` (CONTRACT-P6.md §3).
//
// ARCHITECTURE.md:488-496 has described this symbol for three phases and a6
// found it existed nowhere in the tree. This is the real one.
//
// THE TWO PROCESSES ARE INDEPENDENT. This process is started by whoever starts
// it - a shell, a test, an Android Service - and it LISTENS. The client is
// started separately and CONNECTS. Nothing is inherited: no fds, no memory, no
// parent.
//
// That is a deliberate change from this package's first shape, which forked the
// server and handed it fds 3..6. Inheritance works and is less code, but it can
// only ever produce a server that is a CHILD OF ITS CLIENT - the one arrangement
// the end state cannot use, because there the server is a standing application
// and the client is somewhere else entirely, possibly on another kernel.
// CONTRACT-P6 §3.1 calls this Dial == Connect and had it as a later shape. It is
// the primary one.
//
// WHAT THIS PACKAGE OWNS: the process, the rendezvous, the backend, the session,
// the descriptor hand-off and the exit discipline. NOT what a control frame
// means - that is `cp` - and not the data-plane seam, which is `lk`.
//
// _exit, NOT exit (CONTRACT-P6 §12.4). It decides whether g_processTeardown is
// ever set, and therefore whether seven twin destructors take their early-out or
// call into a driver whose context is already gone. The log is flushed first,
// because _exit runs no atexit handler and a server that died without saying why
// is the diagnostic this project refuses.
#include "../Transport/Doorbell.h" // kWaitForever, SocketDoorbell
#include "../Transport/FdPassing.h"
#include "../Transport/Doorbell.h"
#include "../Transport/SocketTransport.h"
#include "../Transport/WireLog.h"
#include "../Protocol/SurfaceOpCodec.h"
#include "ServerLoop.h"
#include "ServerSession.h"
#include "SurfaceControlFrame.h"
#include <Config.h> // MG_Config::Transport - D1 pins it to Spawn in this process
#include <MG_Backend/MGPipe/PipeInputs.h> // D1c: MGPipeSetServerProcessRole
#include <Config.h>
#include <Init.h>
#include <MG_Backend/MGPipe/PipeInputs.h>
#include <MG_Backend/ServerRole.h>
#include <MG_Util/Metrics/PipeStats.h> // step 1.6: the counters' latch, server-process half
#include <Init.h> // MG_ConfigLoader::Init - the child loads its own config (step 1.5)
#include <MG_Util/Debug/Log.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
#if defined(__unix__) || defined(__APPLE__) || defined(__ANDROID__)
# include <sys/socket.h>
# include <unistd.h>
#endif
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
namespace {
using namespace MobileGL::MG_Remote;
using namespace MobileGL::MG_Remote::Transport;
namespace Protocol = ::MobileGL::Wire;
// The server must not be able to become a client. CONTRACT-P6 §3.1: this is
// catch (a), STRUCTURAL and separate from role selection, because forcing
// MG_Config::Transport to Monolith - what ARCHITECTURE.md:488 says - would
// flip 148 MG_Backend lines to the frontend arm in the one process with no
// frontend. Catch (b) is the launcher's envp scrub, and the two report
// through DIFFERENT exit codes so they are distinguishable: two safeties
// that cannot be told apart are one safety.
bool DialingIsForbidden() {
const char* dial = std::getenv("MOBILEGL_IPC_DIAL");
return dial != nullptr && std::strcmp(dial, "no") == 0;
}
void Refuse(SocketTransport& transport, Protocol::RefuseCode code, const char* detail) {
flatbuffers::FlatBufferBuilder builder(256);
auto refusal = Protocol::CreateRefuseDirect(builder, code, detail);
auto envelope = Protocol::CreateCtrlEnvelope(builder, Protocol::CtrlMsg::Refuse, refusal.Union());
Protocol::FinishCtrlEnvelopeBuffer(builder, envelope);
transport.SendFrame({builder.GetBufferPointer(), builder.GetSize()});
WireLogError("MG_Remote server: Refuse{%s} %s", Protocol::EnumNameRefuseCode(code), detail);
}
bool LooksLikeAClientEnvironment() {
static constexpr const char* kNames[] = {"MOBILEGL_TRANSPORT", "MOBILEGL_IPC_SERVER_PATH",
"MOBILEGL_IPC_RING_MB", "MOBILEGL_IPC_STAGE_MB"};
for (const char* name : kNames) {
if (std::getenv(name) != nullptr) {
return true;
}
}
return false;
}
void ForwardLog(void* user, const char* text) {
auto& transport = *static_cast<ITransport*>(user);
flatbuffers::FlatBufferBuilder builder(512);
auto log = Protocol::CreateLogLine(builder, Protocol::LogLevel::Info, builder.CreateString(text));
auto envelope = Protocol::CreateCtrlEnvelope(builder, Protocol::CtrlMsg::LogLine, log.Union());
Protocol::FinishCtrlEnvelopeBuffer(builder, envelope);
(void)transport.SendFrame({builder.GetBufferPointer(), builder.GetSize()});
}
// The rendezvous. A NAME, because independent processes have no other way to
// find each other. No default: a default rendezvous is one that two unrelated
// runs can collide on.
std::string Endpoint(int argc, char** argv) {
if (argc > 1 && argv[1] != nullptr && argv[1][0] != '\0') {
return argv[1];
}
if (const char* fromEnv = std::getenv("MOBILEGL_IPC_ENDPOINT")) {
return fromEnv;
}
return std::string();
}
} // namespace
extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int argc, char** argv) {
using namespace MobileGL::MG_Remote;
using namespace MobileGL::MG_Remote::Transport;
struct FlushAck { ITransport* transport; std::uint64_t seq; };
void SendLogAck(void* pointer) {
auto& ack = *static_cast<FlushAck*>(pointer);
flatbuffers::FlatBufferBuilder builder(64);
auto flush = Protocol::CreateLogFlush(builder, ack.seq, true);
auto envelope = Protocol::CreateCtrlEnvelope(builder, Protocol::CtrlMsg::LogFlush, flush.Union());
Protocol::FinishCtrlEnvelopeBuffer(builder, envelope);
(void)ack.transport->SendFrame({builder.GetBufferPointer(), builder.GetSize()});
}
// Called only after fork: the supervisor never creates a backend or EGL context.
[[noreturn]] void RunSession(std::unique_ptr<SocketTransport> control) {
const int selfPid = static_cast<int>(::getpid());
// The arm-proof line (CONTRACT-P6 §9.5). ID-124 proved a lane can run
// monolith and pass every other gate, so each entry leaves its own evidence.
WireLogError("MG_Remote server: pid=%d transport=spawn role=server", selfPid);
if (!DialingIsForbidden()) {
std::fprintf(stderr,
"MG_Remote server: pid=%d refusing to start - MOBILEGL_IPC_DIAL=no was not "
"set; anti-recursion catch (a) is missing\n",
selfPid);
std::fflush(nullptr);
::_exit(64);
const bool tcp = control->IsTcp();
std::vector<std::uint8_t> helloFrame;
std::uint64_t bytes = 0;
auto received = control->ReceiveFrame({nullptr, 0}, &bytes, 10000);
if (received != MOBILEGL_ERR_BUFFER_TOO_SMALL || bytes > 1024 * 1024) ::_exit(67);
helloFrame.resize(static_cast<std::size_t>(bytes));
if (control->ReceiveFrame({helloFrame.data(), helloFrame.size()}, &bytes, 0) != MOBILEGL_OK) ::_exit(67);
flatbuffers::Verifier verifier(helloFrame.data(), helloFrame.size());
if (!Protocol::VerifyCtrlEnvelopeBuffer(verifier)) ::_exit(67);
const auto* hello = Protocol::GetCtrlEnvelope(helloFrame.data())->msg_as_Hello();
if (!hello) ::_exit(67);
if (tcp) {
const char* expected = std::getenv("MOBILEGL_IPC_TOKEN");
const std::string token = hello->token() ? hello->token()->str() : std::string();
if (token != (expected ? expected : "")) {
Refuse(*control, Protocol::RefuseCode::Authentication, "TCP token mismatch");
::_exit(0);
}
}
if (LooksLikeAClientEnvironment()) {
std::fprintf(stderr,
"MG_Remote server: pid=%d the envp scrub FAILED - a client knob survived "
"into this process; catch (a) held, catch (b) did not\n",
selfPid);
std::fflush(nullptr);
::_exit(65);
}
const std::string endpoint = Endpoint(argc, argv);
if (endpoint.empty()) {
std::fprintf(stderr,
"MG_Remote server: pid=%d no endpoint - pass one as argv[1] or set "
"MOBILEGL_IPC_ENDPOINT\n",
selfPid);
std::fflush(nullptr);
::_exit(71);
}
// ---- 1. listen, BEFORE the backend. The client's bounded connect retry is
// what tolerates the backend taking a while; a client that connected and then
// waited for a Welcome a failed bring-up will never send would have to learn
// about death from a timeout, which §5.4 forbids.
int listenFd = -1;
if (SocketTransport::Listen(endpoint, &listenFd) != MOBILEGL_OK) {
std::fprintf(stderr, "MG_Remote server: pid=%d could not listen on \"%s\"\n", selfPid,
endpoint.c_str());
std::fflush(nullptr);
::_exit(72);
}
WireLogError("MG_Remote server: pid=%d listening on \"%s\"", selfPid, endpoint.c_str());
// ---- 1.5. CONFIG, AND THEN D1.
//
// TWO SEPARATE DEFECTS LIVED HERE, and both were silent.
//
// (a) NOTHING LOADED THE CONFIG AT ALL. mobilegl_server_main calls
// InitServerRoleForSpawn directly rather than MobileGL::Initialize, and
// MG_ConfigLoader::Init is a step of the latter - so every MG_Config
// value in this process was its STATIC DEFAULT. MOBILEGL_BACKEND_TYPE
// survives the launcher's scrub on purpose, and it was being read by
// nobody: a DirectVulkan session would have brought up a DirectGLES
// server and then disagreed with its client about which backend the
// capability mask describes.
//
// (b) THE CHILD RESOLVED Transport == Monolith. CONTRACT-P6 §3.1 (D1) says
// it must read as Spawn, and a6 measured the cost of getting it wrong:
// 226 live lines test `Transport != Monolith` to select the SERVER arm,
// 148 of them in MG_Backend. Monolith flips all of them to frontend glue
// in the one process that has no frontend. Observed, exactly as
// predicted: TextureImpl::UnitTexturesByHandle() answered false, Clear
// took the legacy arm that dereferences borrowed FRONTEND slot pointers,
// and the apply thread took a SIGSEGV inside SyncNeccessaryTextures on
// the first Clear record of the OpenRA retrace.
//
// IT IS SET HERE AND NOT THROUGH THE ENVIRONMENT because the environment is
// where it must NOT appear: LooksLikeAClientEnvironment above exits 65 if
// MOBILEGL_TRANSPORT survived into this process, and that check is
// anti-recursion catch (b). D1's own answer is that Dial - not Transport -
// is the anti-recursion axis, so Transport is free to say what is true.
MobileGL::MG_ConfigLoader::Init();
MobileGL::MG_Config::Transport = MobileGL::MG_Config::TransportMode::Spawn;
// D1c/D10: THIS PROCESS IS THE SERVER, stated once and before any GL work.
//
// Four guards used to ask "am I on the apply thread" or "is a ClientSession active" - both
// questions a server PROCESS answers wrongly: it has no ClientSession at all, and every one
// of its threads is a server thread, not just the applier's. Saying it here is what arms
// them; without it they are compiled in and permanently false, which is worse than absent
// because it reads as coverage.
MobileGL::MG_Pipe::MGPipeSetServerProcessRole(true);
// ---- 1.6. THE MGPIPE COUNTERS, and this is the ONLY call site outside
// MobileGL::Initialize().
//
// GATE 8'S SERVER HALF WAS STRUCTURALLY ZERO WITHOUT THIS, and the shape of the
// defect is worth stating: the child never runs MobileGL::Initialize, so
// PipeStats::Init() - a step of it - never ran here, and g_pipeStatsEnabled
// stayed false. Every one of the ~80 `if (Enabled())` sites in the backends and
// in MG_Remote (ServerLoop's own ServerWaits/ServerParks publish included)
// compiled in and was permanently false in this process. The contract's
// mandatory "socket doorbell vs inproc condvar" number therefore had no server
// half under `spawn` at all, and its absence read exactly like a real zero:
// `wait[srv=0 srvpark=0]` is what "the server never waited" looks like too.
//
// IT GOES AFTER THE CONFIG LOAD AND AFTER THE ROLE IS STATED, and both halves
// of that order are load-bearing:
//
// * AFTER MG_ConfigLoader::Init, because Init() LATCHES the flag out of
// MG_Config::Features.PipeStats, and the parser does not exist until the
// line above has run. Calling it earlier would latch the static default
// (false) and the MOBILEGL_PIPE_STATS the launcher passed would be read by
// nobody - the same class of defect the config load itself used to have.
// * AFTER MGPipeSetServerProcessRole, because that is what makes the LOG SINK
// know this process is the server, and PipeStats' summary line is emitted
// through MGLOG_I. The sink resolves its role from MOBILEGL_IPC_ROLE (which
// the launcher sets and its envp scrub preserves) and would in fact be
// right either way, but ordering it this way means the line cannot be filed
// under the client by a future change to that resolution. What the contract
// requires - that the summary lands in `<base>.server.log` - is asserted by
// the lane, not assumed from this comment.
//
// MOBILEGL_PIPE_STATS / _PERIOD / _FILE MEAN EXACTLY WHAT THEY MEAN IN THE
// CLIENT PROCESS, and that is the whole point of routing through Init(): all
// three are read from the child's own environment by the same parser and
// latched by the same function. Nothing about the stats channel is spawn-shaped.
// One asymmetry is real and intended: this process has no backend Present of
// its own, so its line cadence rides the present RECORDS it applies
// (PipeApplier -> DirectGLES/DirectVulkan Present -> OnPresent) rather than a
// swap the server does not perform. A spawn session that presents N times
// produces N windows here, which is the same N the client's own line counts.
// The child bypasses MobileGL::Initialize: arm its own role counters.
MobileGL::MG_Util::PipeStats::Init();
WireLogError("MG_Remote server: pid=%d config loaded, backend=%d, Transport pinned to Spawn "
"(D1: this process IS the server arm)",
selfPid, static_cast<int>(MobileGL::MG_Config::ActiveBackendType));
// ---- 2. the backend, through the SAME entry point the inproc server role
// uses (MG_Backend/ServerRole.h). No EGL yet: the context is created on the
// apply thread when the client's first eglMakeCurrent crosses.
if (!MobileGL::MG_Backend::InitServerRoleForSpawn()) {
std::fprintf(stderr,
"MG_Remote server: pid=%d could not create a backend; refusing to accept a "
"session it could never apply\n",
selfPid);
::close(listenFd);
::unlink(endpoint.c_str());
std::fflush(nullptr);
::_exit(66);
const auto backend = static_cast<MobileGL::BackendType>(hello->backendType());
if (backend != MobileGL::BackendType::DirectGLES && backend != MobileGL::BackendType::DirectVulkan) {
Refuse(*control, Protocol::RefuseCode::Backend, "unsupported Hello.backendType");
::_exit(0);
}
// ---- 3. accept the client's TWO connections: control, then aux.
std::unique_ptr<SocketTransport> control;
if (SocketTransport::AcceptPair(listenFd, 30000, control) != MOBILEGL_OK) {
std::fprintf(stderr, "MG_Remote server: pid=%d no client within 30 s\n", selfPid);
::close(listenFd);
::unlink(endpoint.c_str());
std::fflush(nullptr);
::_exit(73);
const char* pinned = std::getenv("MOBILEGL_BACKEND_TYPE");
if (pinned && *pinned && backend != MobileGL::MG_Config::ActiveBackendType) {
Refuse(*control, Protocol::RefuseCode::Backend, "Hello.backendType disagrees with pinned backend");
::_exit(0);
}
// One session per process (CONTRACT-P6 §12.3), so the door closes behind the
// one client that came through it. A second would collide on four process
// globals and on DirectGLES's single per-process EGL tuple.
::close(listenFd);
::unlink(endpoint.c_str());
// ---- 4. the two bells, created HERE and handed over. NOT inherited: there
// is no parent to inherit from. One socketpair per DIRECTION, because
// SocketDoorbell is "write a byte to wake the peer, poll to be woken" and a
// single pair would have each side reading the byte it wrote to the other.
int serverBell[2] = {-1, -1};
int clientBell[2] = {-1, -1};
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, serverBell) != 0 ||
::socketpair(AF_UNIX, SOCK_STREAM, 0, clientBell) != 0) {
std::fprintf(stderr, "MG_Remote server: pid=%d bell socketpair failed\n", selfPid);
std::fflush(nullptr);
::_exit(74);
MobileGL::MG_Config::ActiveBackendType = backend;
if (!MobileGL::MG_Backend::InitServerRoleForSpawn()) ::_exit(66);
auto& session = Server::ServerSessionInstance();
int serverBell[2] = {-1, -1}, clientBell[2] = {-1, -1};
std::unique_ptr<SocketDoorbell> selfBell, peerBell;
if (!tcp) {
if (::socketpair(AF_UNIX, SOCK_STREAM, 0, serverBell) != 0 ||
::socketpair(AF_UNIX, SOCK_STREAM, 0, clientBell) != 0) ::_exit(74);
selfBell = std::make_unique<SocketDoorbell>(serverBell[0], serverBell[1], 1, true);
peerBell = std::make_unique<SocketDoorbell>(-1, clientBell[1], 1, true);
session.SetExternalDoorbells(selfBell.get(), peerBell.get());
}
// PARK AND NOTIFY ON DIFFERENT DESCRIPTORS, and this is not a refinement -
// the single-fd form CANNOT WAKE ITSELF. send() on one end of a socketpair
// is delivered to the OTHER end, so a one-fd bell built on serverBell[0]
// writes to serverBell[1] and polls serverBell[0]: correct for waking a
// PEER, useless for waking a thread in this process. The server needs
// exactly that, because the control pump posts into the apply thread's
// mailbox and then has to ring the bell that thread is parked on. Measured
// symptom when it was wrong: every eglInitialize crossed, landed in the
// mailbox, and timed out at 5 s because nothing ever woke mgl-srv-apply.
SocketDoorbell selfBell(/*parkFd=*/serverBell[0], /*notifyFd=*/serverBell[1], /*code=*/1,
/*ownsFds=*/true);
// RINGS ONLY. The client parks on clientBell[0]; writing clientBell[1] is
// what reaches it. parkFd = -1 says in the type that this side never waits.
SocketDoorbell peerBell(/*parkFd=*/-1, /*notifyFd=*/clientBell[1], /*code=*/1,
/*ownsFds=*/true);
Server::ServerSession& session = Server::ServerSessionInstance();
session.SetExternalDoorbells(&selfBell, &peerBell);
// ---- 5. Accept: the ABI assertion, the four segments, and Welcome.
const MobileGLResult accepted = session.Accept(*control);
const auto accepted = session.Accept(*control, &helloFrame);
if (accepted != MOBILEGL_OK) {
std::fprintf(stderr, "MG_Remote server: pid=%d Accept failed (rc=%d)\n", selfPid,
static_cast<int>(accepted));
WireLogError("MG_Remote server: pid=%d Accept failed (rc=%d)", selfPid, static_cast<int>(accepted));
std::fflush(nullptr);
::_exit(67);
::_exit(accepted == MOBILEGL_ERR_PROTOCOL_MISMATCH ? 0 : 67);
}
// ---- 6. hand SIX descriptors over. THIS IS WHY SCM_RIGHTS IS IN THIS
// DESIGN: the segments are anonymous - ASharedMemory_create has no filesystem
// name at all, and memfd / shm_open+unlink are not openable either
// (ShmSegmentPosix.cpp says so) - so the descriptor IS the only key, and this
// process is the side that created them.
//
// The slot travels in the sideband rather than being inferred from arrival
// order, so a reordered or dropped offer is a NAMED mismatch on arrival
// instead of two segments quietly swapped.
if (!tcp)
{
struct Sideband {
std::uint32_t slot;
@@ -359,87 +160,108 @@ extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int a
::close(clientBell[0]);
}
// ---- 7. the apply thread. It holds the native context for its whole life,
// which is what turns MakeCurrent's cache-invalidation storm into a one-off.
Server::ServerLoop& loop = Server::ServerLoopInstance();
if (loop.Start(session) != MOBILEGL_OK) {
std::fprintf(stderr, "MG_Remote server: pid=%d the apply thread did not start\n", selfPid);
std::fflush(nullptr);
::_exit(70);
}
WireLogError("MG_Remote server: pid=%d transport=spawn role=server ready "
"(backend up, seven descriptors handed over, apply thread running)",
selfPid);
// ---- 8. the control pump. EOF is the whole exit condition: no timeout may
// ever stand in for the death fact (§5.4), because a server one frame behind
// is the intended steady state and a server that is gone is a different thing.
const char* forwarding = std::getenv("MOBILEGL_IPC_LOG_FORWARD");
if (tcp && (!forwarding || std::strcmp(forwarding, "0") != 0))
MobileGL::MG_Util::Debug::SetLogForwarder(&ForwardLog, control.get());
auto& loop = Server::ServerLoopInstance();
if (loop.Start(session) != MOBILEGL_OK) ::_exit(70);
WireLogError("MG_Remote server: pid=%d transport=spawn role=server ready control=%s data=%s",
selfPid, tcp ? "tcp" : "unix", tcp ? "stream" : "shm");
std::vector<std::uint8_t> buffer(64 * 1024);
for (;;) {
std::uint64_t size = 0;
MobileGLMutableByteSpan span{buffer.data(), buffer.size()};
const MobileGLResult result = control->ReceiveFrame(span, &size, kWaitForever);
if (result == MOBILEGL_ERR_TRANSPORT_CLOSED) {
WireLogError("MG_Remote server: pid=%d peer closed the control stream; exiting",
selfPid);
break;
const auto result = control->ReceiveFrame({buffer.data(), buffer.size()}, &size, kWaitForever);
if (result == MOBILEGL_ERR_BUFFER_TOO_SMALL) { buffer.resize(static_cast<std::size_t>(size)); continue; }
if (result != MOBILEGL_OK) break;
flatbuffers::Verifier check(buffer.data(), static_cast<std::size_t>(size));
if (!Protocol::VerifyCtrlEnvelopeBuffer(check)) break;
const auto* envelope = Protocol::GetCtrlEnvelope(buffer.data());
if (const auto* flush = envelope->msg_as_LogFlush()) {
if (flush->ack()) break;
FlushAck ack{control.get(), flush->seq()};
MobileGL::MG_Util::Debug::WithLogBarrier(&SendLogAck, &ack);
continue;
}
if (result == MOBILEGL_ERR_BUFFER_TOO_SMALL) {
buffer.resize(static_cast<std::size_t>(size));
continue; // still queued: ITransport's contract
}
if (result != MOBILEGL_OK) {
WireLogError("MG_Remote server: pid=%d control stream failed (rc=%d); exiting", selfPid,
static_cast<int>(result));
break;
}
// `cp`: decode, apply, reply. THIS THREAD IS NOT THE APPLY THREAD, which
// is what CONTRACT-P6 §6.2 requires - a control arrival must not wake the
// applier, and frame decode must not happen on the thread holding the
// native context. ServerApplyWireSurfaceOp posts through ServerLoop's
// existing one-slot mailbox, unchanged.
const ::MobileGL::Wire::CtrlEnvelope* envelope = nullptr;
{
::flatbuffers::Verifier verifier(buffer.data(), static_cast<std::size_t>(size));
if (::MobileGL::Wire::VerifyCtrlEnvelopeBuffer(verifier)) {
envelope = ::MobileGL::Wire::GetCtrlEnvelope(buffer.data());
}
}
if (envelope == nullptr || envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::SurfaceOp ||
envelope->msg_as_SurfaceOp() == nullptr) {
WireLogError("MG_Remote server: pid=%d a control frame that is not a verifiable "
"SurfaceOp arrived; ignoring it would be the silent drop this project "
"refuses, so the session ends here",
selfPid);
break;
}
const auto* operation = envelope->msg_as_SurfaceOp();
if (!operation) break;
Server::SurfaceControlFrame reply{};
const MobileGLResult applied =
ServerApplyWireSurfaceOp(*envelope->msg_as_SurfaceOp(), &reply);
(void)applied; // carried in the reply's ok/result, not thrown away here
::flatbuffers::FlatBufferBuilder builder(256);
(void)ServerApplyWireSurfaceOp(*operation, &reply);
session.FlushDataProgress();
if (session.DataLink()) reply.eventHead = session.DataLink()->EventPublishedHead();
flatbuffers::FlatBufferBuilder builder(256);
EncodeSurfaceReplyFrame(reply, &builder);
if (control->SendFrame(
MobileGLByteSpan{builder.GetBufferPointer(), builder.GetSize()}) != MOBILEGL_OK) {
break;
}
if (control->SendFrame({builder.GetBufferPointer(), builder.GetSize()}) != MOBILEGL_OK) break;
}
loop.Stop();
session.Close();
control->Shutdown();
// Step 1.6's other half, and its position is the same argument MobileGL::Destroy
// makes: AFTER loop.Stop(), because the apply thread is what publishes
// ServerWaits / ServerParks and a dump taken while it still runs is a race; and
// BEFORE _exit, because _exit runs no atexit handler and this is the only place
// the server's run totals and its JSON dump can be written at all. Without it
// the server printed whatever windows its last period boundary happened to
// reach and lost the final one - the same "the last frame's numbers can be
// lost" case Init.cpp's Shutdown call exists to avoid on the client side.
// Publish the final server window while log forwarding is still attached.
MobileGL::MG_Util::PipeStats::Shutdown();
session.Close();
MobileGL::MG_Util::Debug::SetLogForwarder(nullptr, nullptr);
control->Shutdown();
std::fflush(nullptr);
::_exit(0);
}
}
extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int argc, char** argv) {
const char* dial = std::getenv("MOBILEGL_IPC_DIAL");
if (!dial || std::strcmp(dial, "no") != 0) {
std::fprintf(stderr, "MG_Remote server: MOBILEGL_IPC_DIAL=no anti-recursion catch (a) missing\n");
return 64;
}
for (const char* name : {"MOBILEGL_TRANSPORT", "MOBILEGL_IPC_SERVER_PATH", "MOBILEGL_IPC_RING_MB", "MOBILEGL_IPC_STAGE_MB"}) {
if (std::getenv(name)) {
std::fprintf(stderr, "MG_Remote server: envp scrub FAILED, catch (b): %s\n", name);
return 65;
}
}
std::string endpoint;
bool serve = false;
unsigned maxSessions = 0;
for (int i = 1; i < argc; ++i) {
const std::string arg(argv[i]);
if (arg == "--serve") serve = true;
else if (arg == "--max-sessions" && i + 1 < argc) {
char* end = nullptr;
const auto count = std::strtoul(argv[++i], &end, 10);
if (*end || count == 0 || count > 1000000) return 71;
maxSessions = static_cast<unsigned>(count);
} else if (endpoint.empty() && arg.compare(0, 2, "--") != 0) endpoint = arg;
else return 71;
}
if (endpoint.empty()) if (const char* value = std::getenv("MOBILEGL_IPC_ENDPOINT")) endpoint = value;
if (endpoint.empty()) return 71;
int listener = -1;
if (SocketTransport::Listen(endpoint, &listener) != MOBILEGL_OK) return 72;
WireLogError("MG_Remote server: pid=%d listening on %s", static_cast<int>(::getpid()), endpoint.c_str());
pid_t active = -1;
unsigned sessions = 0;
for (;;) {
if (active > 0 && ::waitpid(active, nullptr, WNOHANG) == active) active = -1;
if (maxSessions && sessions >= maxSessions && active < 0) break;
std::unique_ptr<SocketTransport> control;
const auto accepted = SocketTransport::AcceptPair(listener, serve ? 250 : 30000, control);
if (accepted == MOBILEGL_ERR_TIMEOUT && serve) continue;
if (accepted != MOBILEGL_OK) { ::close(listener); return 73; }
if (!serve) {
::close(listener);
if (endpoint[0] != '@' && endpoint.compare(0, 6, "tcp://") != 0) ::unlink(endpoint.c_str());
RunSession(std::move(control));
}
if (active > 0 && ::waitpid(active, nullptr, WNOHANG) == active) active = -1;
if (active > 0 || (maxSessions && sessions >= maxSessions)) {
Refuse(*control, Protocol::RefuseCode::Busy, "one session is already active");
continue;
}
std::fflush(nullptr);
const pid_t child = ::fork();
if (child == 0) { ::close(listener); RunSession(std::move(control)); }
if (child < 0) { Refuse(*control, Protocol::RefuseCode::Busy, "could not create session child"); continue; }
active = child;
++sessions;
control->CloseLocalCopy();
}
::close(listener);
if (endpoint[0] != '@' && endpoint.compare(0, 6, "tcp://") != 0) ::unlink(endpoint.c_str());
return 0;
}
+108 -80
View File
@@ -8,10 +8,12 @@
// P5 package s1: the server half of a session.
#include "../Transport/SocketTransport.h"
#include "ServerSession.h"
#include <MG_Remote/FatalFunnel.h>
#include "../CapsCodec.h"
#include "../Handshake.h"
#include "../Protocol/generated/protocol_generated.h"
#include "../Transport/InProcessTransport.h"
@@ -109,23 +111,6 @@ namespace MobileGL::MG_Remote::Server {
return ::MobileGL::Wire::GetCtrlEnvelope(bytes.data());
}
[[noreturn]] void FatalAbiMismatch(const char* what, Uint64 ours, Uint64 theirs,
const char* ourStamp, const char* theirStamp) {
// NEVER a downgrade. Every alternative to aborting here reads one struct as
// another - MGPCaps has only a compositional size assertion because
// DynamicBackendParameters still carries SizeT, so a peer built from a different
// tree hands over a caps block whose members are at different offsets and whose
// bytes are all individually plausible.
SessionFail(MGFatalFamily::AbiMismatch, "MGPipe: Fatal{AbiMismatch, \"%s\"} ours=%llu theirs=%llu ourBuild=%s "
"theirBuild=%s - the two peers were not built from the same struct shapes, "
"and there is no downgrade path: the caps block's size is ABI-dependent "
"(MGPipeTypes.h:145-146) and every field past the first difference would be "
"read at the wrong offset",
what, static_cast<unsigned long long>(ours),
static_cast<unsigned long long>(theirs),
ourStamp == nullptr ? "?" : ourStamp, theirStamp == nullptr ? "?" : theirStamp);
}
// MOBILEGL_IPC_RING_MB / MOBILEGL_IPC_STAGE_MB, actually applied.
//
// The first version of this file called this only `if (m_sizes.CmdBytes == 0)`, and
@@ -246,17 +231,17 @@ namespace MobileGL::MG_Remote::Server {
static_cast<unsigned long long>(cap));
}
Transport::RingControl& control = session.Control();
Transport::Doorbell& bell = session.ConsumerDoorbell();
const auto drained = [&control] {
return control.eventRingFull.load(std::memory_order_acquire) == 0;
const auto signals = session.DataLink()->Signals();
Transport::Doorbell& bell = session.DataLink()->ConsumerBell();
const auto drained = [signals] {
return signals.EventRingFull->load(std::memory_order_acquire) == 0;
};
for (Uint32 round = 0; round < 2; ++round) {
// PUBLISH AND RING FIRST. The client cannot drain a ring whose head it has not
// been shown, and the latch Reserve just set is only useful to a client that
// gets a bell with it.
session.PublishEvents();
if (!drained() && !bell.Wait(control.consumerParked, drained, /*spinUs=*/0,
if (!drained() && !bell.Wait(*signals.ConsumerParked, drained, /*spinUs=*/0,
kEventBacklogWaitMs)) {
SessionFail(MGFatalFamily::EventRingOverflow, "MGPipe: Fatal{EventRingOverflow} - %s waited %u ms for the client "
"to drain SEG_EVENT and it never did. Under run-ahead the apply "
@@ -394,6 +379,11 @@ namespace MobileGL::MG_Remote::Server {
ServerSession* ServerSession::Active() { return g_active.load(std::memory_order_acquire); }
ServerSession::ServerSession() {
m_link = Transport::CreateSharedLink(Transport::TransportRoleTag::ServerConsumer);
m_commands = &m_link->CommandsIn(); m_events = &m_link->EventsOut();
}
ServerSession::~ServerSession() { Close(); }
void ServerSession::SetSegmentSizes(const Transport::SessionSegmentSizes& sizes) {
@@ -429,7 +419,8 @@ namespace MobileGL::MG_Remote::Server {
Bool ServerSession::Accepted() const { return m_accepted; }
MobileGLResult ServerSession::Accept(Transport::ITransport& transport) {
MobileGLResult ServerSession::Accept(Transport::ITransport& transport,
const std::vector<Uint8>* firstFrame) {
ServerSession* expectedOwner = nullptr;
if (!g_sessionOwner.compare_exchange_strong(expectedOwner, this,
std::memory_order_acq_rel, std::memory_order_acquire)) {
@@ -457,7 +448,9 @@ namespace MobileGL::MG_Remote::Server {
// ---- 1/2. the ABI assertion, BEFORE a single record is decoded and before a byte of
// shared memory exists. Its whole purpose is to refuse to interpret the peer's bytes.
std::vector<Uint8> frame;
const MobileGLResult received = ReceiveEnvelope(transport, frame, kHandshakeTimeoutMs);
if (firstFrame != nullptr) frame = *firstFrame;
const MobileGLResult received = firstFrame != nullptr ? MOBILEGL_OK
: ReceiveEnvelope(transport, frame, kHandshakeTimeoutMs);
if (received != MOBILEGL_OK) {
MGLOG_E("MG_Remote server: no Hello within %u ms (rc=%d)", kHandshakeTimeoutMs,
static_cast<int>(received));
@@ -472,57 +465,69 @@ namespace MobileGL::MG_Remote::Server {
if (envelope == nullptr || envelope->msg_type() != ::MobileGL::Wire::CtrlMsg::Hello ||
envelope->msg_as_Hello() == nullptr) {
MGLOG_E("MG_Remote server: the first control frame is not a verifiable Hello");
return MOBILEGL_ERR_PROTOCOL_MISMATCH;
return RefuseHandshake(transport, ::MobileGL::Wire::RefuseCode::MalformedHello,
"first control frame is not a verifiable Hello");
}
const ::MobileGL::Wire::Hello* hello = envelope->msg_as_Hello();
const char* theirStamp =
hello->buildFingerprint() == nullptr ? nullptr : hello->buildFingerprint()->c_str();
if (hello->abiMajor() != static_cast<Uint32>(MOBILEGL_PROTOCOL_ABI_MAJOR) ||
hello->abiMinor() != static_cast<Uint32>(MOBILEGL_PROTOCOL_ABI_MINOR)) {
FatalAbiMismatch("protocol version",
MOBILEGL_ABI_VERSION(MOBILEGL_PROTOCOL_ABI_MAJOR,
MOBILEGL_PROTOCOL_ABI_MINOR),
MOBILEGL_ABI_VERSION(hello->abiMajor(), hello->abiMinor()),
GIT_COMMIT_HASH_SHORT, theirStamp);
}
const Uint64 ourFingerprint = CapsAbiFingerprint();
if (hello->abiFingerprint() != ourFingerprint) {
FatalAbiMismatch("struct shapes", ourFingerprint, hello->abiFingerprint(),
GIT_COMMIT_HASH_SHORT, theirStamp);
}
if (hello->dialMode() != ::MobileGL::Wire::DialMode::No &&
hello->dialMode() != ::MobileGL::Wire::DialMode::Fork &&
hello->dialMode() != ::MobileGL::Wire::DialMode::Connect)
return RefuseHandshake(transport, ::MobileGL::Wire::RefuseCode::MalformedHello,
"unknown dial mode");
const MobileGLResult compatible = ValidatePeerHandshake(transport, hello->abiMajor(),
hello->abiMinor(), hello->wireFingerprint(), theirStamp, hello->dialMode());
if (compatible != MOBILEGL_OK) return compatible;
const Uint64 ourFingerprint = WireFingerprint();
const auto* terms = hello->linkTerms();
if (terms == nullptr || terms->wireForm() != ::MobileGL::Wire::WireForm::StructImage ||
(terms->dataPlane() != ::MobileGL::Wire::DataPlane::SharedSegments &&
terms->dataPlane() != ::MobileGL::Wire::DataPlane::Stream))
return RefuseHandshake(transport, ::MobileGL::Wire::RefuseCode::LinkTerms,
"unsupported or missing link terms");
const bool stream = terms->dataPlane() == ::MobileGL::Wire::DataPlane::Stream;
if (stream && transport.Role() == Transport::TransportRole::InProcess)
return RefuseHandshake(transport, ::MobileGL::Wire::RefuseCode::LinkTerms,
"stream requires a data connection");
const char* expectedToken = std::getenv("MOBILEGL_IPC_TOKEN");
if (transport.Role() != Transport::TransportRole::InProcess && expectedToken != nullptr &&
expectedToken[0] != '\0' && (hello->token() == nullptr ||
std::strcmp(hello->token()->c_str(), expectedToken) != 0))
return RefuseHandshake(transport, ::MobileGL::Wire::RefuseCode::Authentication,
"token mismatch");
if (hello->backendType() >= static_cast<Uint32>(BackendType::BackendTypeCount) ||
(m_backend != nullptr && hello->backendType() != static_cast<Uint32>(m_backend->GetBackendType())))
return RefuseHandshake(transport, ::MobileGL::Wire::RefuseCode::Backend,
"backend request differs from server", m_backend == nullptr ? 0 :
static_cast<Uint32>(m_backend->GetBackendType()), hello->backendType());
// ---- 3. the four segments, both control pages, the rings.
const MobileGLResult created = m_shm.Create(m_sizes, Transport::MemoryRole::Server);
MobileGLResult created = MOBILEGL_ERR_UNSUPPORTED;
if (stream) {
auto* socket = dynamic_cast<Transport::SocketTransport*>(&transport);
if (socket && socket->IsTcp()) created = AttachStreamLink(socket->TakeDataFd(), m_sizes);
} else {
created = m_link->Memory().Create(m_sizes, Transport::MemoryRole::Server);
}
if (created != MOBILEGL_OK) {
return created;
}
Transport::RingControl* control = m_shm.CmdControl();
m_commands = Transport::RingConsumer(control, m_shm.CmdRingBase(), m_shm.CmdRingCapacity(),
Transport::RingCursorSet::Cmd);
if (!m_commands.Valid()) {
m_link->InitializeEndpoints();
if (!m_commands->Valid()) {
Close();
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
m_consumer.Attach(control, &m_commands, &ProducerDoorbell(), &ConsumerDoorbell(),
SpinUsFromConfig());
m_link->BindDoorbells(&ConsumerDoorbell(), &ProducerDoorbell());
m_consumer.Attach(*m_link, SpinUsFromConfig());
m_replies = ReplyPool();
m_replies.SetLink(m_link.get());
{
Transport::ReplySlotPool pool(m_shm.ReplyBase(), m_shm.ReplyBytes(),
m_shm.ReplySlotCount());
if (!pool.Valid()) {
Close();
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
// A stale stamp from a previous session must never read as this session's answer.
pool.Clear();
m_replies = ReplyPool(m_shm.ReplyBase(), m_shm.ReplyBytes(), m_shm.ReplySlotCount(),
pool.SlotBytes());
}
m_events = Transport::EventRingProducer(m_shm.EventControl(), control, m_shm.EventRingBase(),
m_shm.EventRingCapacity());
m_consumer.SetLink(m_link->Capabilities().PublishIsDelivery ? nullptr : m_link.get());
m_replies.SetLink(m_link.get());
m_applier = PipeApplier(&m_segments, &m_replies);
// ---- 4. Welcome: the four SegmentRefs, plus this side's half of the ABI statement.
@@ -531,14 +536,18 @@ namespace MobileGL::MG_Remote::Server {
using Slot = Transport::SessionSegmentSlot;
const auto segmentRef = [&](Uint32 id, ::MobileGL::Wire::SegmentKind kind, Slot slot) {
return ::MobileGL::Wire::CreateSegmentRefDirect(builder, id, kind,
m_shm.AnnouncedSize(slot),
m_shm.AnnouncedName(slot));
m_link->Memory().AnnouncedSize(slot),
m_link->Memory().AnnouncedName(slot));
};
auto cmd = segmentRef(1, ::MobileGL::Wire::SegmentKind::Cmd, Slot::Cmd);
auto stage = segmentRef(2, ::MobileGL::Wire::SegmentKind::Stage, Slot::Stage);
auto reply = segmentRef(3, ::MobileGL::Wire::SegmentKind::Reply, Slot::Reply);
auto event = segmentRef(4, ::MobileGL::Wire::SegmentKind::Event, Slot::Event);
auto stamp = builder.CreateString(GIT_COMMIT_HASH_SHORT);
auto stamp = builder.CreateString(BuildFingerprint());
const Uint64 maxReply = m_sizes.ReplyBytes / m_sizes.ReplySlotCount - sizeof(Transport::ReplySlotHeader);
auto negotiated = ::MobileGL::Wire::CreateLinkTerms(builder, terms->dataPlane(),
::MobileGL::Wire::WireForm::StructImage, maxReply, m_link->Memory().CmdRingCapacity(),
m_link->Memory().StageBytes(), m_link->Memory().EventRingCapacity());
auto welcome = ::MobileGL::Wire::CreateWelcome(
builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR,
// CONTRACT-P6 4.3: THE SERVER'S OWN PID, not an echo of the client's.
@@ -549,7 +558,8 @@ namespace MobileGL::MG_Remote::Server {
// that a same-process session cannot produce, because there the two pids are
// equal by construction.
static_cast<Uint32>(SelfProcessId()),
cmd, stage, reply, event, stamp, ourFingerprint);
cmd, stage, reply, event, stamp, ourFingerprint, ourFingerprint, negotiated,
m_backend == nullptr ? hello->backendType() : static_cast<Uint32>(m_backend->GetBackendType()));
auto root = ::MobileGL::Wire::CreateCtrlEnvelope(
builder, ::MobileGL::Wire::CtrlMsg::Welcome, welcome.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root);
@@ -571,15 +581,7 @@ namespace MobileGL::MG_Remote::Server {
// w1 lands. That is deliberate and is where the bring-up currently stops: a session
// that skipped them and carried on would be a decoder with no segments, which is the
// "split lane ran monolith and went green" shape.
m_segments.Install(Wire::kSegCmd,
Wire::SegmentView{m_shm.CmdRingBase(), m_shm.CmdRingCapacity()});
m_segments.Install(Wire::kSegStage,
Wire::SegmentView{m_shm.StageBase(), m_shm.StageBytes()});
m_segments.Install(Wire::kSegReply,
Wire::SegmentView{m_shm.ReplyBase(), m_shm.ReplyBytes()});
m_segments.Install(Wire::kSegEvent, Wire::SegmentView{m_shm.EventSegmentBase(),
m_shm.AnnouncedSize(
Transport::SessionSegmentSlot::Event)});
m_segments.AttachLink(m_link.get());
m_segments.InstallProcessResolver();
m_accepted = true;
@@ -671,6 +673,26 @@ namespace MobileGL::MG_Remote::Server {
return SendEnvelope(*m_transport, builder);
}
MobileGLResult ServerSession::AttachStreamLink(int fd, const Transport::SessionSegmentSizes& sizes) {
std::unique_ptr<Transport::ILink> link;
const auto attached = Transport::CreateStreamLink(fd, sizes, Transport::TransportRoleTag::ServerConsumer, link);
if (attached != MOBILEGL_OK) return attached;
AttachDataLink(std::move(link));
return MOBILEGL_OK;
}
void ServerSession::AttachDataLink(std::unique_ptr<Transport::ILink> link) {
m_link = std::move(link);
m_commands = &m_link->CommandsIn(); m_events = &m_link->EventsOut();
m_consumer.SetLink(m_link->Capabilities().PublishIsDelivery ? nullptr : m_link.get());
m_replies.SetLink(m_link.get());
if (m_link) SetExternalDoorbells(&m_link->ConsumerBell(), &m_link->ProducerBell());
}
void ServerSession::FlushDataProgress() {
if (m_link) m_link->FlushProgress();
}
void ServerSession::Close() {
ServerSession* expectedActive = this;
g_active.compare_exchange_strong(expectedActive, nullptr,
@@ -695,10 +717,13 @@ namespace MobileGL::MG_Remote::Server {
}
}
m_consumer.Detach();
m_commands = Transport::RingConsumer();
m_events = Transport::EventRingProducer();
*m_commands = Transport::RingConsumer();
*m_events = Transport::EventRingProducer();
m_replies = ReplyPool();
m_shm.Close();
m_link = Transport::CreateSharedLink(Transport::TransportRoleTag::ServerConsumer);
m_commands = &m_link->CommandsIn(); m_events = &m_link->EventsOut();
m_externalConsumerBell = nullptr; m_externalProducerBell = nullptr;
m_link->Memory().Close();
m_transport = nullptr;
m_accepted = false;
ServerSession* expectedOwner = this;
@@ -706,10 +731,10 @@ namespace MobileGL::MG_Remote::Server {
std::memory_order_release, std::memory_order_relaxed);
}
Transport::RingConsumer& ServerSession::CommandRing() { return m_commands; }
Transport::RingConsumer& ServerSession::CommandRing() { return *m_commands; }
Transport::RingControl& ServerSession::Control() {
Transport::RingControl* control = m_shm.CmdControl();
Transport::RingControl* control = m_link->Memory().CmdControl();
if (control == nullptr) {
SessionFail(MGFatalFamily::ProtocolCorruption, "MGPipe: Fatal{ProtocolCorruption, \"ServerSession::Control\"} - the control "
"page does not exist until Accept() has mapped SEG_CMD");
@@ -766,9 +791,9 @@ namespace MobileGL::MG_Remote::Server {
static_cast<unsigned>(m_transport->Role()));
}
Transport::SessionSegments& ServerSession::Shm() { return m_shm; }
Transport::SessionSegments& ServerSession::Shm() { return m_link->Memory(); }
Transport::SessionConsumer& ServerSession::Consumer() { return m_consumer; }
Transport::EventRingProducer& ServerSession::Events() { return m_events; }
Transport::EventRingProducer& ServerSession::Events() { return *m_events; }
Transport::ITransport* ServerSession::Control_Plane() { return m_transport; }
void ServerSession::PublishEvents() {
@@ -777,8 +802,9 @@ namespace MobileGL::MG_Remote::Server {
}
// Publish, THEN ring - the same order as the forward direction, and the session picks
// the bell so that no caller can pair the right ring with the wrong flag.
m_events.Ring().Publish();
m_events->Ring().Publish();
m_consumer.NotifyClient();
FlushDataProgress();
}
void ServerSession::PostGlError(Uint32 code, const char* message) {
@@ -820,6 +846,7 @@ namespace MobileGL::MG_Remote::Server {
return;
}
m_consumer.CompleteFrame(serial);
FlushDataProgress();
}
void ServerSession::ReturnPresentCredit(Uint64 serial) {
@@ -827,6 +854,7 @@ namespace MobileGL::MG_Remote::Server {
return;
}
m_consumer.ReturnPresentCredit(serial);
FlushDataProgress();
}
Transport::RoleMemorySample ServerSession::SampleMemory() const {
+12 -4
View File
@@ -40,7 +40,9 @@
// refused to exist until both landed would block every other package's bring-up.
#pragma once
#include "../Transport/ILink.h"
#include <Includes.h>
#include <vector>
#include <MG_Pipe/MGPipe.h>
@@ -60,13 +62,19 @@ namespace MobileGL::MG_Remote::Server {
public:
static ServerSession* Active();
ServerSession();
~ServerSession();
MobileGLResult AttachStreamLink(int dataFd, const Transport::SessionSegmentSizes& sizes);
void AttachDataLink(std::unique_ptr<Transport::ILink> link);
Transport::ILink* DataLink() const { return m_link.get(); }
void FlushDataProgress();
// Maps the four segments, answers Hello with Welcome, and publishes the first
// CapsSnapshot. The ABI assertion (CapsCodec.h) happens HERE, before a single record is
// decoded: sizeof(DynamicBackendParameters), sizeof(MGPCaps), sizeof(GLFunctionsTable)
// and the build fingerprint must match, and a mismatch is Fatal{AbiMismatch}.
MobileGLResult Accept(Transport::ITransport& transport);
MobileGLResult Accept(Transport::ITransport& transport,
const std::vector<Uint8>* firstFrame = nullptr);
// Re-publishes the whole snapshot. R-12: a SECOND arrival IS the invalidation signal,
// which is how DirectGLES - which has no OnCapsInvalidated producer - tells the client
@@ -199,17 +207,17 @@ namespace MobileGL::MG_Remote::Server {
void LogMemory(const char* phase) const;
private:
std::unique_ptr<Transport::ILink> m_link;
// sm: non-owning; the spawn entry point owns the SocketDoorbells.
Transport::Doorbell* m_externalConsumerBell = nullptr;
Transport::Doorbell* m_externalProducerBell = nullptr;
Transport::RingConsumer m_commands;
Transport::RingConsumer* m_commands = nullptr;
Wire::SegmentTable m_segments;
PipeApplier m_applier;
ReplyPool m_replies;
Transport::SessionSegments m_shm;
Transport::SessionConsumer m_consumer;
Transport::EventRingProducer m_events;
Transport::EventRingProducer* m_events = nullptr;
Transport::ITransport* m_transport = nullptr;
MG_Backend::BackendObject* m_backend = nullptr;
Transport::SessionSegmentSizes m_sizes;
@@ -96,6 +96,7 @@ namespace MobileGL::MG_Remote::Server {
Bool ok = false;
Int eglMajor = 0;
Int eglMinor = 0;
Uint64 eventHead = 0; // data-plane delivery fence carried by SurfaceReply
};
namespace Detail {
@@ -110,14 +111,15 @@ namespace MobileGL::MG_Remote::Server {
// Exhaustive by the language's aggregate decomposition rule: adding ANY member
// without extending this binding is a compile error, so new fields cannot evade it.
const auto& [kind, seq, display, surface, readSurface, context, windowBackend,
nativeToken, width, height, swapInterval, ok, eglMajor, eglMinor] = frame;
nativeToken, width, height, swapInterval, ok, eglMajor, eglMinor, eventHead] = frame;
return IsSurfaceControlValue<decltype(kind)> && IsSurfaceControlValue<decltype(seq)> &&
IsSurfaceControlValue<decltype(display)> && IsSurfaceControlValue<decltype(surface)> &&
IsSurfaceControlValue<decltype(readSurface)> && IsSurfaceControlValue<decltype(context)> &&
IsSurfaceControlValue<decltype(windowBackend)> && IsSurfaceControlValue<decltype(nativeToken)> &&
IsSurfaceControlValue<decltype(width)> && IsSurfaceControlValue<decltype(height)> &&
IsSurfaceControlValue<decltype(swapInterval)> && IsSurfaceControlValue<decltype(ok)> &&
IsSurfaceControlValue<decltype(eglMajor)> && IsSurfaceControlValue<decltype(eglMinor)>;
IsSurfaceControlValue<decltype(eglMajor)> && IsSurfaceControlValue<decltype(eglMinor)> &&
IsSurfaceControlValue<decltype(eventHead)>;
}
} // namespace Detail
@@ -0,0 +1,77 @@
#include "ControlInbox.h"
#include "Doorbell.h"
#include "../Protocol/SurfaceOpCodec.h"
#include <MG_Util/Debug/Log.h>
#include <chrono>
namespace MobileGL::MG_Remote::Transport {
ControlInbox::ControlInbox(ITransport& transport) : m_transport(transport), m_reader([this] { Read(); }) {}
ControlInbox::~ControlInbox() { Stop(); }
void ControlInbox::Stop() {
{
std::lock_guard<std::mutex> lock(m_mutex);
m_stop = true;
m_cv.notify_all();
}
// Receive uses a short bounded wait, so no shutdown of the data plane is needed.
if (m_reader.joinable()) m_reader.join();
}
void ControlInbox::Read() {
for (;;) {
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_stop) return;
}
std::uint64_t size = 0;
auto result = m_transport.ReceiveFrame({nullptr, 0}, &size, 100);
if (result == MOBILEGL_ERR_TIMEOUT) continue;
std::vector<std::uint8_t> frame;
if (result == MOBILEGL_ERR_BUFFER_TOO_SMALL && size <= 64ull * 1024 * 1024) {
frame.resize(static_cast<std::size_t>(size));
result = m_transport.ReceiveFrame({frame.data(), frame.size()}, &size, 0);
}
if (result == MOBILEGL_OK) {
flatbuffers::Verifier verifier(frame.data(), frame.size());
if (!::MobileGL::Wire::VerifyCtrlEnvelopeBuffer(verifier)) result = MOBILEGL_ERR_PROTOCOL_MISMATCH;
else {
const auto* envelope = ::MobileGL::Wire::GetCtrlEnvelope(frame.data());
if (const auto* log = envelope->msg_as_LogLine()) {
if (log->text()) MG_Util::Debug::WritePeerLog(log->text()->c_str());
continue;
}
}
}
std::unique_lock<std::mutex> lock(m_mutex);
if (result != MOBILEGL_OK) {
m_result = result;
m_cv.notify_all();
return;
}
m_cv.wait(lock, [&] { return m_stop || (m_frames.size() < 256 && m_bytes < 64ull * 1024 * 1024); });
if (m_stop) return;
m_bytes += frame.size();
m_frames.push_back(std::move(frame));
m_cv.notify_all();
}
}
MobileGLResult ControlInbox::Receive(std::vector<std::uint8_t>& frame, std::uint32_t timeoutMs) {
std::unique_lock<std::mutex> lock(m_mutex);
const auto ready = [&] { return m_stop || !m_frames.empty() || m_result != MOBILEGL_OK; };
if (timeoutMs == kWaitForever) m_cv.wait(lock, ready);
else if (!m_cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), ready)) return MOBILEGL_ERR_TIMEOUT;
if (m_frames.empty()) return m_result == MOBILEGL_OK ? MOBILEGL_ERR_TRANSPORT_CLOSED : m_result;
frame = std::move(m_frames.front());
m_frames.pop_front();
m_bytes -= frame.size();
m_cv.notify_all();
return MOBILEGL_OK;
}
std::uint64_t ControlInbox::Peek() {
std::lock_guard<std::mutex> lock(m_mutex);
return m_frames.empty() ? 0 : m_frames.front().size();
}
}
@@ -0,0 +1,31 @@
#pragma once
#include "ITransport.h"
#include <condition_variable>
#include <deque>
#include <mutex>
#include <thread>
#include <vector>
namespace MobileGL::MG_Remote::Transport {
// The control reader remains runnable while the GL thread waits on data.
// Log forwarding can therefore never fill a socket and block the applier.
class ControlInbox {
public:
explicit ControlInbox(ITransport& transport);
~ControlInbox();
void Stop();
MobileGLResult Receive(std::vector<std::uint8_t>& frame, std::uint32_t timeoutMs);
std::uint64_t Peek();
private:
void Read();
ITransport& m_transport;
std::mutex m_mutex;
std::condition_variable m_cv;
std::deque<std::vector<std::uint8_t>> m_frames;
std::uint64_t m_bytes = 0;
bool m_stop = false;
MobileGLResult m_result = MOBILEGL_OK;
std::thread m_reader;
};
}
+3
View File
@@ -186,6 +186,7 @@ namespace MobileGL::MG_Remote::Transport {
// finished with every payload pointer it popped: a writeback's bytes live
// in the ring itself, so retiring early is the R-11 violation one level
// down.
void SetLink(ILink* link) { m_link = link; }
void Drained() {
m_consumer.PublishRetired();
if (m_cmdControl != nullptr) {
@@ -202,6 +203,7 @@ namespace MobileGL::MG_Remote::Transport {
NotifyIfParked(*m_serverBell, m_cmdControl->consumerParked);
}
}
if (m_link) m_link->Flush();
}
// The bell the SERVER parks on, lent by the session so that Drained() can ring it.
@@ -231,6 +233,7 @@ namespace MobileGL::MG_Remote::Transport {
private:
RingControl* m_cmdControl = nullptr;
ILink* m_link = nullptr;
RingConsumer m_consumer;
const std::uint8_t* m_segmentBase = nullptr;
// P5e (ra): the server's bell, rung by Drained() when it cleared a full latch.
+45
View File
@@ -72,10 +72,17 @@
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <memory>
namespace MobileGL::MG_Remote::Transport {
class Doorbell;
class SessionSegments;
struct SessionSegmentSizes;
class RingProducer;
class RingConsumer;
class EventRingProducer;
class EventRingConsumer;
// Which end of the link this instance is. Deliberately NOT ITransport's
// TransportRole: that enum's InProcess value names a control-plane
@@ -194,6 +201,16 @@ namespace MobileGL::MG_Remote::Transport {
std::uint64_t MaxReplyBytes = 0;
};
// Non-owning synchronization addresses, acquired once at attachment. They
// denote process-local atomics on a stream and shared atomics on a mapping.
struct LinkSignals {
std::atomic<std::uint64_t>* CmdHead = nullptr;
std::atomic<std::uint64_t>* SubmittedSeq = nullptr;
std::atomic<std::uint32_t>* ConsumerParked = nullptr;
std::atomic<std::uint32_t>* ProducerParked = nullptr;
std::atomic<std::uint32_t>* EventRingFull = nullptr;
};
// -------------------------------------------------------------------------
// The seam itself.
//
@@ -211,6 +228,19 @@ namespace MobileGL::MG_Remote::Transport {
virtual LinkCapabilities Capabilities() const = 0;
// Setup views. Ownership remains in the link; callers cache the ring
// endpoint once so reserve/pop and spin predicates remain non-virtual.
virtual SessionSegments& Memory() = 0;
virtual void InitializeEndpoints() = 0;
LinkSignals Signals();
std::uint64_t SegmentSize(LinkSegment segment);
virtual RingProducer& CommandsOut() = 0;
virtual RingConsumer& CommandsIn() = 0;
virtual EventRingProducer& EventsOut() = 0;
virtual EventRingConsumer& EventsIn() = 0;
virtual void BindDoorbells(Doorbell*, Doorbell*) {}
// The watermark block. Taken once; every wait predicate reads through
// the returned pointer with no further virtual dispatch. Never null on
// an attached link.
@@ -259,6 +289,8 @@ namespace MobileGL::MG_Remote::Transport {
virtual LinkArena* EventArena() = 0;
virtual LinkCursor* EventCursor() = 0;
virtual std::uint64_t EventPublishedHead() const = 0;
virtual MobileGLResult WaitForEventDelivery(std::uint64_t head, std::uint32_t timeoutMs) = 0;
// ---- wakeups --------------------------------------------------------
//
@@ -289,6 +321,14 @@ namespace MobileGL::MG_Remote::Transport {
// waiting on. The second method writing StreamLink revealed.
virtual MobileGLResult FlushProgress() = 0;
// The encoder keeps its established stage allocator; the link tracks only the
// newly written spans that must precede command delivery.
virtual void NoteStage(LinkSpan) {}
// Called after local progress advances. Stream links coalesce transmission;
// the explicit FlushProgress at every park remains mandatory.
virtual void ProgressChanged() {}
virtual bool PeerHungUp() const { return false; }
// ---- lifecycle -------------------------------------------------------
virtual bool Attached() const = 0;
@@ -299,4 +339,9 @@ namespace MobileGL::MG_Remote::Transport {
ILink() = default;
};
// Setup factories keep concrete link selection out of codec/session code.
std::unique_ptr<ILink> CreateSharedLink(TransportRoleTag role);
MobileGLResult CreateStreamLink(int fd, const SessionSegmentSizes& sizes,
TransportRoleTag role, std::unique_ptr<ILink>& out);
} // namespace MobileGL::MG_Remote::Transport
@@ -0,0 +1,107 @@
// SPDX-License-Identifier: LGPL-3.0-only
#include "LinkMetrics.h"
#include <MG_Util/Debug/Log.h>
#include <array>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#if !defined(_WIN32)
#include <time.h>
#endif
namespace MobileGL::MG_Remote::Transport {
namespace {
struct Window {
std::uint64_t waitReplies = 0, samples = 0, replyNs = 0, stageBytes = 0;
std::array<std::uint64_t, 32> histogram{};
};
// The client already has exactly one record producer. The server never writes these.
struct Metrics {
bool active = false;
std::uint64_t frame = 0, started = 0, lastFrame = 0, cpuStarted = 0, cpuLast = 0;
Window total{}, current{};
} metrics;
std::uint64_t ClockNs() {
return std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
}
std::uint64_t ThreadCpuNs() {
#if defined(CLOCK_THREAD_CPUTIME_ID)
timespec value{};
if (::clock_gettime(CLOCK_THREAD_CPUTIME_ID, &value) == 0)
return std::uint64_t(value.tv_sec) * 1000000000ull + value.tv_nsec;
#endif
return 0;
}
std::uint64_t QuantileUpperUs(const Window& w, unsigned percentile) {
if (w.samples == 0) return 0;
const auto target = (w.samples * percentile + 99) / 100;
std::uint64_t count = 0;
for (unsigned i = 0; i < w.histogram.size(); ++i) {
count += w.histogram[i];
if (count >= target) return 1ull << i;
}
return 1ull << 31;
}
void Emit(const char* kind, const Window& w, std::uint64_t wallNs, std::uint64_t cpuNs) {
char histogram[768]{};
std::size_t at = 0;
for (unsigned i = 0; i < w.histogram.size(); ++i) {
const int n = std::snprintf(histogram + at, sizeof(histogram) - at, "%s%llu",
i == 0 ? "" : ",", static_cast<unsigned long long>(w.histogram[i]));
if (n < 0 || static_cast<std::size_t>(n) >= sizeof(histogram) - at) break;
at += static_cast<std::size_t>(n);
}
MGLOG_I("P65LinkMetrics kind=%s frame=%llu wait_replies=%llu rtt_samples=%llu "
"rtt_mean_us=%.3f rtt_p50_upper_us=%llu rtt_p99_upper_us=%llu "
"stage_bytes=%llu wall_ns=%llu client_thread_cpu_ns=%llu rtt_hist_us_pow2=%s",
kind, static_cast<unsigned long long>(metrics.frame),
static_cast<unsigned long long>(w.waitReplies), static_cast<unsigned long long>(w.samples),
w.samples == 0 ? 0.0 : double(w.replyNs) / double(w.samples) / 1000.0,
static_cast<unsigned long long>(QuantileUpperUs(w, 50)),
static_cast<unsigned long long>(QuantileUpperUs(w, 99)),
static_cast<unsigned long long>(w.stageBytes), static_cast<unsigned long long>(wallNs),
static_cast<unsigned long long>(cpuNs), histogram);
}
}
void LinkMetricsBegin() {
metrics = {};
const char* enabled = std::getenv("MOBILEGL_PIPE_STATS");
metrics.active = enabled != nullptr && std::strcmp(enabled, "1") == 0;
if (!metrics.active) return;
metrics.started = metrics.lastFrame = ClockNs();
metrics.cpuStarted = metrics.cpuLast = ThreadCpuNs();
}
std::uint64_t LinkMetricsBeginReply(bool wantsReply) {
if (!metrics.active || !wantsReply) return 0;
++metrics.current.waitReplies; ++metrics.total.waitReplies;
return ClockNs();
}
void LinkMetricsReplyApplied(std::uint64_t startedNs) {
if (!metrics.active || startedNs == 0) return;
const auto ns = ClockNs() - startedNs;
const auto us = (ns + 999) / 1000;
unsigned bucket = 0;
while (bucket < 31 && us > (1ull << bucket)) ++bucket;
for (Window* w : {&metrics.current, &metrics.total}) {
++w->samples; w->replyNs += ns; ++w->histogram[bucket];
}
}
void LinkMetricsStageBytes(std::uint64_t bytes) {
if (!metrics.active) return;
metrics.current.stageBytes += bytes; metrics.total.stageBytes += bytes;
}
void LinkMetricsPresent() {
if (!metrics.active) return;
++metrics.frame;
const auto now = ClockNs(), cpu = ThreadCpuNs();
Emit("frame", metrics.current, now - metrics.lastFrame, cpu - metrics.cpuLast);
metrics.current = {}; metrics.lastFrame = now; metrics.cpuLast = cpu;
}
void LinkMetricsEnd() {
if (!metrics.active) return;
Emit("summary", metrics.total, ClockNs() - metrics.started, ThreadCpuNs() - metrics.cpuStarted);
metrics.active = false;
}
}
@@ -0,0 +1,13 @@
// P6.5 transport measurements. Called only by the single client producer.
#pragma once
#include <cstdint>
namespace MobileGL::MG_Remote::Transport {
// Disabled sessions never read a clock. Samples are a fixed 32-bucket histogram.
void LinkMetricsBegin();
void LinkMetricsEnd();
std::uint64_t LinkMetricsBeginReply(bool wantsReply);
void LinkMetricsReplyApplied(std::uint64_t startedNs);
void LinkMetricsStageBytes(std::uint64_t bytes);
void LinkMetricsPresent();
}
+13
View File
@@ -257,6 +257,19 @@ namespace MobileGL::MG_Remote::Transport {
std::memcpy(slot, &header, sizeof(header));
}
// The link can lend the immutable payload after appliedSeq without a
// second copy. The same seq and size honesty checks guard both readers.
bool ReadView(std::uint64_t seq, std::int32_t* status, const void** payload,
std::uint64_t* size) const {
if (!m_base || !seq || !status || !payload || !size) return false;
const auto* slot = SlotAt(seq);
ReplySlotHeader header{}; std::memcpy(&header, slot, sizeof header);
std::atomic_thread_fence(std::memory_order_acquire);
if (header.Seq != seq || header.Size > MaxReplyBytes()) return false;
*status = header.Status; *size = header.Size;
*payload = slot + sizeof header; return true;
}
// Client side. Returns false when the slot does not carry THIS seq - the
// self-check the stamp exists for. `outBytes` may be null for an answer
// with no payload (every DECLINE, and the four Bool acceptances).
+20 -17
View File
@@ -417,6 +417,11 @@ namespace MobileGL::MG_Remote::Transport {
// SessionProducer
// -----------------------------------------------------------------------
void SessionProducer::Attach(ILink& link, std::uint32_t spinUs) {
Attach(link.Memory().CmdControl(), &link.CommandsOut(), &link.ConsumerBell(), &link.ProducerBell(), spinUs);
SetLink(link.Capabilities().PublishIsDelivery ? nullptr : &link);
}
void SessionProducer::Attach(RingControl* control, RingProducer* cmd, Doorbell* peerBell,
Doorbell* selfBell, std::uint32_t spinUs) {
m_control = control;
@@ -432,6 +437,7 @@ namespace MobileGL::MG_Remote::Transport {
m_peerBell = nullptr;
m_selfBell = nullptr;
m_lastPublishedSeq = 0;
m_link = nullptr;
}
void SessionProducer::PublishAndNotify(std::uint64_t submittedSeq) {
@@ -469,6 +475,7 @@ namespace MobileGL::MG_Remote::Transport {
// COUNTED BEFORE THE WAIT AND ONLY WHEN ONE REALLY HAPPENS - after the two refusals
// above, which answer TimedOut without ever reaching the bell. See Waits()/Parks() in
// the header for why the pair is the reading and either number alone is not.
if (m_link && m_link->Flush() != MOBILEGL_OK) return SessionWait::ShutDown;
m_waits.fetch_add(1, std::memory_order_relaxed);
// &m_parks is the OTHER half of the pair, and it is passed rather than read off the
// bell: the bell is this endpoint's and the encoder waits on it too, so its own
@@ -547,6 +554,11 @@ namespace MobileGL::MG_Remote::Transport {
// SessionConsumer
// -----------------------------------------------------------------------
void SessionConsumer::Attach(ILink& link, std::uint32_t spinUs) {
Attach(link.Memory().CmdControl(), &link.CommandsIn(), &link.ProducerBell(), &link.ConsumerBell(), spinUs);
SetLink(link.Capabilities().PublishIsDelivery ? nullptr : &link);
}
void SessionConsumer::Attach(RingControl* control, RingConsumer* cmd, Doorbell* peerBell,
Doorbell* selfBell, std::uint32_t spinUs) {
m_control = control;
@@ -566,6 +578,7 @@ namespace MobileGL::MG_Remote::Transport {
m_selfBell = nullptr;
m_retirableCursor = 0;
m_borrowHeld = false;
m_link = nullptr;
}
SessionWait SessionConsumer::WaitForWork(std::uint32_t timeoutMs) {
@@ -574,6 +587,7 @@ namespace MobileGL::MG_Remote::Transport {
}
RingControl* control = m_control;
RingConsumer* cmd = m_cmd;
if (m_link && m_link->FlushProgress() != MOBILEGL_OK) return SessionWait::ShutDown;
const bool woke = m_selfBell->Wait(
control->consumerParked,
[control, cmd] {
@@ -647,6 +661,7 @@ namespace MobileGL::MG_Remote::Transport {
}
void SessionConsumer::NotifyClient() {
if (m_link) m_link->ProgressChanged();
if (m_control != nullptr && m_peerBell != nullptr) {
NotifyIfParked(*m_peerBell, m_control->producerParked);
}
@@ -671,29 +686,17 @@ namespace MobileGL::MG_Remote::Transport {
};
mix(inputs.DynamicParamsSize);
mix(inputs.CapsSize);
mix(inputs.FunctionTableSize);
mix(inputs.MemberLayout);
mix(inputs.CatalogueLayout);
mix(inputs.RenderStateLayout);
mix(inputs.FormatCapabilityTargets);
mix(inputs.FormatCapabilityFormats);
mix(inputs.FormatCapabilitiesCodecVersion);
mix(inputs.RendererInfoCodecVersion);
mix(inputs.OpCount);
mix(inputs.AbiVersion);
// CONTRACT-P6 4.2: THE BUILD SYSTEM'S OWN ANSWER, mixed before the bytes and separately
// from them. `BuildStampPresent == 0` means the build could not determine a commit at
// all; that is a different claim from "the commit is the empty string", and before this
// field existed the two collapsed - so every stampless build agreed with every other,
// which is the exact agreement the stamp is there to refuse.
mix(inputs.BuildStampPresent);
// A presence marker before the bytes, so that "no stamp" (nullptr) and
// "an empty stamp" ("") are different inputs rather than the same
// absence of bytes.
mix(inputs.BuildStamp != nullptr ? 1u : 0u);
if (inputs.BuildStamp != nullptr) {
for (const char* c = inputs.BuildStamp; *c != '\0'; ++c) {
hash ^= static_cast<std::uint64_t>(static_cast<unsigned char>(*c));
hash *= 1099511628211ull;
}
}
mix(inputs.PointerBits);
mix(inputs.LittleEndian);
// 0 is reserved for "not stated": a peer that forgot to fill the field
// must not accidentally agree with one that did.
return hash == 0 ? 1ull : hash;
+21 -42
View File
@@ -138,6 +138,8 @@ namespace MobileGL::MG_Remote::Transport {
// Creates and maps all four, initialises BOTH control pages (SEG_CMD's
// and SEG_EVENT's), and books the mapping in `role`'s ledger.
MobileGLResult Create(const SessionSegmentSizes& sizes, MemoryRole role);
// Private, identically addressed mirrors for the stream data plane.
MobileGLResult CreatePrivate(const SessionSegmentSizes& sizes, MemoryRole role);
// The inproc peer's view of the owner's four segments, booked under the
// OTHER role. It does NOT re-init the control pages - there is one shared
@@ -210,6 +212,8 @@ namespace MobileGL::MG_Remote::Transport {
private:
void DeriveViews();
void* m_private[4] = {};
std::uint64_t m_privateSizes[4] = {};
ShmSegment m_owned[4]; // empty on an attached (peer) view
ShmSegment* m_segments[4] = {nullptr, nullptr, nullptr, nullptr};
@@ -314,9 +318,11 @@ namespace MobileGL::MG_Remote::Transport {
// stageHead with nothing advancing the two tails, so FreeBytes() would
// fall to zero the first time the head lapped the capacity and never
// recover: a guaranteed hang, not a slow path.
void Attach(ILink& link, std::uint32_t spinUs);
void Attach(RingControl* control, RingProducer* cmd, Doorbell* peerBell, Doorbell* selfBell,
std::uint32_t spinUs);
void Detach();
void SetLink(ILink* link) { m_link = link; }
bool Valid() const { return m_control != nullptr && m_cmd != nullptr; }
// Publish the command ring's head, record submittedSeq, THEN ring - in
@@ -365,6 +371,7 @@ namespace MobileGL::MG_Remote::Transport {
// stalls forever.
SessionWait WaitForCmdSpace(std::uint64_t bytes, std::uint32_t timeoutMs);
LinkProgress* Progress() const { return m_control ? &m_control->Progress : nullptr; }
RingControl* Control() const { return m_control; }
RingProducer* Cmd() const { return m_cmd; }
Doorbell* PeerDoorbell() const { return m_peerBell; }
@@ -398,6 +405,7 @@ namespace MobileGL::MG_Remote::Transport {
template <class Ready>
SessionWait Park(Ready&& ready, std::uint32_t timeoutMs);
ILink* m_link = nullptr;
RingControl* m_control = nullptr;
RingProducer* m_cmd = nullptr;
Doorbell* m_peerBell = nullptr;
@@ -420,9 +428,11 @@ namespace MobileGL::MG_Remote::Transport {
public:
SessionConsumer() = default;
void Attach(ILink& link, std::uint32_t spinUs);
void Attach(RingControl* control, RingConsumer* cmd, Doorbell* peerBell, Doorbell* selfBell,
std::uint32_t spinUs);
void Detach();
void SetLink(ILink* link) { m_link = link; }
bool Valid() const { return m_control != nullptr && m_cmd != nullptr; }
// Park until a record is waiting, the session is shut down, or the
@@ -520,6 +530,7 @@ namespace MobileGL::MG_Remote::Transport {
// How many kRecBorrowSlot records this consumer has seen. Non-zero in P5
// is a finding, not a statistic.
std::uint64_t BorrowedRecordsSeen() const { return m_borrowedSeen; }
LinkProgress* Progress() const { return m_control ? &m_control->Progress : nullptr; }
RingControl* Control() const { return m_control; }
RingConsumer* Cmd() const { return m_cmd; }
Doorbell* PeerDoorbell() const { return m_peerBell; }
@@ -527,6 +538,7 @@ namespace MobileGL::MG_Remote::Transport {
std::uint32_t SpinUs() const { return m_spinUs; }
private:
ILink* m_link = nullptr;
RingControl* m_control = nullptr;
RingConsumer* m_cmd = nullptr;
Doorbell* m_peerBell = nullptr;
@@ -543,58 +555,25 @@ namespace MobileGL::MG_Remote::Transport {
};
// -----------------------------------------------------------------------
// The ABI fingerprint's mixer - THE ONE IMPLEMENTATION.
//
// CapsCodec.cpp's CapsAbiFingerprint(), the value both handshakes compare,
// is exactly MixAbiFingerprint(CapsAbiFingerprintInputs()). It lives under
// Transport/ so that it can be tested without the GL frontend's umbrella
// header, and it takes its inputs as a struct so that the SAME function the
// handshake calls can be driven with one field perturbed at a time.
//
// The wave-1 review (ID-46 finding 6) found the previous shape - a
// five-argument mixer here and a SEPARATE hand-rolled FNV loop in
// CapsCodec.cpp - had exactly one caller of this function: the sensitivity
// test. Production never called it, so replacing CapsAbiFingerprint() with
// `return 1;` left every fingerprint test green and both peers agreeing on
// nothing. Now there is one mixer, the sensitivity case starts from the
// production entry point, and that perturbation turns it red.
// P6.5 wire fingerprint: one mixer, driven by compiler-derived layout facts.
// CapsCodec supplies generated member/catalogue digests; build identity and
// negotiated window sizes do not participate in wire compatibility.
// -----------------------------------------------------------------------
// Wire facts only. Segment geometry and build identity are negotiated separately.
struct AbiFingerprintInputs {
// The three struct shapes table 0's ABI-agreement row names.
std::uint64_t DynamicParamsSize = 0;
std::uint64_t CapsSize = 0;
std::uint64_t FunctionTableSize = 0;
// The caps blob's own geometry and the two blob codecs' versions: the
// format-capability table's extents and the codec version stamps.
std::uint64_t MemberLayout = 0;
std::uint64_t CatalogueLayout = 0;
std::uint64_t RenderStateLayout = 0;
std::uint64_t FormatCapabilityTargets = 0;
std::uint64_t FormatCapabilityFormats = 0;
std::uint64_t FormatCapabilitiesCodecVersion = 0;
std::uint64_t RendererInfoCodecVersion = 0;
// The catalogue's length (ID-33): a peer with one more opcode is a
// different wire even if every struct kept its size.
std::uint64_t OpCount = 0;
// MOBILEGL_ABI_VERSION(major, minor): a protocol change that left every
// struct the same size, which nothing above can see.
std::uint32_t AbiVersion = 0;
// MOBILEGL_BUILD_STAMP_VALUE: two builds of the same sizes can still
// disagree about a FIELD ORDER, which no sizeof can see. nullptr and ""
// are distinct inputs and neither equals a real stamp.
const char* BuildStamp = nullptr;
// CONTRACT-P6 4.2. WHETHER THE BUILD COULD NAME ITS COMMIT AT ALL, which
// is not the same question as whether BuildStamp is empty - and telling
// them apart is the entire fix.
//
// The old mix took GIT_COMMIT_HASH_SHORT, and a `git rev-parse` that
// FAILED yielded "" with nothing reporting it (no RESULT_VARIABLE).
// Every stampless build in the world then mixed the same "" and agreed
// with every other, which is precisely the agreement the stamp exists to
// refuse. Measured in this worktree: WSL cannot resolve the Windows path
// in its `.git` file, so every WSL build was stampless and silent.
//
// Mixed as its own field so that 0-with-"" and 1-with-"" are different
// fingerprints: a build that says "I have no stamp" must not collide
// with one whose stamp genuinely is the empty string.
std::uint32_t BuildStampPresent = 0;
std::uint32_t PointerBits = 0;
std::uint32_t LittleEndian = 0;
};
// FNV-1a over every field above, in declaration order. Never 0: that value is
+136
View File
@@ -0,0 +1,136 @@
// SPDX-License-Identifier: LGPL-3.0-only
#include "ShmLink.h"
namespace MobileGL::MG_Remote::Transport {
LinkSignals ILink::Signals() {
auto* c = Memory().CmdControl();
return {&c->cmdHead, &c->submittedSeq, &c->consumerParked, &c->producerParked, &c->eventRingFull};
}
std::uint64_t ILink::SegmentSize(LinkSegment segment) {
auto& m = Memory();
switch (segment) {
case LinkSegment::Cmd:
return m.CmdRingCapacity();
case LinkSegment::Stage:
return m.StageBytes();
case LinkSegment::Reply:
return m.ReplyBytes();
case LinkSegment::Event:
return m.AnnouncedSize(SessionSegmentSlot::Event);
default:
return 0;
}
}
std::unique_ptr<ILink> CreateSharedLink(TransportRoleTag role) {
auto link = std::make_unique<ShmLink>();
link->SetRole(role);
return link;
}
void ShmLink::InitializeEndpoints() {
auto* c = m_memory.CmdControl();
m_commandsOut = RingProducer(c, m_memory.CmdRingBase(), m_memory.CmdRingCapacity(), RingCursorSet::Cmd);
m_commandsIn = RingConsumer(c, m_memory.CmdRingBase(), m_memory.CmdRingCapacity(), RingCursorSet::Cmd);
m_eventsOut =
EventRingProducer(m_memory.EventControl(), c, m_memory.EventRingBase(), m_memory.EventRingCapacity());
m_eventsIn = EventRingConsumer(m_memory.EventControl(), c, m_memory.EventRingBase(),
m_memory.EventRingCapacity(), m_memory.EventSegmentBase());
if (m_role == TransportRoleTag::ServerConsumer) {
ReplySlotPool pool(m_memory.ReplyBase(), m_memory.ReplyBytes(), m_memory.ReplySlotCount());
pool.Clear();
}
}
void ShmLink::BindDoorbells(Doorbell* c, Doorbell* p) {
m_consumer = c;
m_producer = p;
}
LinkCapabilities ShmLink::Capabilities() const {
const ReplySlotPool replies(m_memory.ReplyBase(), m_memory.ReplyBytes(), m_memory.ReplySlotCount());
return {true, true, true, m_memory.CmdRingCapacity() / 2, replies.MaxReplyBytes()};
}
LinkProgress* ShmLink::Progress() {
return m_memory.Valid() ? &m_memory.CmdControl()->Progress : nullptr;
}
LinkEventFlags* ShmLink::EventFlags() {
return m_memory.Valid() ? reinterpret_cast<LinkEventFlags*>(&m_memory.CmdControl()->eventRingFull) : nullptr;
}
LinkArena* ShmLink::RecordArena() {
m_recordArena = {m_memory.CmdRingBase(), m_memory.CmdRingCapacity(), m_memory.CmdRingCapacity() - 1,
m_commandsOut.LocalHead()};
return &m_recordArena;
}
LinkCursor* ShmLink::RecordCursor() {
m_recordCursor = {m_memory.CmdRingBase(), m_memory.CmdRingCapacity(), m_memory.CmdRingCapacity() - 1,
m_commandsIn.LocalTail()};
return &m_recordCursor;
}
LinkArena* ShmLink::EventArena() {
m_eventArena = {m_memory.EventRingBase(), m_memory.EventRingCapacity(), m_memory.EventRingCapacity() - 1,
m_eventsOut.Ring().LocalHead()};
return &m_eventArena;
}
LinkCursor* ShmLink::EventCursor() {
m_eventCursor = {m_memory.EventRingBase(), m_memory.EventRingCapacity(), m_memory.EventRingCapacity() - 1,
m_eventsIn.Ring().LocalTail()};
return &m_eventCursor;
}
MobileGLResult ShmLink::StageBytes(std::uint64_t, LinkSpan*, void**) {
return MOBILEGL_ERR_UNSUPPORTED;
}
MobileGLResult ShmLink::ResolveSpan(LinkSegment segment, LinkSpan span, const void** out) {
if (!out || !Attached()) return MOBILEGL_ERR_NOT_INITIALIZED;
const void* base = nullptr;
std::uint64_t size = 0;
switch (segment) {
case LinkSegment::Cmd:
base = m_memory.CmdRingBase();
size = m_memory.CmdRingCapacity();
break;
case LinkSegment::Stage:
base = m_memory.StageBase();
size = m_memory.StageBytes();
break;
case LinkSegment::Reply:
base = m_memory.ReplyBase();
size = m_memory.ReplyBytes();
break;
case LinkSegment::Event:
base = m_memory.EventSegmentBase();
size = m_memory.AnnouncedSize(SessionSegmentSlot::Event);
break;
default:
return MOBILEGL_ERR_UNSUPPORTED;
}
if (span.Offset > size || span.Size > size - span.Offset) return MOBILEGL_ERR_PROTOCOL_MISMATCH;
*out = static_cast<const std::uint8_t*>(base) + span.Offset;
return MOBILEGL_OK;
}
MobileGLResult ShmLink::PostReply(std::uint64_t seq, std::int32_t status, const void* payload, std::uint64_t size) {
ReplySlotPool replies(m_memory.ReplyBase(), m_memory.ReplyBytes(), m_memory.ReplySlotCount());
replies.Post(seq, status, payload, size);
return MOBILEGL_OK;
}
MobileGLResult ShmLink::ReadReply(std::uint64_t seq, std::int32_t* status, const void** payload,
std::uint64_t* size) {
if (!status || !payload || !size) return MOBILEGL_ERR_INVALID_ARGUMENT;
ReplySlotPool replies(m_memory.ReplyBase(), m_memory.ReplyBytes(), m_memory.ReplySlotCount());
return replies.ReadView(seq, status, payload, size) ? MOBILEGL_OK : MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
std::uint64_t ShmLink::EventPublishedHead() const {
return m_memory.Valid() ? m_memory.EventControl()->cmdHead.load(std::memory_order_acquire) : 0;
}
MobileGLResult ShmLink::WaitForEventDelivery(std::uint64_t head, std::uint32_t) {
return EventPublishedHead() >= head ? MOBILEGL_OK : MOBILEGL_ERR_PROTOCOL_MISMATCH;
}
Doorbell& ShmLink::ConsumerBell() {
return *m_consumer;
}
Doorbell& ShmLink::ProducerBell() {
return *m_producer;
}
void ShmLink::Detach() {
m_commandsOut = {};
m_commandsIn = {};
m_eventsOut = {};
m_eventsIn = {};
m_memory.Close();
}
} // namespace MobileGL::MG_Remote::Transport
+55
View File
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: LGPL-3.0-only
#pragma once
#include "ILink.h"
#include "SessionRings.h"
#include <vector>
namespace MobileGL::MG_Remote::Transport {
// Owns the mappings and every physical endpoint. Shared and private mirrored
// storage use identical ring algorithms; only delivery belongs to the link.
class ShmLink final : public ILink {
public:
ShmLink() = default;
~ShmLink() override = default;
SessionSegments& Memory() override { return m_memory; }
RingProducer& CommandsOut() override { return m_commandsOut; }
RingConsumer& CommandsIn() override { return m_commandsIn; }
EventRingProducer& EventsOut() override { return m_eventsOut; }
EventRingConsumer& EventsIn() override { return m_eventsIn; }
void InitializeEndpoints() override;
void BindDoorbells(Doorbell* consumer, Doorbell* producer) override;
void SetRole(TransportRoleTag role) { m_role = role; }
LinkCapabilities Capabilities() const override;
LinkProgress* Progress() override;
LinkEventFlags* EventFlags() override;
LinkArena* RecordArena() override;
LinkCursor* RecordCursor() override;
MobileGLResult StageBytes(std::uint64_t, LinkSpan*, void**) override;
MobileGLResult ResolveSpan(LinkSegment, LinkSpan, const void**) override;
MobileGLResult PostReply(std::uint64_t, std::int32_t, const void*, std::uint64_t) override;
MobileGLResult ReadReply(std::uint64_t, std::int32_t*, const void**, std::uint64_t*) override;
LinkArena* EventArena() override;
LinkCursor* EventCursor() override;
std::uint64_t EventPublishedHead() const override;
MobileGLResult WaitForEventDelivery(std::uint64_t head, std::uint32_t timeoutMs) override;
Doorbell& ConsumerBell() override;
Doorbell& ProducerBell() override;
MobileGLResult Flush() override { return MOBILEGL_OK; }
MobileGLResult FlushProgress() override { return MOBILEGL_OK; }
bool Attached() const override { return m_memory.Valid(); }
void Detach() override;
TransportRoleTag Role() const override { return m_role; }
private:
SessionSegments m_memory;
RingProducer m_commandsOut;
RingConsumer m_commandsIn;
EventRingProducer m_eventsOut;
EventRingConsumer m_eventsIn;
Doorbell* m_consumer = nullptr;
Doorbell* m_producer = nullptr;
TransportRoleTag m_role = TransportRoleTag::ClientProducer;
LinkArena m_recordArena{}, m_eventArena{};
LinkCursor m_recordCursor{}, m_eventCursor{};
std::vector<std::uint8_t> m_reply;
};
} // namespace MobileGL::MG_Remote::Transport
+34 -1
View File
@@ -17,6 +17,7 @@
#include "RoleMemory.h"
#include "SessionRings.h"
#include <new>
#include <MG_Util/Debug/Log.h>
@@ -308,6 +309,36 @@ namespace MobileGL::MG_Remote::Transport {
return MOBILEGL_OK;
}
MobileGLResult SessionSegments::CreatePrivate(const SessionSegmentSizes& sizes, MemoryRole role) {
Close();
const std::uint64_t bytes[4] = {SegmentBytesForRing(sizes.CmdRingBytes), sizes.StageBytes,
sizes.ReplyBytes, SegmentBytesForRing(sizes.EventRingBytes)};
for (unsigned i = 0; i != 4; ++i) {
if (!bytes[i] || bytes[i] > static_cast<std::uint64_t>(SIZE_MAX)) {
Close(); return MOBILEGL_ERR_INVALID_ARGUMENT;
}
m_private[i] = ::operator new(static_cast<std::size_t>(bytes[i]),
std::align_val_t{alignof(RingControl)}, std::nothrow);
if (!m_private[i]) { Close(); return MOBILEGL_ERR_INVALID_ARGUMENT; }
m_privateSizes[i] = bytes[i];
std::memset(m_private[i], 0, static_cast<std::size_t>(bytes[i]));
m_mappedBytes += bytes[i];
}
m_cmdControl = static_cast<RingControl*>(m_private[0]);
m_cmdRingBase = static_cast<std::uint8_t*>(m_private[0]) + sizeof(RingControl);
m_cmdRingCapacity = RingCapacityForSegment(bytes[0]);
m_stageBase = m_private[1]; m_stageBytes = bytes[1];
m_replyBase = m_private[2]; m_replyBytes = bytes[2]; m_replySlotCount = sizes.ReplySlotCount;
m_eventControl = static_cast<RingControl*>(m_private[3]);
m_eventSegmentBase = m_private[3];
m_eventRingBase = static_cast<std::uint8_t*>(m_private[3]) + sizeof(RingControl);
m_eventRingCapacity = RingCapacityForSegment(bytes[3]);
InitRingControl(*m_cmdControl); InitRingControl(*m_eventControl);
m_role = role; m_valid = true;
LedgerAddSegment(role, m_mappedBytes); m_booked = true;
return MOBILEGL_OK;
}
MobileGLResult SessionSegments::AttachInProcess(SessionSegments& owner, MemoryRole role) {
Close();
if (!owner.Valid()) {
@@ -497,6 +528,8 @@ namespace MobileGL::MG_Remote::Transport {
}
for (std::size_t index = 0; index < kSlotCount; ++index) {
m_segments[index] = nullptr;
if (m_private[index]) ::operator delete(m_private[index], std::align_val_t{alignof(RingControl)});
m_private[index] = nullptr; m_privateSizes[index] = 0;
}
m_cmdControl = nullptr;
m_cmdRingBase = nullptr;
@@ -516,7 +549,7 @@ namespace MobileGL::MG_Remote::Transport {
std::uint64_t SessionSegments::AnnouncedSize(SessionSegmentSlot slot) const {
const ShmSegment* segment = m_segments[SlotIndex(slot)];
return segment == nullptr ? 0 : segment->Size();
return segment == nullptr ? m_privateSizes[SlotIndex(slot)] : segment->Size();
}
const char* SessionSegments::AnnouncedName(SessionSegmentSlot slot) const {
@@ -16,6 +16,10 @@
#include <cstddef>
#include <cstring>
#include <memory>
#include <chrono>
#include <cstdlib>
#include <algorithm>
#include <cstdio>
#if defined(__unix__) || defined(__APPLE__) || defined(__ANDROID__)
# define MOBILEGL_SOCKET_TRANSPORT_POSIX 1
@@ -25,6 +29,10 @@
# include <sys/socket.h>
# include <sys/types.h>
# include <unistd.h>
# include <fcntl.h>
# include <netdb.h>
# include <netinet/in.h>
# include <netinet/tcp.h>
#else
# define MOBILEGL_SOCKET_TRANSPORT_POSIX 0
#endif
@@ -36,6 +44,140 @@ namespace MobileGL::MG_Remote::Transport {
namespace {
constexpr std::uint64_t kPumpChunkBytes = 64ull * 1024;
bool TcpName(const std::string& name) { return name.compare(0, 6, "tcp://") == 0; }
bool TcpAddress(const std::string& name, addrinfo** result) {
*result = nullptr;
const std::string address = name.substr(6);
std::string host, port;
if (!address.empty() && address[0] == '[') {
const auto end = address.find(']');
if (end == std::string::npos || end + 1 >= address.size() || address[end + 1] != ':')
return false;
host = address.substr(1, end - 1);
port = address.substr(end + 2);
} else {
const auto colon = address.find(':');
if (colon == std::string::npos || address.find(':', colon + 1) != std::string::npos)
return false;
host = address.substr(0, colon);
port = address.substr(colon + 1);
}
if (host.empty() || port.empty() || port.find_first_not_of("0123456789") != std::string::npos)
return false;
char* end = nullptr;
const auto number = std::strtoul(port.c_str(), &end, 10);
if (*end || number == 0 || number > 65535) return false;
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_NUMERICSERV;
return ::getaddrinfo(host.c_str(), port.c_str(), &hints, result) == 0;
}
bool Loopback(const sockaddr* address) {
if (address->sa_family == AF_INET)
return (ntohl(reinterpret_cast<const sockaddr_in*>(address)->sin_addr.s_addr) >> 24) == 127;
if (address->sa_family == AF_INET6)
return IN6_IS_ADDR_LOOPBACK(&reinterpret_cast<const sockaddr_in6*>(address)->sin6_addr);
return false;
}
bool TcpSocket(int fd) {
sockaddr_storage address{};
socklen_t size = sizeof(address);
return ::getsockname(fd, reinterpret_cast<sockaddr*>(&address), &size) == 0 &&
(address.ss_family == AF_INET || address.ss_family == AF_INET6);
}
bool ConfigureTcp(int fd) {
const int one = 1, idle = 2, interval = 1, count = 3, timeout = 5000;
if (::setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)) != 0 ||
::setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof(one)) != 0) return false;
#if defined(TCP_KEEPIDLE)
if (::setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle)) != 0) return false;
#endif
#if defined(TCP_KEEPINTVL)
if (::setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval)) != 0) return false;
#endif
#if defined(TCP_KEEPCNT)
if (::setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(count)) != 0) return false;
#endif
#if defined(TCP_USER_TIMEOUT)
if (::setsockopt(fd, IPPROTO_TCP, TCP_USER_TIMEOUT, &timeout, sizeof(timeout)) != 0) return false;
#endif
return true;
}
MobileGLResult ConnectTcp(const std::string& name, std::uint32_t timeoutMs, int* outFd) {
addrinfo* addresses = nullptr;
if (!TcpAddress(name, &addresses)) return MOBILEGL_ERR_INVALID_ARGUMENT;
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::milliseconds(std::max(1u, timeoutMs));
do {
for (auto* address = addresses; address; address = address->ai_next) {
const int fd = ::socket(address->ai_family, SOCK_STREAM, IPPROTO_TCP);
if (fd < 0) continue;
const int flags = ::fcntl(fd, F_GETFL, 0);
::fcntl(fd, F_SETFL, flags | O_NONBLOCK);
bool connected = ::connect(fd, address->ai_addr, address->ai_addrlen) == 0;
if (!connected && errno == EINPROGRESS) {
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(
deadline - std::chrono::steady_clock::now()).count();
pollfd pfd{fd, POLLOUT, 0};
int rc;
do { rc = ::poll(&pfd, 1, static_cast<int>(std::max<std::int64_t>(0, remaining))); }
while (rc < 0 && errno == EINTR && std::chrono::steady_clock::now() < deadline);
int error = 0;
socklen_t length = sizeof(error);
connected = rc > 0 && ::getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &length) == 0 && error == 0;
}
if (connected && ConfigureTcp(fd)) {
::fcntl(fd, F_SETFL, flags);
::fcntl(fd, F_SETFD, FD_CLOEXEC);
*outFd = fd;
::freeaddrinfo(addresses);
return MOBILEGL_OK;
}
::close(fd);
if (std::chrono::steady_clock::now() >= deadline) break;
}
::usleep(5000);
} while (std::chrono::steady_clock::now() < deadline);
::freeaddrinfo(addresses);
WireLogError("MG_Remote SocketTransport: TCP connect to %s exhausted %u ms", name.c_str(), timeoutMs);
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
MobileGLResult ListenTcp(const std::string& name, int* outFd) {
addrinfo* addresses = nullptr;
if (!TcpAddress(name, &addresses)) return MOBILEGL_ERR_INVALID_ARGUMENT;
const char* token = std::getenv("MOBILEGL_IPC_TOKEN");
const bool authenticated = token != nullptr && token[0] != '\0';
MobileGLResult result = MOBILEGL_ERR_UNSUPPORTED;
for (auto* address = addresses; address; address = address->ai_next) {
if (!authenticated && !Loopback(address->ai_addr)) {
WireLogError("MG_Remote: Refuse{AuthenticationRequired} non-loopback TCP listen needs MOBILEGL_IPC_TOKEN");
result = MOBILEGL_ERR_PROTOCOL_MISMATCH;
continue;
}
const int fd = ::socket(address->ai_family, SOCK_STREAM, IPPROTO_TCP);
if (fd < 0) continue;
const int one = 1;
::setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
::fcntl(fd, F_SETFD, FD_CLOEXEC);
if (::bind(fd, address->ai_addr, address->ai_addrlen) == 0 && ::listen(fd, 8) == 0) {
*outFd = fd;
result = MOBILEGL_OK;
break;
}
::close(fd);
}
::freeaddrinfo(addresses);
return result;
}
void CloseIfOpen(int& fd) {
if (fd >= 0) {
::close(fd);
@@ -116,6 +258,7 @@ namespace MobileGL::MG_Remote::Transport {
}
MobileGLResult ConnectOne(const std::string& path, std::uint32_t timeoutMs, int* outFd) {
if (TcpName(path)) return ConnectTcp(path, timeoutMs, outFd);
sockaddr_un address{};
socklen_t length = 0;
if (!FillAddress(path, &address, &length)) {
@@ -155,6 +298,7 @@ namespace MobileGL::MG_Remote::Transport {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
*outListenFd = -1;
if (TcpName(path)) return ListenTcp(path, outListenFd);
sockaddr_un address{};
socklen_t length = 0;
if (!FillAddress(path, &address, &length)) {
@@ -210,7 +354,7 @@ namespace MobileGL::MG_Remote::Transport {
}
int accepted[2] = {-1, -1};
for (int index = 0; index < 2; ++index) {
const int ready = WaitReadable(listenFd, timeoutMs);
const int ready = WaitReadable(listenFd, index == 0 ? timeoutMs : 2000);
if (ready <= 0) {
for (int fd : accepted) {
if (fd >= 0) ::close(fd);
@@ -218,7 +362,7 @@ namespace MobileGL::MG_Remote::Transport {
// Half a client is not a client. Both connections or neither, so
// a peer that died between the two cannot leave a session that
// looks up but has no way to receive a descriptor.
WireLogError("MG_Remote SocketTransport: only %d of 2 connections arrived within "
if (index != 0 || ready < 0) WireLogError("MG_Remote SocketTransport: only %d of 2 connections arrived within "
"%u ms", index, timeoutMs);
return ready == 0 ? MOBILEGL_ERR_TIMEOUT : MOBILEGL_ERR_TRANSPORT_CLOSED;
}
@@ -230,6 +374,11 @@ namespace MobileGL::MG_Remote::Transport {
WireLogError("MG_Remote SocketTransport: accept failed: %s", std::strerror(errno));
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
::fcntl(accepted[index], F_SETFD, FD_CLOEXEC);
if (TcpSocket(accepted[index]) && !ConfigureTcp(accepted[index])) {
for (int fd : accepted) if (fd >= 0) ::close(fd);
return MOBILEGL_ERR_UNSUPPORTED;
}
}
// First connection is control, second is aux. The order IS the protocol;
// it is stated here and in ConnectTo and nowhere else.
@@ -292,7 +441,20 @@ namespace MobileGL::MG_Remote::Transport {
}
SocketTransport::SocketTransport(int streamFd, int auxFd, TransportRole role)
: m_streamFd(streamFd), m_auxFd(auxFd), m_role(role) {}
: m_streamFd(streamFd), m_auxFd(auxFd), m_tcp(TcpSocket(streamFd)), m_role(role) {}
int SocketTransport::TakeDataFd() {
if (!m_tcp) return -1;
const int fd = m_auxFd;
m_auxFd = -1;
return fd;
}
void SocketTransport::CloseLocalCopy() {
CloseIfOpen(m_streamFd);
CloseIfOpen(m_auxFd);
m_closed = true;
}
SocketTransport::~SocketTransport() { Shutdown(); }
@@ -340,7 +502,8 @@ namespace MobileGL::MG_Remote::Transport {
m_closed = true;
return MOBILEGL_ERR_TRANSPORT_CLOSED;
}
WireLogError("MG_Remote SocketTransport: send failed after %llu of %llu bytes: %s",
// SendFrame is also the log sink: do not recurse into the logger.
std::fprintf(stderr, "MG_Remote SocketTransport: send failed after %llu of %llu bytes: %s\n",
static_cast<unsigned long long>(written),
static_cast<unsigned long long>(framed.size()), std::strerror(errno));
m_closed = true;
@@ -457,7 +620,7 @@ namespace MobileGL::MG_Remote::Transport {
}
MobileGLResult SocketTransport::ShareFd(int fd, MobileGLByteSpan sideband) {
if (m_auxFd < 0) {
if (m_tcp || m_auxFd < 0) {
// A link that declared no descriptor passing. Rule G: it does not
// invent a third answer, and the caller transfers the bytes instead.
return MOBILEGL_ERR_UNSUPPORTED;
@@ -472,13 +635,15 @@ namespace MobileGL::MG_Remote::Transport {
MobileGLResult SocketTransport::ReceiveFd(int* outFd, MobileGLMutableByteSpan sideband,
std::uint64_t* outSidebandSize,
std::uint32_t timeoutMs) {
if (m_auxFd < 0) {
if (m_tcp || m_auxFd < 0) {
return MOBILEGL_ERR_UNSUPPORTED;
}
return FdPassing::ReceiveFd(m_auxFd, outFd, sideband, outSidebandSize, timeoutMs);
}
void SocketTransport::Shutdown() {
// Wake a blocking ReceiveFrame before taking its lock.
if (m_streamFd >= 0) ::shutdown(m_streamFd, SHUT_RDWR);
// Idempotent, and tears down BOTH directions - which on a socket is what
// closing does anyway. Taking both locks in a fixed order (send, then
// recv) so a concurrent sender and the dedicated reader cannot deadlock
@@ -510,6 +675,8 @@ namespace MobileGL::MG_Remote::Transport {
SocketTransport::SocketTransport(int, int, TransportRole role) : m_role(role) {}
SocketTransport::~SocketTransport() = default;
int SocketTransport::TakeDataFd() { return -1; }
void SocketTransport::CloseLocalCopy() {}
MobileGLResult SocketTransport::SendFrame(MobileGLByteSpan) { return MOBILEGL_ERR_UNSUPPORTED; }
MobileGLResult SocketTransport::ReceiveFrame(MobileGLMutableByteSpan, std::uint64_t*,
+10 -1
View File
@@ -43,6 +43,7 @@
// calls it, the peer's reads return 0 and its sends fail.
#pragma once
#include <atomic>
#include "Framing.h"
#include "ITransport.h"
@@ -124,6 +125,13 @@ namespace MobileGL::MG_Remote::Transport {
// transport owns, so a test can assert the child inherited exactly these.
int StreamFd() const { return m_streamFd; }
int AuxFd() const { return m_auxFd; }
bool IsTcp() const { return m_tcp; }
// TCP's second connection belongs exclusively to the data plane.
// Taking it disables all descriptor passing on this transport.
int TakeDataFd();
// Parent of a session child closes its duplicate without shutdown(2),
// which would also disconnect the child's inherited socket.
void CloseLocalCopy();
private:
// Pulls whatever the socket has into the reassembler. Returns
@@ -132,6 +140,7 @@ namespace MobileGL::MG_Remote::Transport {
int m_streamFd = -1;
int m_auxFd = -1;
bool m_tcp = false;
TransportRole m_role = TransportRole::Client;
// ITransport's threading rule: callers serialise sends, and receives may
@@ -143,7 +152,7 @@ namespace MobileGL::MG_Remote::Transport {
std::mutex m_recvMutex;
FrameReader m_reader;
bool m_closed = false;
std::atomic<bool> m_closed{false};
bool m_failed = false; // framing violated: latched, never recovers
};
+506
View File
@@ -0,0 +1,506 @@
// SPDX-License-Identifier: LGPL-3.0-only
#include "StreamLink.h"
#include "SessionRings.h"
#include "ShmLink.h"
#include "Doorbell.h"
#include "ReplySlot.h"
#include "Framing.h"
#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <cstring>
#include <mutex>
#include <thread>
#include <vector>
#if !defined(_WIN32)
#include <cerrno>
#include <sys/socket.h>
#include <unistd.h>
#if defined(__linux__)
#include <pthread.h>
#endif
#endif
namespace MobileGL::MG_Remote::Transport {
namespace {
constexpr std::uint32_t kDataMagic = 0x444c474d; // MGLD, little endian
enum Kind : unsigned {
Cmd = 1,
Stage,
ClientProgress,
ProgressFrame,
Reply,
Event
};
void Put32(std::uint8_t* p, std::uint32_t v) {
for (unsigned i = 0; i < 4; ++i)
p[i] = v >> (i * 8);
}
void Put64(std::uint8_t* p, std::uint64_t v) {
for (unsigned i = 0; i < 8; ++i)
p[i] = v >> (i * 8);
}
std::uint32_t Get32(const std::uint8_t* p) {
std::uint32_t v = 0;
for (unsigned i = 0; i < 4; ++i)
v |= std::uint32_t(p[i]) << (i * 8);
return v;
}
std::uint64_t Get64(const std::uint8_t* p) {
std::uint64_t v = 0;
for (unsigned i = 0; i < 8; ++i)
v |= std::uint64_t(p[i]) << (i * 8);
return v;
}
[[noreturn]] void Corrupt(const char* detail) {
WireLogFatal("MGPipe: Fatal{ProtocolCorruption, \"StreamLink\"} %s", detail);
}
void Advance(std::atomic<std::uint64_t>& value, std::uint64_t next) {
if (next < value.load(std::memory_order_acquire)) Corrupt("watermark moved backwards");
value.store(next, std::memory_order_release);
}
class StreamBell final : public Doorbell {
public:
void Notify() override { m_bell.Notify(); }
bool Park(std::uint32_t timeout) override { return m_bell.Park(timeout); }
void Reset() override { m_bell.Reset(); }
bool Dead() const override { return m_bell.Dead(); }
bool PeerHungUp() const override { return m_peer.load(std::memory_order_acquire); }
void Kill(bool peer) {
if (peer) m_peer.store(true, std::memory_order_release);
m_bell.Kill();
}
private:
CondVarDoorbell m_bell;
std::atomic<bool> m_peer{false};
};
} // namespace
struct StreamLink::Impl {
std::unique_ptr<ShmLink> owned = std::make_unique<ShmLink>();
SessionSegments* memory = nullptr;
TransportRoleTag role = TransportRoleTag::ClientProducer;
int fd = -1;
std::atomic<bool> closed{false}, stopping{false};
StreamBell consumer, producer;
std::thread reader;
std::mutex writeMutex, replyMutex;
std::condition_variable replyReady;
std::vector<LinkSpan> stage;
std::vector<std::uint8_t> reply, readReply;
std::uint64_t replySeq = 0, sentCmd = 0, receivedCmd = 0, sentEvent = 0, receivedEvent = 0;
std::int32_t replyStatus = 0;
std::uint64_t lastProgressSeq = 0;
std::chrono::steady_clock::time_point lastProgress = std::chrono::steady_clock::now();
LinkArena records{}, events{};
LinkCursor recordCursor{}, eventCursor{};
bool Client() const { return role == TransportRoleTag::ClientProducer; }
RingControl& C() const { return *memory->CmdControl(); }
RingControl& E() const { return *memory->EventControl(); }
std::uint64_t MaxReply() const {
return memory->ReplyBytes() / memory->ReplySlotCount() - sizeof(ReplySlotHeader);
}
void Die(bool peer) {
closed.store(true, std::memory_order_release);
consumer.Kill(peer);
producer.Kill(peer);
replyReady.notify_all();
#if !defined(_WIN32)
if (fd >= 0) ::shutdown(fd, SHUT_RDWR);
#endif
}
bool Write(const void* data, std::size_t size) {
#if !defined(_WIN32)
auto* p = static_cast<const std::uint8_t*>(data);
while (size) {
const auto n = ::send(fd, p, size, MSG_NOSIGNAL);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) {
Die(!stopping.load(std::memory_order_acquire));
return false;
}
p += n;
size -= static_cast<std::size_t>(n);
}
return true;
#else
(void)data;
(void)size;
return false;
#endif
}
bool Read(void* data, std::size_t size) {
#if !defined(_WIN32)
auto* p = static_cast<std::uint8_t*>(data);
while (size) {
const auto n = ::recv(fd, p, size, 0);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) {
Die(!stopping.load(std::memory_order_acquire));
return false;
}
p += n;
size -= static_cast<std::size_t>(n);
}
return true;
#else
(void)data;
(void)size;
return false;
#endif
}
bool Send(unsigned kind, std::uint64_t a, std::uint64_t b, const void* payload, std::uint64_t size) {
if (size > kMaxFramePayloadSize) Corrupt("outgoing frame exceeds limit");
std::uint8_t h[28]{};
Put32(h, kDataMagic);
Put32(h + 4, static_cast<std::uint32_t>(size));
h[8] = static_cast<std::uint8_t>(kind);
Put64(h + 12, a);
Put64(h + 20, b);
return Write(h, sizeof h) && (!size || Write(payload, static_cast<std::size_t>(size)));
}
bool SendRing(unsigned kind, void* base, std::uint64_t capacity, std::uint64_t head, std::uint64_t& sent) {
if (head < sent || head - sent > capacity) Corrupt("outgoing ring window invalid");
while (sent < head) {
const auto at = sent % capacity;
const auto count = std::min({head - sent, capacity - at, kMaxFramePayloadSize});
if (!Send(kind, sent, 0, static_cast<std::uint8_t*>(base) + at, count)) return false;
sent += count;
}
return true;
}
bool SendEvents() {
return SendRing(Event, memory->EventRingBase(), memory->EventRingCapacity(),
E().cmdHead.load(std::memory_order_acquire), sentEvent);
}
bool SendProgress() {
// Events and replies always precede the applied watermark that publishes them.
if (!SendEvents()) return false;
std::uint8_t p[64]{};
Put64(p, C().Progress.appliedSeq.load(std::memory_order_acquire));
Put64(p + 8, C().Progress.retiredSeq.load(std::memory_order_acquire));
Put64(p + 16, C().Progress.completedFrameSerial.load(std::memory_order_acquire));
Put64(p + 24, C().Progress.presentAckSerial.load(std::memory_order_acquire));
Put64(p + 32, C().cmdAppliedTail.load(std::memory_order_acquire));
// FreeBytes uses RETIRED tail. Sending only applied tail silently permits
// reuse of borrowed command slots, and omitting this tail wedges on wrap.
Put64(p + 40, C().cmdRetiredTail.load(std::memory_order_acquire));
Put64(p + 48, C().eventRingFull.load(std::memory_order_acquire));
Put64(p + 56, C().eventDropped.load(std::memory_order_acquire));
lastProgressSeq = Get64(p);
lastProgress = std::chrono::steady_clock::now();
return Send(ProgressFrame, 0, 0, p, sizeof p);
}
void ReceiveRing(void* base, std::uint64_t capacity, std::uint64_t tail, std::uint64_t a,
const std::vector<std::uint8_t>& p, std::uint64_t& received) {
if (a != received || a < tail || a - tail > capacity || p.size() > capacity - (a - tail) ||
p.size() > capacity - a % capacity || p.empty())
Corrupt("ring frame outside receive window");
std::memcpy(static_cast<std::uint8_t*>(base) + a % capacity, p.data(), p.size());
received += p.size();
}
void Loop() {
#if defined(__linux__)
pthread_setname_np(pthread_self(), Client() ? "mgl-cli-io" : "mgl-srv-io");
#endif
for (;;) {
std::uint8_t h[28];
if (!Read(h, sizeof h)) return;
const auto size = Get32(h + 4), kind = unsigned(h[8]);
const auto a = Get64(h + 12), b = Get64(h + 20);
if (Get32(h) != kDataMagic || h[9] || h[10] || h[11] || size > kMaxFramePayloadSize)
Corrupt("invalid data frame header");
// Reject the size/window before allocating or reading a hostile payload.
if ((kind == Stage && (Client() || a > memory->StageBytes() || size > memory->StageBytes() - a)) ||
(kind == Cmd && (Client() || size > memory->CmdRingCapacity())) ||
(kind == Event && (!Client() || size > memory->EventRingCapacity())) ||
(kind == Reply && (!Client() || size > MaxReply())) ||
(kind == ProgressFrame && (!Client() || size != 64)) ||
(kind == ClientProgress && (Client() || size != 8)) || kind < Cmd || kind > Event)
Corrupt("frame kind or size outside negotiated window");
std::vector<std::uint8_t> p(size);
if (size && !Read(p.data(), size)) return;
if (kind == Stage) {
std::memcpy(static_cast<std::uint8_t*>(memory->StageBase()) + a, p.data(), size);
} else if (kind == Cmd) {
ReceiveRing(memory->CmdRingBase(), memory->CmdRingCapacity(),
C().cmdRetiredTail.load(std::memory_order_acquire), a, p, receivedCmd);
} else if (kind == ClientProgress) {
if (a != receivedCmd) Corrupt("cmdHead does not match delivered bytes");
const auto tail = Get64(p.data());
if (tail > E().cmdHead.load(std::memory_order_acquire)) Corrupt("event tail ahead of head");
const auto oldTail = E().cmdRetiredTail.load(std::memory_order_acquire);
Advance(E().cmdAppliedTail, tail);
Advance(E().cmdRetiredTail, tail);
// Only a real drain can clear a latched backlog. A command-only
// publication carrying the old tail must not undo a new full latch.
if (tail > oldTail) C().eventRingFull.store(0, std::memory_order_release);
Watermark::AdvanceSubmitted(C(), b);
Advance(C().cmdHead, a);
consumer.Notify();
} else if (kind == Event) {
ReceiveRing(memory->EventRingBase(), memory->EventRingCapacity(),
E().cmdRetiredTail.load(std::memory_order_acquire), a, p, receivedEvent);
Advance(E().cmdHead, receivedEvent);
producer.Notify();
} else if (kind == ProgressFrame) {
const auto* v = p.data();
if (Get64(v + 8) > Get64(v) || Get64(v) > C().submittedSeq.load(std::memory_order_acquire) ||
Get64(v + 40) > Get64(v + 32) || Get64(v + 32) > C().cmdHead.load(std::memory_order_acquire) ||
Get64(v + 48) > 1)
Corrupt("progress ahead of delivered work");
Advance(C().cmdAppliedTail, Get64(v + 32));
Advance(C().cmdRetiredTail, Get64(v + 40));
C().eventRingFull.store(static_cast<std::uint32_t>(Get64(v + 48)), std::memory_order_release);
C().eventDropped.store(static_cast<std::uint32_t>(Get64(v + 56)), std::memory_order_release);
Watermark::AdvanceRetired(C(), Get64(v + 8));
Watermark::AdvanceCompletedFrame(C(), Get64(v + 16));
Watermark::AdvancePresentAck(C(), Get64(v + 24));
Watermark::AdvanceApplied(C(), Get64(v));
producer.Notify();
} else if (kind == Reply) {
{
std::lock_guard<std::mutex> lock(replyMutex);
if (a <= replySeq) Corrupt("reply sequence moved backwards");
replySeq = a;
replyStatus = static_cast<std::int32_t>(static_cast<std::uint32_t>(b));
reply = std::move(p);
}
replyReady.notify_all();
producer.Notify();
}
}
}
};
StreamLink::StreamLink() : m_impl(new Impl) {}
StreamLink::~StreamLink() {
Detach();
}
MobileGLResult StreamLink::Attach(int fd, SessionSegments& mirrors, TransportRoleTag role) {
#if defined(_WIN32)
(void)fd;
(void)mirrors;
(void)role;
return MOBILEGL_ERR_UNSUPPORTED;
#else
if (fd < 0 || !mirrors.Valid() || !mirrors.ReplySlotCount() ||
mirrors.ReplyBytes() / mirrors.ReplySlotCount() <= sizeof(ReplySlotHeader))
return MOBILEGL_ERR_INVALID_ARGUMENT;
Detach();
m_impl.reset(new Impl);
auto& x = *m_impl;
x.fd = fd;
x.memory = &mirrors;
x.role = role;
x.records = {mirrors.CmdRingBase(), mirrors.CmdRingCapacity(), mirrors.CmdRingCapacity() - 1, 0};
x.recordCursor = {mirrors.CmdRingBase(), mirrors.CmdRingCapacity(), mirrors.CmdRingCapacity() - 1, 0};
x.events = {mirrors.EventRingBase(), mirrors.EventRingCapacity(), mirrors.EventRingCapacity() - 1, 0};
x.eventCursor = {mirrors.EventRingBase(), mirrors.EventRingCapacity(), mirrors.EventRingCapacity() - 1, 0};
x.reader = std::thread([&x] { x.Loop(); });
return MOBILEGL_OK;
#endif
}
MobileGLResult StreamLink::AttachOwned(int fd, const SessionSegmentSizes& sizes, TransportRoleTag role) {
auto owned = std::make_unique<ShmLink>();
const auto result = owned->Memory().CreatePrivate(
sizes, role == TransportRoleTag::ClientProducer ? MemoryRole::Client : MemoryRole::Server);
if (result != MOBILEGL_OK) return result;
const auto attached = Attach(fd, owned->Memory(), role);
if (attached == MOBILEGL_OK) m_impl->owned = std::move(owned);
return attached;
}
MobileGLResult CreateStreamLink(int fd, const SessionSegmentSizes& sizes, TransportRoleTag role,
std::unique_ptr<ILink>& out) {
auto link = std::make_unique<StreamLink>();
const auto result = link->AttachOwned(fd, sizes, role);
if (result == MOBILEGL_OK) out = std::move(link);
#if !defined(_WIN32)
else if (fd >= 0)
::close(fd);
#endif
return result;
}
void StreamLink::InitializeEndpoints() {
m_impl->owned->SetRole(m_impl->role);
m_impl->owned->InitializeEndpoints();
}
SessionSegments& StreamLink::Memory() {
return m_impl->owned->Memory();
}
RingProducer& StreamLink::CommandsOut() {
return m_impl->owned->CommandsOut();
}
RingConsumer& StreamLink::CommandsIn() {
return m_impl->owned->CommandsIn();
}
EventRingProducer& StreamLink::EventsOut() {
return m_impl->owned->EventsOut();
}
EventRingConsumer& StreamLink::EventsIn() {
return m_impl->owned->EventsIn();
}
LinkCapabilities StreamLink::Capabilities() const {
if (!m_impl->memory) return {};
return {false, false, false, m_impl->memory->CmdRingCapacity() / 2, m_impl->MaxReply()};
}
LinkProgress* StreamLink::Progress() {
return m_impl->memory ? &m_impl->C().Progress : nullptr;
}
LinkEventFlags* StreamLink::EventFlags() {
return m_impl->memory ? reinterpret_cast<LinkEventFlags*>(&m_impl->C().eventRingFull) : nullptr;
}
LinkArena* StreamLink::RecordArena() {
return m_impl->Client() ? &m_impl->records : nullptr;
}
LinkCursor* StreamLink::RecordCursor() {
return m_impl->Client() ? nullptr : &m_impl->recordCursor;
}
LinkArena* StreamLink::EventArena() {
return m_impl->Client() ? nullptr : &m_impl->events;
}
LinkCursor* StreamLink::EventCursor() {
return m_impl->Client() ? &m_impl->eventCursor : nullptr;
}
Doorbell& StreamLink::ConsumerBell() {
return m_impl->consumer;
}
Doorbell& StreamLink::ProducerBell() {
return m_impl->producer;
}
MobileGLResult StreamLink::StageBytes(std::uint64_t, LinkSpan*, void**) {
return MOBILEGL_ERR_UNSUPPORTED;
}
MobileGLResult StreamLink::ResolveSpan(LinkSegment segment, LinkSpan span, const void** out) {
if (!out || !m_impl->memory) return MOBILEGL_ERR_INVALID_ARGUMENT;
auto& m = *m_impl->memory;
const void* base = nullptr;
std::uint64_t cap = 0;
switch (segment) {
case LinkSegment::Cmd:
base = m.CmdRingBase();
cap = m.CmdRingCapacity();
break;
case LinkSegment::Stage:
base = m.StageBase();
cap = m.StageBytes();
break;
case LinkSegment::Reply:
return MOBILEGL_ERR_UNSUPPORTED;
case LinkSegment::Event:
base = m.EventSegmentBase();
cap = m.AnnouncedSize(SessionSegmentSlot::Event);
break;
default:
return MOBILEGL_ERR_UNSUPPORTED;
}
if (span.Offset > cap || span.Size > cap - span.Offset) return MOBILEGL_ERR_PROTOCOL_MISMATCH;
*out = static_cast<const std::uint8_t*>(base) + span.Offset;
return MOBILEGL_OK;
}
void StreamLink::NoteStage(LinkSpan span) {
auto& x = *m_impl;
if (!x.Client() || span.Offset > x.memory->StageBytes() || span.Size > x.memory->StageBytes() - span.Offset)
Corrupt("staged span outside window");
if (!x.stage.empty() && x.stage.back().Offset + x.stage.back().Size == span.Offset)
x.stage.back().Size += span.Size;
else
x.stage.push_back(span);
}
MobileGLResult StreamLink::Flush() {
auto& x = *m_impl;
if (!Attached()) return MOBILEGL_ERR_TRANSPORT_CLOSED;
std::lock_guard<std::mutex> lock(x.writeMutex);
if (!x.Client()) return x.SendEvents() ? MOBILEGL_OK : MOBILEGL_ERR_TRANSPORT_CLOSED;
for (const auto span : x.stage) {
std::uint64_t sent = 0;
while (sent < span.Size) {
const auto count = std::min(span.Size - sent, kMaxFramePayloadSize);
if (!x.Send(Stage, span.Offset + sent, 0,
static_cast<std::uint8_t*>(x.memory->StageBase()) + span.Offset + sent, count))
return MOBILEGL_ERR_TRANSPORT_CLOSED;
sent += count;
}
}
x.stage.clear();
const auto head = x.C().cmdHead.load(std::memory_order_acquire);
if (!x.SendRing(Cmd, x.memory->CmdRingBase(), x.memory->CmdRingCapacity(), head, x.sentCmd))
return MOBILEGL_ERR_TRANSPORT_CLOSED;
std::uint8_t tail[8];
Put64(tail, x.E().cmdRetiredTail.load(std::memory_order_acquire));
return x.Send(ClientProgress, head, x.C().submittedSeq.load(std::memory_order_acquire), tail, sizeof tail)
? MOBILEGL_OK
: MOBILEGL_ERR_TRANSPORT_CLOSED;
}
MobileGLResult StreamLink::FlushProgress() {
auto& x = *m_impl;
if (!Attached()) return MOBILEGL_ERR_TRANSPORT_CLOSED;
if (x.Client()) return Flush();
std::lock_guard<std::mutex> lock(x.writeMutex);
return x.SendProgress() ? MOBILEGL_OK : MOBILEGL_ERR_TRANSPORT_CLOSED;
}
void StreamLink::ProgressChanged() {
auto& x = *m_impl;
if (x.Client() || !Attached()) return;
const auto seq = x.C().Progress.appliedSeq.load(std::memory_order_acquire);
if (seq - x.lastProgressSeq >= 64 ||
std::chrono::steady_clock::now() - x.lastProgress >= std::chrono::milliseconds(1))
FlushProgress();
}
MobileGLResult StreamLink::PostReply(std::uint64_t seq, std::int32_t status, const void* p, std::uint64_t size) {
auto& x = *m_impl;
if (x.Client() || !Attached()) return MOBILEGL_ERR_TRANSPORT_CLOSED;
if (size > x.MaxReply() || (size && !p)) return MOBILEGL_ERR_BUFFER_TOO_SMALL;
std::lock_guard<std::mutex> lock(x.writeMutex);
return x.Send(Reply, seq, static_cast<std::uint32_t>(status), p, size) && x.SendProgress()
? MOBILEGL_OK
: MOBILEGL_ERR_TRANSPORT_CLOSED;
}
MobileGLResult StreamLink::ReadReply(std::uint64_t seq, std::int32_t* status, const void** p, std::uint64_t* size) {
auto& x = *m_impl;
if (!status || !p || !size || !x.Client()) return MOBILEGL_ERR_INVALID_ARGUMENT;
std::unique_lock<std::mutex> lock(x.replyMutex);
// The applied watermark is sent after the reply, so normal callers never
// wait here. A standalone reply reader may wait without polling.
x.replyReady.wait(lock, [&] { return x.replySeq >= seq || x.closed.load(std::memory_order_acquire); });
if (x.replySeq != seq) return x.closed.load() ? MOBILEGL_ERR_TRANSPORT_CLOSED : MOBILEGL_ERR_PROTOCOL_MISMATCH;
x.readReply = x.reply;
*status = x.replyStatus;
*size = x.readReply.size();
*p = x.readReply.data();
return MOBILEGL_OK;
}
std::uint64_t StreamLink::EventPublishedHead() const {
return m_impl->memory ? m_impl->E().cmdHead.load(std::memory_order_acquire) : 0;
}
MobileGLResult StreamLink::WaitForEventDelivery(std::uint64_t head, std::uint32_t timeoutMs) {
auto& x = *m_impl;
if (!x.memory) return MOBILEGL_ERR_NOT_INITIALIZED;
const auto ready = [&] { return x.E().cmdHead.load(std::memory_order_acquire) >= head; };
if (ready()) return MOBILEGL_OK;
const bool delivered = x.producer.Wait(x.C().producerParked, ready, 0, timeoutMs);
return delivered ? MOBILEGL_OK : x.producer.Dead() ? MOBILEGL_ERR_TRANSPORT_CLOSED : MOBILEGL_ERR_TIMEOUT;
}
bool StreamLink::Attached() const {
return m_impl->memory && !m_impl->closed.load(std::memory_order_acquire);
}
bool StreamLink::PeerHungUp() const {
return m_impl->producer.PeerHungUp();
}
TransportRoleTag StreamLink::Role() const {
return m_impl->role;
}
void StreamLink::Detach() {
auto& x = *m_impl;
if (!x.memory) return;
x.stopping.store(true, std::memory_order_release);
x.Die(false);
if (x.reader.joinable()) x.reader.join();
#if !defined(_WIN32)
if (x.fd >= 0) ::close(x.fd);
#endif
x.fd = -1;
x.memory = nullptr;
}
} // namespace MobileGL::MG_Remote::Transport
+25 -83
View File
@@ -1,108 +1,50 @@
// MobileGL - MobileGL/MG_Remote/Transport/StreamLink.h
// 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
// ILink's second implementation, DECLARED BY c6 AND IMPLEMENTED BY NOBODY.
// Authority: CONTRACT-P6.md §8.4. Its phase is P6.5.
//
// The link it exists for is TCP across machines (end state re-ruled 2026-09-22,
// CONTRACT-P6.md §10.3, ARCHITECTURE.md §11.9), and it is one axis of two: the
// control plane (ITransport) is chosen independently, so tcp control + shm data
// on one machine is a legal pair and nothing above Transport/ may tell which.
//
// WHY A HEADER FOR SOMETHING THAT DOES NOT EXIST. ILink's adequacy - "can this
// seam name what a byte stream needs?" - is the one claim c6 makes that an
// argument cannot settle and a compile can. If ILink cannot express progress
// transmission, a send window, a flush, span resolution without a mapping, and
// replies that are messages rather than slots, then StreamLink cannot be
// declared against it, and the build says so. That is c6's red-once: delete
// one ILink method this class declares and the build goes red.
//
// It has already earned its keep. Writing it forced THREE methods that the
// original brief's list - progress transmission, send window, flush - did not
// name:
//
// ResolveSpan - a stream has no mapping to do pointer arithmetic over, so
// the eleven ResolveOrFatal sites in the decoder need the
// link to answer, not a segment table. Without it the seam
// was unimplementable for the case it exists for.
// FlushProgress - separate from Flush because the triggers differ: Flush is
// about records the peer has not seen, FlushProgress is
// about watermarks the peer is WAITING on. On a shared page
// a release-store is both; on a stream, conflating them
// either sends too much or hangs a waiter.
// PostReply / - SEG_REPLY carries no message today: the server memcpys
// ReadReply into a slot addressed `seq % slotCount` and the client
// reads it after observing appliedSeq. A stream turns that
// into a real message carrying its own seq, which DELETES
// the slot pool, the modulus addressing and the stamp
// self-check - so the seam has to own the operation rather
// than hand out the pool.
//
// WHAT P6.5 STILL HAS TO SETTLE, recorded here so it is not rediscovered:
// - SEG_STAGE's three ring cursors are DELIBERATELY DEAD (Ring.h's "DEAD IN P5, DELIBERATELY",
// pinned by a test). Staging is an encoder-local linear allocator that
// reclaims on retiredSeq. A send window either revives them COMPLETELY or
// does not use them; the header names the middle state as a guaranteed
// hang rather than a slow path.
// - appliedSeq is excluded BY NAME from lazy publication (Ring.h's "BATCHING MAY ONLY MAKE A WATERMARK LATE").
// P6.5 AMENDS that rule with a flush-on-idle discipline; it does not infer
// around it.
// - one dedicated reader thread per direction per process, or the event-ring
// flow-control deadlock returns as write() blocked against write().
#pragma once
#include "ILink.h"
#include <memory>
namespace MobileGL::MG_Remote::Transport {
// Every entry point is a named refusal. There is no partial implementation
// and there may not be one: a half-wired stream link is the shape Ring.h
// warns about, where FreeBytes() falls to zero on the first lap and never
// recovers.
//
// The refusals live in StreamLink.cpp (package P6.5) rather than inline,
// for the reason ILink.h gives: no header under Transport/ pulls the logger
// in, and this one does not start.
class SessionSegments;
// One reader owns receive/reassembly. Writers serialize complete frames and use
// the caller thread, so both directions can always drain a blocked peer writer.
class StreamLink final : public ILink {
public:
StreamLink();
~StreamLink() override;
MobileGLResult Attach(int dataFd, SessionSegments& mirrors, TransportRoleTag role);
MobileGLResult AttachOwned(int dataFd, const struct SessionSegmentSizes& sizes, TransportRoleTag role);
SessionSegments& Memory() override;
void InitializeEndpoints() override;
RingProducer& CommandsOut() override;
RingConsumer& CommandsIn() override;
EventRingProducer& EventsOut() override;
EventRingConsumer& EventsIn() override;
LinkCapabilities Capabilities() const override;
LinkProgress* Progress() override;
LinkEventFlags* EventFlags() override;
LinkArena* RecordArena() override;
LinkCursor* RecordCursor() override;
MobileGLResult StageBytes(std::uint64_t bytes, LinkSpan* outSpan,
void** outWritePtr) override;
MobileGLResult ResolveSpan(LinkSegment segment, LinkSpan span,
const void** outPtr) override;
MobileGLResult PostReply(std::uint64_t seq, std::int32_t status,
const void* payload, std::uint64_t size) override;
MobileGLResult ReadReply(std::uint64_t seq, std::int32_t* outStatus,
const void** outPayload, std::uint64_t* outSize) override;
MobileGLResult StageBytes(std::uint64_t, LinkSpan*, void**) override;
MobileGLResult ResolveSpan(LinkSegment, LinkSpan, const void**) override;
MobileGLResult PostReply(std::uint64_t, std::int32_t, const void*, std::uint64_t) override;
MobileGLResult ReadReply(std::uint64_t, std::int32_t*, const void**, std::uint64_t*) override;
LinkArena* EventArena() override;
LinkCursor* EventCursor() override;
std::uint64_t EventPublishedHead() const override;
MobileGLResult WaitForEventDelivery(std::uint64_t head, std::uint32_t timeoutMs) override;
Doorbell& ConsumerBell() override;
Doorbell& ProducerBell() override;
MobileGLResult Flush() override;
MobileGLResult FlushProgress() override;
void NoteStage(LinkSpan span) override;
void ProgressChanged() override;
bool PeerHungUp() const override;
bool Attached() const override;
void Detach() override;
TransportRoleTag Role() const override;
};
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
} // namespace MobileGL::MG_Remote::Transport
+50 -11
View File
@@ -26,6 +26,7 @@
// first place. The static_asserts below pin the collision so it cannot be re-introduced by
// someone who reads the comment and not this file.
#include <MG_Remote/Transport/LinkMetrics.h>
#include "PipeWireCodec.h"
#include <MG_Remote/FatalFunnel.h>
@@ -392,6 +393,17 @@ namespace MobileGL::MG_Remote::Wire {
// SegmentTable
// ---------------------------------------------------------------------------------
void SegmentTable::AttachLink(Transport::ILink* link) {
m_link = link;
for (Uint32 id = kSegCmd; id <= kSegEvent; ++id) {
const auto segment = static_cast<Transport::LinkSegment>(id);
const Uint64 size = link ? link->SegmentSize(segment) : 0;
const void* base = nullptr;
if (link && link->ResolveSpan(segment, {0, size}, &base) != MOBILEGL_OK) base = nullptr;
Install(static_cast<SegmentId>(id), {const_cast<void*>(base), size});
}
}
void SegmentTable::Install(SegmentId seg, SegmentView view) {
if (seg == kSegNone || static_cast<Uint32>(seg) > static_cast<Uint32>(kSegAdopt)) {
WireProtocolFatalAt("SegmentTable::Install", static_cast<Uint64>(seg),
@@ -418,6 +430,11 @@ namespace MobileGL::MG_Remote::Wire {
if (size == 0) {
return nullptr;
}
if (m_link) {
const void* bytes = nullptr;
return m_link->ResolveSpan(static_cast<Transport::LinkSegment>(seg), {offset, size}, &bytes)
== MOBILEGL_OK ? bytes : nullptr;
}
const SegmentView& view = m_views[static_cast<SizeT>(seg)];
if (view.Base == nullptr) {
return nullptr;
@@ -822,11 +839,24 @@ namespace MobileGL::MG_Remote::Wire {
// Encoder
// ---------------------------------------------------------------------------------
PipeWireEncoder::PipeWireEncoder(Transport::ILink* link, SegmentTable* segments)
: m_segments(segments) {
if (!link) return;
m_progress = link->Progress(); m_signals = link->Signals(); m_cmd = &link->CommandsOut();
m_link = link->Capabilities().PublishIsDelivery ? nullptr : link;
}
PipeWireEncoder::PipeWireEncoder(Transport::RingControl* control, Transport::RingProducer* cmd,
Transport::RingProducer* stage, SegmentTable* segments)
: m_control(control), m_cmd(cmd), m_stage(stage), m_segments(segments) {}
: m_cmd(cmd), m_stage(stage), m_segments(segments) {
if (control) {
m_progress = &control->Progress;
m_signals = {&control->cmdHead, &control->submittedSeq, &control->consumerParked,
&control->producerParked, &control->eventRingFull};
}
}
Bool PipeWireEncoder::Valid() const { return m_control != nullptr && m_cmd != nullptr; }
Bool PipeWireEncoder::Valid() const { return m_progress != nullptr && m_cmd != nullptr; }
Uint8* PipeWireEncoder::StageAllocate(Uint64 size) {
if (m_stageBase == nullptr) {
@@ -896,6 +926,7 @@ namespace MobileGL::MG_Remote::Wire {
if ((m_stageHead + skip + need) - m_stageTail <= m_stageCapacity) {
const Uint64 at = (m_stageHead + skip) % m_stageCapacity;
m_stageHead += skip + need;
Transport::LinkMetricsStageBytes(size);
return m_stageBase + at;
}
if (!reclaimed) {
@@ -907,7 +938,7 @@ namespace MobileGL::MG_Remote::Wire {
if (m_stageRetirementBell == nullptr || m_stageMarkFront == m_stageMarks.size()) break;
const Uint64 pending = m_stageMarks[m_stageMarkFront].Seq;
const auto ready = [&] {
return m_control->Progress.retiredSeq.load(std::memory_order_acquire) >= pending;
return m_progress->retiredSeq.load(std::memory_order_acquire) >= pending;
};
if (!ready()) {
// The allocation still cannot progress after reclamation. Count this
@@ -920,7 +951,8 @@ namespace MobileGL::MG_Remote::Wire {
// again. The hook is null in every standalone codec, which has no session and
// therefore no reverse channel to drain.
if (m_stageWaitHook != nullptr) m_stageWaitHook(m_stageWaitSelf);
if (!ready() && !m_stageRetirementBell->Wait(m_control->producerParked, ready, 0, 5000)) {
if (m_link) m_link->Flush();
if (!ready() && !m_stageRetirementBell->Wait(*m_signals.ProducerParked, ready, 0, 5000)) {
SessionFail(MGFatalFamily::RetirementWaitFailed, "MGPipe: Fatal{RetirementWaitFailed, \"SEG_STAGE\"} producer wait "
"ended before the pending allocation retired (shutdown or timeout)");
}
@@ -936,7 +968,7 @@ namespace MobileGL::MG_Remote::Wire {
static_cast<unsigned long long>(m_stageCapacity),
static_cast<unsigned long long>(m_stageHead - m_stageTail),
static_cast<unsigned long long>(
m_control != nullptr ? m_control->Progress.retiredSeq.load(std::memory_order_acquire) : 0));
m_progress != nullptr ? m_progress->retiredSeq.load(std::memory_order_acquire) : 0));
}
MGPBlobRef PipeWireEncoder::StageBytes(const void* bytes, Uint64 size) {
@@ -958,6 +990,7 @@ namespace MobileGL::MG_Remote::Wire {
Uint8* slot = StageAllocate(size);
std::memcpy(slot, bytes, static_cast<SizeT>(size));
const Uint64 offset = static_cast<Uint64>(slot - m_stageBase);
if (m_link) m_link->NoteStage({offset, size});
MGPBlobRef ref{};
ref.Seg = kSegStage;
@@ -1222,10 +1255,10 @@ namespace MobileGL::MG_Remote::Wire {
}
void PipeWireEncoder::ReclaimStagedBytes() {
if (m_control == nullptr) {
if (m_progress == nullptr) {
return;
}
const Uint64 retired = m_control->Progress.retiredSeq.load(std::memory_order_acquire);
const Uint64 retired = m_progress->retiredSeq.load(std::memory_order_acquire);
Uint64 upTo = m_stageTail;
while (m_stageMarkFront < m_stageMarks.size() &&
m_stageMarks[m_stageMarkFront].Seq <= retired) {
@@ -1281,11 +1314,11 @@ namespace MobileGL::MG_Remote::Wire {
// popping a ring, and the release store on SEG_CMD's head below is what orders the
// staged writes before the record that names them.
m_cmd->Publish();
if (m_control != nullptr) {
if (m_progress != nullptr) {
// submittedSeq is the one watermark the PRODUCER owns (Ring.h's five-watermark
// block). Nobody waits on it; it answers "how far ahead of the server is the
// client right now".
m_control->submittedSeq.store(m_emitSeq, std::memory_order_release);
m_signals.SubmittedSeq->store(m_emitSeq, std::memory_order_release);
}
// The reclaim has a trigger in this package, rather than depending on a c1 barrier
// that does not exist yet: every publish is a chance to notice what the server has
@@ -1324,9 +1357,15 @@ namespace MobileGL::MG_Remote::Wire {
// Decoder
// ---------------------------------------------------------------------------------
PipeWireDecoder::PipeWireDecoder(Transport::ILink* link, SegmentTable* segments, ReplySink* replies)
: m_segments(segments), m_replies(replies), m_valid(link && link->Attached()) {
m_auditPoison = MG_Config::Ipc.Audit;
InstallApplyHook();
}
PipeWireDecoder::PipeWireDecoder(Transport::RingControl* control, SegmentTable* segments,
ReplySink* replies)
: m_control(control), m_segments(segments), m_replies(replies) {
: m_segments(segments), m_replies(replies), m_valid(control != nullptr) {
m_auditPoison = MG_Config::Ipc.Audit;
// Once, at construction, beside the resolver it is modelled on - not per record from
// the apply thread. The thunk is inert without a decoder on the calling thread, so an
@@ -1340,7 +1379,7 @@ namespace MobileGL::MG_Remote::Wire {
void PipeWireDecoder::UninstallApplyHook() { MG_Pipe::gMGPipeWireRecordApply = nullptr; }
Bool PipeWireDecoder::Valid() const { return m_control != nullptr && m_segments != nullptr; }
Bool PipeWireDecoder::Valid() const { return m_valid && m_segments != nullptr; }
Uint64 PipeWireDecoder::AppliedSeq() const { return m_applySeq; }
+9 -2
View File
@@ -84,6 +84,7 @@ namespace MobileGL::MG_Remote::Wire {
// span at all (it only ever writes Ptr = nullptr). Install() therefore takes the role.
class SegmentTable {
public:
void AttachLink(Transport::ILink* link);
void Install(SegmentId seg, SegmentView view);
SegmentView Get(SegmentId seg) const;
@@ -100,6 +101,7 @@ namespace MobileGL::MG_Remote::Wire {
static void UninstallProcessResolver();
private:
Transport::ILink* m_link = nullptr;
SegmentView m_views[kSegAdopt + 1];
};
@@ -218,6 +220,7 @@ namespace MobileGL::MG_Remote::Wire {
class PipeWireEncoder {
public:
PipeWireEncoder() = default;
PipeWireEncoder(Transport::ILink* link, SegmentTable* segments);
PipeWireEncoder(Transport::RingControl* control, Transport::RingProducer* cmd,
Transport::RingProducer* stage, SegmentTable* segments);
@@ -338,6 +341,7 @@ namespace MobileGL::MG_Remote::Wire {
Uint64 StageReclaimWaits() const;
// The live session supplies its shutdown-aware producer doorbell. Standalone codecs
// without a consumer cannot wait for retirement and retain the named refusal.
void SetLink(Transport::ILink* link) { m_link = link; }
void SetStageRetirementDoorbell(Transport::Doorbell* bell) { m_stageRetirementBell = bell; }
// P5e (ra, CONTRACT-P5E §2.6): THE STAGE BELL IS A CLIENT WAIT, so it has to drain the
@@ -373,6 +377,7 @@ namespace MobileGL::MG_Remote::Wire {
// over the segment; the in-segment offset is `cursor % capacity` and a run that would
// straddle the end skips to the boundary, exactly as a ring does, but WITHOUT touching
// RingControl - see ReclaimStagedBytes above.
Transport::ILink* m_link = nullptr;
Uint8* StageAllocate(Uint64 size);
// AN EMPTY STAGE STARTS OVER AT ZERO, so that the wrap skip is only ever charged
@@ -386,7 +391,8 @@ namespace MobileGL::MG_Remote::Wire {
// the head and underflow StagedBytesInFlight().
void RebaseEmptyStage();
Transport::RingControl* m_control = nullptr;
Transport::LinkProgress* m_progress = nullptr;
Transport::LinkSignals m_signals{};
Transport::RingProducer* m_cmd = nullptr;
Transport::RingProducer* m_stage = nullptr;
SegmentTable* m_segments = nullptr;
@@ -606,6 +612,7 @@ namespace MobileGL::MG_Remote::Wire {
class PipeWireDecoder {
public:
PipeWireDecoder() = default;
PipeWireDecoder(Transport::ILink* link, SegmentTable* segments, ReplySink* replies);
PipeWireDecoder(Transport::RingControl* control, SegmentTable* segments,
ReplySink* replies);
@@ -707,7 +714,7 @@ namespace MobileGL::MG_Remote::Wire {
friend Bool MGPipeWireRecordApplyThunk(MG_Pipe::MGPWireOp, const void*, Uint64, Uint64);
Transport::RingControl* m_control = nullptr;
Bool m_valid = false;
SegmentTable* m_segments = nullptr;
ReplySink* m_replies = nullptr;
WireVerbSink* m_verbs = nullptr;
+1 -1
View File
@@ -22,7 +22,7 @@ if (NOT WIN32)
# P6 so: the SAME nine ITransport-contract cases InProcessTransportTest runs, over a real
# socketpair with real threads, plus the three a stream has and a message queue does not.
# POSIX for the same reason as FdPassingTest - and because CONTRACT-P6 §2.6 lands POSIX only.
list(APPEND MOBILEGL_WIRE_TESTS SocketTransportTest)
list(APPEND MOBILEGL_WIRE_TESTS SocketTransportTest StreamLinkTest)
# P6 sm: two MobileGL processes. Needs the real server image beside it, so
# the dependency is declared after the loop rather than assumed.
list(APPEND MOBILEGL_WIRE_TESTS ServerSpawnTest)
@@ -118,6 +118,8 @@ TEST(ProtocolSmokeTest, UnionTagsAreFrozenWireValues) {
EXPECT_EQ(static_cast<int>(CtrlMsg::AuxRequest), 8);
EXPECT_EQ(static_cast<int>(CtrlMsg::Fatal), 9);
EXPECT_EQ(static_cast<int>(CtrlMsg::LogLine), 10);
EXPECT_EQ(static_cast<int>(CtrlMsg::Refuse), 11);
EXPECT_EQ(static_cast<int>(CtrlMsg::LogFlush), 12);
EXPECT_EQ(static_cast<int>(SegmentKind::Cmd), 1);
EXPECT_EQ(static_cast<int>(SegmentKind::Adopt), 6);
@@ -156,3 +158,27 @@ TEST(ProtocolSmokeTest, TravelsAcrossTheTransportUnchanged) {
ASSERT_NE(hello, nullptr);
EXPECT_EQ(hello->pid(), 4242u);
}
TEST(ProtocolSmokeTest, StreamHandshakeCarriesIndependentWireBuildAndServerWindows) {
::flatbuffers::FlatBufferBuilder builder(512);
auto terms = CreateLinkTerms(builder, DataPlane::Stream, WireForm::StructImage,
1048560, 8388608, 33554432, 262144);
auto hello = CreateHelloDirect(builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR,
"commit-123", 1, 42, nullptr, 0, 0x12345678, terms, "pairing-token", DialMode::Connect);
auto envelope = CreateCtrlEnvelope(builder, CtrlMsg::Hello, hello.Union());
FinishCtrlEnvelopeBuffer(builder, envelope);
::flatbuffers::Verifier verifier(builder.GetBufferPointer(), builder.GetSize());
ASSERT_TRUE(VerifyCtrlEnvelopeBuffer(verifier));
const auto* parsed = GetCtrlEnvelope(builder.GetBufferPointer())->msg_as_Hello();
ASSERT_NE(parsed, nullptr);
EXPECT_EQ(parsed->wireFingerprint(), 0x12345678u);
EXPECT_EQ(parsed->buildFingerprint()->str(), "commit-123");
EXPECT_EQ(parsed->token()->str(), "pairing-token");
EXPECT_EQ(parsed->dialMode(), DialMode::Connect);
ASSERT_NE(parsed->linkTerms(), nullptr);
EXPECT_EQ(parsed->linkTerms()->dataPlane(), DataPlane::Stream);
EXPECT_EQ(parsed->linkTerms()->maxReplyBytes(), 1048560u);
EXPECT_EQ(parsed->linkTerms()->cmdWindowBytes(), 8388608u);
EXPECT_EQ(parsed->linkTerms()->stageWindowBytes(), 33554432u);
EXPECT_EQ(parsed->linkTerms()->eventWindowBytes(), 262144u);
}
+2 -1
View File
@@ -153,7 +153,8 @@ namespace {
auto stamp = builder.CreateString(GIT_COMMIT_HASH_SHORT);
auto hello = ::MobileGL::Wire::CreateHello(
builder, MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR, stamp,
/*backendType=*/0u, /*pid=*/0u, /*configBlob=*/0, CapsAbiFingerprint());
/*backendType=*/0u, /*pid=*/0u, /*configBlob=*/0, CapsAbiFingerprint(),
CapsAbiFingerprint(), ::MobileGL::Wire::CreateLinkTerms(builder));
auto root = ::MobileGL::Wire::CreateCtrlEnvelope(
builder, ::MobileGL::Wire::CtrlMsg::Hello, hello.Union());
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, root);
+136 -86
View File
@@ -39,6 +39,8 @@
#include "Includes.h"
#include <MG_Remote/CapsCodec.h>
#include <MG_Remote/Handshake.h>
#include <MG_Pipe/PipeWireLayout.h>
#include <MG_Remote/Client/ClientSession.h>
#include <MG_Remote/Protocol/generated/protocol_generated.h>
#include <MG_Remote/Server/ServerSession.h>
@@ -114,100 +116,148 @@ namespace {
// below. Also red, separately, by deleting any one `mix(...)` line from MixAbiFingerprint: the
// matching EXPECT_NE names the input that stopped being mixed. Both perturbations were run.
TEST(SessionHandshakeTest, TheAbiFingerprintChangesWhenAnyOfItsInputsDoes) {
const Uint64 production = CapsAbiFingerprint();
EXPECT_NE(production, 0u) << "0 is reserved for \"not stated\"";
EXPECT_EQ(production, CapsAbiFingerprint()) << "not stable within one build";
const Transport::AbiFingerprintInputs inputs = CapsAbiFingerprintInputs();
EXPECT_EQ(production, Transport::MixAbiFingerprint(inputs))
<< "CapsAbiFingerprint() is not MixAbiFingerprint over CapsAbiFingerprintInputs(): the "
"handshake compares a value this case cannot reach, which is finding 6 again";
// The inputs are the real ones, so a CapsAbiFingerprintInputs() that hard-coded a size
// would be caught here rather than agreed with by a peer built from a different tree.
const Uint64 production = WireFingerprint();
const auto inputs = CapsAbiFingerprintInputs();
EXPECT_NE(production, 0u);
EXPECT_EQ(production, CapsAbiFingerprint());
EXPECT_EQ(production, Transport::MixAbiFingerprint(inputs));
EXPECT_EQ(inputs.DynamicParamsSize, sizeof(MG_Backend::DynamicBackendParameters));
EXPECT_EQ(inputs.CapsSize, sizeof(MG_Pipe::MGPCaps));
EXPECT_EQ(inputs.FunctionTableSize, sizeof(MG_Backend::GLFunctionsTable));
EXPECT_EQ(inputs.FormatCapabilityTargets,
static_cast<Uint64>(MG_Backend::kFormatCapabilityTargetCount));
EXPECT_EQ(inputs.FormatCapabilityFormats,
static_cast<Uint64>(MG_Backend::kFormatCapabilityFormatCount));
EXPECT_NE(inputs.FormatCapabilitiesCodecVersion, 0u);
EXPECT_NE(inputs.RendererInfoCodecVersion, 0u);
EXPECT_EQ(inputs.MemberLayout, MG_Pipe::kMGPipeWireMemberLayoutDigest);
EXPECT_EQ(inputs.CatalogueLayout, MG_Pipe::kMGPipeWireCatalogueDigest);
EXPECT_EQ(inputs.RenderStateLayout, MG_Pipe::WireRenderStateDigest());
EXPECT_EQ(inputs.OpCount, static_cast<Uint64>(MG_Pipe::MGPWireOp::kOpCount));
EXPECT_EQ(inputs.AbiVersion, static_cast<Uint32>(MOBILEGL_ABI_VERSION(
MOBILEGL_PROTOCOL_ABI_MAJOR, MOBILEGL_PROTOCOL_ABI_MINOR)));
// CONTRACT-P6 4.2. THE ASSERTION SPLITS WITH THE MACRO PAIR, and it has to: this used to
// demand a non-empty stamp unconditionally and was RED in every build whose git could not be
// read - a real signal, but one that told the reader nothing about WHICH of the two states
// the build was in. Now the build system states it, and both states are asserted.
ASSERT_NE(inputs.BuildStamp, nullptr);
EXPECT_EQ(inputs.BuildStampPresent, static_cast<Uint32>(MOBILEGL_BUILD_STAMP_PRESENT))
<< "the fingerprint disagrees with the build system about whether this build has a stamp";
#if MGL_HANDSHAKE_TEST_HAS_GIT_HASH
EXPECT_STREQ(inputs.BuildStamp, MOBILEGL_BUILD_STAMP_VALUE);
#endif
#if MOBILEGL_BUILD_STAMP_PRESENT
EXPECT_NE(inputs.BuildStamp[0], '\0')
<< "the build claims a stamp and the fingerprint mixed an empty one";
#else
// A stampless build is a KNOWN, DECLARED state - CMake warns at configure time - but it must
// not be a silent one here either. What still has to hold is that the ABSENCE is mixed, which
// the BuildStampPresent perturbation below proves.
EXPECT_STREQ(inputs.BuildStamp, "")
<< "PRESENT is 0 but a value came through; the two halves of the pair disagree";
#endif
// Every input moves the answer. Each lambda changes exactly one field of a copy of the
// REAL inputs, so what is proven is that the production value depends on that field.
const auto perturbed = [&](auto&& mutate) {
Transport::AbiFingerprintInputs copy = inputs;
EXPECT_EQ(inputs.PointerBits, sizeof(void*) * 8);
const auto perturbed = [&](auto mutate) {
auto copy = inputs;
mutate(copy);
return Transport::MixAbiFingerprint(copy);
};
EXPECT_NE(production, perturbed([](Transport::AbiFingerprintInputs& i) { ++i.DynamicParamsSize; }))
<< "sizeof(DynamicBackendParameters) is not mixed";
EXPECT_NE(production, perturbed([](Transport::AbiFingerprintInputs& i) { ++i.CapsSize; }))
<< "sizeof(MGPCaps) is not mixed";
EXPECT_NE(production, perturbed([](Transport::AbiFingerprintInputs& i) { ++i.FunctionTableSize; }))
<< "sizeof(GLFunctionsTable) is not mixed";
EXPECT_NE(production,
perturbed([](Transport::AbiFingerprintInputs& i) { ++i.FormatCapabilityTargets; }))
<< "kFormatCapabilityTargetCount is not mixed";
EXPECT_NE(production,
perturbed([](Transport::AbiFingerprintInputs& i) { ++i.FormatCapabilityFormats; }))
<< "kFormatCapabilityFormatCount is not mixed";
EXPECT_NE(production, perturbed([](Transport::AbiFingerprintInputs& i) {
++i.FormatCapabilitiesCodecVersion;
}))
<< "kFormatCapabilitiesCodecVersion is not mixed";
EXPECT_NE(production,
perturbed([](Transport::AbiFingerprintInputs& i) { ++i.RendererInfoCodecVersion; }))
<< "kRendererInfoCodecVersion is not mixed";
EXPECT_NE(production, perturbed([](Transport::AbiFingerprintInputs& i) { ++i.OpCount; }))
<< "MGPWireOp::kOpCount is not mixed (ID-33)";
EXPECT_NE(production, perturbed([](Transport::AbiFingerprintInputs& i) { ++i.AbiVersion; }))
<< "the protocol ABI version is not mixed (ID-33)";
EXPECT_NE(production,
perturbed([](Transport::AbiFingerprintInputs& i) { i.BuildStamp = "not-this-build"; }))
<< "the git stamp is not mixed";
// A missing stamp is not the same as an empty one, and neither is the same as a real build.
// THE PRESENCE FLAG IS ITSELF AN INPUT (4.2), and this is the assertion that makes a
// stampless build safe to reason about: a peer that says "I could not name my commit" must
// not produce the same fingerprint as one whose commit genuinely is "". It has to move the
// answer in EITHER build state, which is why it sits outside the #if below.
EXPECT_NE(production, perturbed([](Transport::AbiFingerprintInputs& i) { ++i.BuildStampPresent; }))
<< "BuildStampPresent is not mixed; 'no stamp' and 'empty stamp' are one input again";
EXPECT_NE(production, perturbed([](auto& i) { ++i.DynamicParamsSize; })) << "DynamicParamsSize";
EXPECT_NE(production, perturbed([](auto& i) { ++i.CapsSize; })) << "CapsSize";
EXPECT_NE(production, perturbed([](auto& i) { ++i.MemberLayout; })) << "MemberLayout";
EXPECT_NE(production, perturbed([](auto& i) { ++i.CatalogueLayout; })) << "CatalogueLayout";
EXPECT_NE(production, perturbed([](auto& i) { ++i.RenderStateLayout; })) << "RenderStateLayout";
EXPECT_NE(production, perturbed([](auto& i) { ++i.FormatCapabilityTargets; })) << "FormatCapabilityTargets";
EXPECT_NE(production, perturbed([](auto& i) { ++i.FormatCapabilityFormats; })) << "FormatCapabilityFormats";
EXPECT_NE(production, perturbed([](auto& i) { ++i.FormatCapabilitiesCodecVersion; })) << "FormatCapabilitiesCodecVersion";
EXPECT_NE(production, perturbed([](auto& i) { ++i.RendererInfoCodecVersion; })) << "RendererInfoCodecVersion";
EXPECT_NE(production, perturbed([](auto& i) { ++i.OpCount; })) << "OpCount";
EXPECT_NE(production, perturbed([](auto& i) { ++i.AbiVersion; })) << "AbiVersion";
EXPECT_NE(production, perturbed([](auto& i) { ++i.PointerBits; })) << "PointerBits";
EXPECT_NE(production, perturbed([](auto& i) { ++i.LittleEndian; })) << "LittleEndian";
}
EXPECT_NE(production, perturbed([](Transport::AbiFingerprintInputs& i) { i.BuildStamp = nullptr; }));
#if MOBILEGL_BUILD_STAMP_PRESENT
// Vacuous without a stamp: production's value already IS "", so this perturbation would be
// the identity and the assertion would pass while proving nothing.
EXPECT_NE(production, perturbed([](Transport::AbiFingerprintInputs& i) { i.BuildStamp = ""; }));
TEST(SessionHandshakeTest, SameWidthFieldReorderingChangesTheWireDigestWithoutABuildStamp) {
using namespace MG_Pipe;
std::vector<WireLayoutMember> fields(std::begin(kMGPipeWireLayoutMembers),
std::end(kMGPipeWireLayoutMembers));
// MGPRange.Offset and Size have the same width: sizeof-only checks cannot see this.
SizeT offsetIndex = 0, sizeIndex = 0;
for (SizeT i = 0; i < fields.size(); ++i) {
if (std::strcmp(fields[i].Name, "MGPRange.Offset") == 0) offsetIndex = i;
if (std::strcmp(fields[i].Name, "MGPRange.Size") == 0) sizeIndex = i;
}
ASSERT_NE(offsetIndex, sizeIndex);
ASSERT_EQ(fields[offsetIndex].Size, fields[sizeIndex].Size);
std::swap(fields[offsetIndex].Offset, fields[sizeIndex].Offset);
EXPECT_NE(WireMemberLayoutDigest(fields.data(), fields.size()), kMGPipeWireMemberLayoutDigest);
const auto before = WireFingerprint();
EXPECT_STREQ(BuildFingerprint(), MOBILEGL_BUILD_STAMP_VALUE);
EXPECT_EQ(BuildFingerprintPresent(), MOBILEGL_BUILD_STAMP_PRESENT != 0);
EXPECT_EQ(before, WireFingerprint());
}
namespace {
class ScopedHandshakeEnvironment {
public:
ScopedHandshakeEnvironment(const char* name, const char* value) : m_name(name) {
const char* previous = std::getenv(name);
m_present = previous != nullptr;
if (m_present) m_previous = previous;
Set(value);
}
~ScopedHandshakeEnvironment() { Set(m_present ? m_previous.c_str() : nullptr); }
private:
void Set(const char* value) {
#if defined(_WIN32)
::_putenv_s(m_name, value == nullptr ? "" : value);
#else
if (value == nullptr) ::unsetenv(m_name); else ::setenv(m_name, value, 1);
#endif
EXPECT_NE(perturbed([](Transport::AbiFingerprintInputs& i) { i.BuildStamp = nullptr; }),
perturbed([](Transport::AbiFingerprintInputs& i) { i.BuildStamp = ""; }))
<< "\"no stamp\" and \"an empty stamp\" collapsed into one input";
}
const char* m_name;
bool m_present = false;
std::string m_previous;
};
std::vector<Uint8> ReadHandshakeFrame(Transport::ITransport& transport) {
Uint64 bytes = 0;
if (transport.ReceiveFrame({nullptr, 0}, &bytes, 1000) != MOBILEGL_ERR_BUFFER_TOO_SMALL) return {};
std::vector<Uint8> frame(bytes);
if (transport.ReceiveFrame({frame.data(), frame.size()}, &bytes, 1000) != MOBILEGL_OK) return {};
return frame;
}
void CheckHandshake(Uint32 major, Uint64 wire, const char* build,
::MobileGL::Wire::DialMode dial, ::MobileGL::Wire::RefuseCode expected) {
using namespace ::MobileGL::Wire;
std::unique_ptr<Transport::InProcessTransport> client, server;
Transport::InProcessTransport::CreatePair(client, server);
::flatbuffers::FlatBufferBuilder builder(512);
auto terms = CreateLinkTerms(builder);
auto hello = CreateHelloDirect(builder, major, MOBILEGL_PROTOCOL_ABI_MINOR, build,
0, 1, nullptr, wire, wire, terms, nullptr, dial);
auto root = CreateCtrlEnvelope(builder, CtrlMsg::Hello, hello.Union());
FinishCtrlEnvelopeBuffer(builder, root);
std::vector<Uint8> first(builder.GetBufferPointer(), builder.GetBufferPointer() + builder.GetSize());
Server::ServerSession session;
Transport::SessionSegmentSizes sizes;
sizes.CmdRingBytes = 4096; sizes.StageBytes = 4096;
sizes.ReplyBytes = 4096; sizes.EventRingBytes = 4096;
session.SetSegmentSizes(sizes);
const auto result = session.Accept(*server, &first);
EXPECT_EQ(result, expected == RefuseCode::None ? MOBILEGL_OK : MOBILEGL_ERR_PROTOCOL_MISMATCH);
const auto reply = ReadHandshakeFrame(*client);
ASSERT_FALSE(reply.empty());
::flatbuffers::Verifier verifier(reply.data(), reply.size());
ASSERT_TRUE(VerifyCtrlEnvelopeBuffer(verifier));
const auto* envelope = GetCtrlEnvelope(reply.data());
if (expected == RefuseCode::None) {
ASSERT_NE(envelope->msg_as_Welcome(), nullptr);
EXPECT_EQ(envelope->msg_as_Welcome()->wireFingerprint(), WireFingerprint());
ASSERT_NE(envelope->msg_as_Welcome()->linkTerms(), nullptr);
EXPECT_EQ(envelope->msg_as_Welcome()->linkTerms()->cmdWindowBytes(), 4096u);
} else {
ASSERT_NE(envelope->msg_as_Refuse(), nullptr);
EXPECT_EQ(envelope->msg_as_Refuse()->code(), expected);
EXPECT_FALSE(session.Accepted());
}
session.Close();
}
}
TEST(SessionHandshakeTest, WireMajorMismatchReturnsNamedRefuseWithoutAborting) {
CheckHandshake(99, WireFingerprint(), BuildFingerprint(), ::MobileGL::Wire::DialMode::No,
::MobileGL::Wire::RefuseCode::ProtocolVersion);
}
TEST(SessionHandshakeTest, WireLayoutMismatchReturnsNamedRefuseWithoutAborting) {
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint() ^ 1, BuildFingerprint(),
::MobileGL::Wire::DialMode::No, ::MobileGL::Wire::RefuseCode::WireFingerprint);
}
TEST(SessionHandshakeTest, ConnectAcceptsDifferentBuildWithIdenticalWire) {
ScopedHandshakeEnvironment required("MOBILEGL_IPC_REQUIRE_SAME_BUILD", "0");
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint(), "different-commit",
::MobileGL::Wire::DialMode::Connect, ::MobileGL::Wire::RefuseCode::None);
}
TEST(SessionHandshakeTest, ForkRefusesDifferentBuildWithIdenticalWire) {
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint(), "different-commit",
::MobileGL::Wire::DialMode::Fork, ::MobileGL::Wire::RefuseCode::BuildFingerprint);
}
TEST(SessionHandshakeTest, ConnectCanRequireTheSameBuild) {
ScopedHandshakeEnvironment required("MOBILEGL_IPC_REQUIRE_SAME_BUILD", "1");
CheckHandshake(MOBILEGL_PROTOCOL_ABI_MAJOR, WireFingerprint(), "different-commit",
::MobileGL::Wire::DialMode::Connect, ::MobileGL::Wire::RefuseCode::BuildFingerprint);
}
// ---------------------------------------------------------------------------
@@ -51,6 +51,8 @@
#if !defined(_WIN32)
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <unistd.h>
#endif
@@ -58,6 +60,68 @@ using namespace MobileGL::MG_Remote::Transport;
#if !defined(_WIN32)
TEST(SocketTransportTest, TcpCarriesControlAndIndependentDataWithKeepalive) {
// Find an available loopback port; Listen deliberately refuses ambiguous port 0 URIs.
int reservation = ::socket(AF_INET, SOCK_STREAM, 0);
ASSERT_GE(reservation, 0);
sockaddr_in address{};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
ASSERT_EQ(::bind(reservation, reinterpret_cast<sockaddr*>(&address), sizeof(address)), 0);
socklen_t addressSize = sizeof(address);
ASSERT_EQ(::getsockname(reservation, reinterpret_cast<sockaddr*>(&address), &addressSize), 0);
const auto endpoint = std::string("tcp://127.0.0.1:") + std::to_string(ntohs(address.sin_port));
::close(reservation);
int listener = -1;
ASSERT_EQ(SocketTransport::Listen(endpoint, &listener), MOBILEGL_OK);
std::unique_ptr<SocketTransport> client, server;
ASSERT_EQ(SocketTransport::ConnectTo(endpoint, 1000, client), MOBILEGL_OK);
ASSERT_EQ(SocketTransport::AcceptPair(listener, 1000, server), MOBILEGL_OK);
::close(listener);
ASSERT_TRUE(client->IsTcp());
ASSERT_TRUE(server->IsTcp());
for (auto* transport : {client.get(), server.get()}) {
int option = 0;
socklen_t size = sizeof(option);
ASSERT_EQ(::getsockopt(transport->StreamFd(), IPPROTO_TCP, TCP_NODELAY, &option, &size), 0);
EXPECT_EQ(option, 1);
ASSERT_EQ(::getsockopt(transport->StreamFd(), SOL_SOCKET, SO_KEEPALIVE, &option, &size), 0);
EXPECT_EQ(option, 1);
EXPECT_EQ(transport->ShareFd(0, {nullptr, 0}), MOBILEGL_ERR_UNSUPPORTED);
}
const int clientData = client->TakeDataFd(), serverData = server->TakeDataFd();
ASSERT_GE(clientData, 0);
ASSERT_GE(serverData, 0);
EXPECT_EQ(client->TakeDataFd(), -1);
const char message[] = "independent";
ASSERT_EQ(::send(clientData, message, sizeof(message), MSG_NOSIGNAL), sizeof(message));
char received[sizeof(message)]{};
ASSERT_EQ(::recv(serverData, received, sizeof(received), MSG_WAITALL), sizeof(received));
EXPECT_EQ(std::memcmp(message, received, sizeof(message)), 0);
ASSERT_EQ(server->SendFrame({message, sizeof(message)}), MOBILEGL_OK);
std::uint64_t size = 0;
ASSERT_EQ(client->ReceiveFrame({received, sizeof(received)}, &size, 1000), MOBILEGL_OK);
EXPECT_EQ(size, sizeof(message));
::close(clientData);
::close(serverData);
}
TEST(SocketTransportTest, TcpRejectsMalformedNamesAndUnauthenticatedWildcard) {
int listener = -1;
for (const char* endpoint : {"tcp://", "tcp://localhost", "tcp://localhost:0", "tcp://localhost:65536",
"tcp://localhost:-1", "tcp://[::1]broken", "tcp://::1:40000"}) {
EXPECT_EQ(SocketTransport::Listen(endpoint, &listener), MOBILEGL_ERR_INVALID_ARGUMENT) << endpoint;
EXPECT_EQ(listener, -1);
}
const char* prior = std::getenv("MOBILEGL_IPC_TOKEN");
const std::string saved = prior ? prior : "";
const bool wasSet = prior != nullptr;
::unsetenv("MOBILEGL_IPC_TOKEN");
EXPECT_EQ(SocketTransport::Listen("tcp://0.0.0.0:40613", &listener), MOBILEGL_ERR_PROTOCOL_MISMATCH);
EXPECT_EQ(listener, -1);
if (wasSet) ::setenv("MOBILEGL_IPC_TOKEN", saved.c_str(), 1);
}
namespace {
MobileGLByteSpan Span(const std::string& text) {
+207
View File
@@ -0,0 +1,207 @@
// SPDX-License-Identifier: LGPL-3.0-only
#include <MG_Remote/Transport/StreamLink.h>
#include <MG_Remote/Transport/SessionRings.h>
#include <gtest/gtest.h>
#include <array>
#include <chrono>
#include <cstring>
#include <thread>
#include <sys/socket.h>
#include <unistd.h>
using namespace MobileGL::MG_Remote::Transport;
namespace {
class StreamPair : public testing::Test {
protected:
SessionSegments clientMemory, serverMemory;
StreamLink client, server;
RingProducer commands;
RingConsumer consumer;
SessionProducer publish;
SessionConsumer apply;
void SetUp() override {
SessionSegmentSizes sizes;
sizes.CmdRingBytes = 1024;
sizes.StageBytes = 4096;
sizes.ReplyBytes = 4096;
sizes.EventRingBytes = 1024;
ASSERT_EQ(clientMemory.CreatePrivate(sizes, MemoryRole::Client), MOBILEGL_OK);
ASSERT_EQ(serverMemory.CreatePrivate(sizes, MemoryRole::Server), MOBILEGL_OK);
int sockets[2];
ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM, 0, sockets), 0);
ASSERT_EQ(client.Attach(sockets[0], clientMemory, TransportRoleTag::ClientProducer), MOBILEGL_OK);
ASSERT_EQ(server.Attach(sockets[1], serverMemory, TransportRoleTag::ServerConsumer), MOBILEGL_OK);
commands = RingProducer(clientMemory.CmdControl(), clientMemory.CmdRingBase(), 1024, RingCursorSet::Cmd);
consumer = RingConsumer(serverMemory.CmdControl(), serverMemory.CmdRingBase(), 1024, RingCursorSet::Cmd);
publish.Attach(clientMemory.CmdControl(), &commands, &client.ConsumerBell(), &client.ProducerBell(), 0);
publish.SetLink(&client);
apply.Attach(serverMemory.CmdControl(), &consumer, &server.ProducerBell(), &server.ConsumerBell(), 0);
apply.SetLink(&server);
}
void TearDown() override {
client.Detach();
server.Detach();
}
void Queue(std::uint64_t seq, std::uint64_t value) {
auto* p = commands.Reserve(1, 0, sizeof value);
ASSERT_NE(p, nullptr);
std::memcpy(p, &value, sizeof value);
publish.PublishAndNotify(seq);
}
template <class F>
bool Eventually(F ready) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
while (!ready() && std::chrono::steady_clock::now() < deadline)
std::this_thread::yield();
return ready();
}
};
TEST_F(StreamPair, StagingArrivesBeforeItsCommandAndLastRecordProgressFlushesOnIdle) {
const std::array<unsigned char, 8> bytes{1, 2, 3, 4, 5, 6, 7, 8};
std::memcpy(static_cast<unsigned char*>(clientMemory.StageBase()) + 24, bytes.data(), bytes.size());
client.NoteStage({24, bytes.size()});
Queue(1, 24);
ASSERT_EQ(client.Flush(), MOBILEGL_OK);
ASSERT_EQ(apply.WaitForWork(2000), SessionWait::Reached);
ASSERT_TRUE(apply.ApplyOne([&](const RingRecordView& v) {
std::uint64_t offset;
std::memcpy(&offset, v.payload, sizeof offset);
const void* p = nullptr;
ASSERT_EQ(server.ResolveSpan(LinkSegment::Stage, {offset, bytes.size()}, &p), MOBILEGL_OK);
EXPECT_EQ(std::memcmp(p, bytes.data(), bytes.size()), 0);
}));
apply.RetireThrough(1);
// This is the production idle discipline, not a polling sender thread.
EXPECT_EQ(apply.WaitForWork(1), SessionWait::TimedOut);
EXPECT_EQ(publish.WaitForApplied(1, 2000), SessionWait::Reached);
EXPECT_EQ(client.Progress()->retiredSeq.load(), 1u);
}
TEST_F(StreamPair, RetiredByteCursorCrossesManyWrapsWithoutStageRingCursors) {
for (std::uint64_t seq = 1; seq <= 160; ++seq) {
Queue(seq, seq);
ASSERT_EQ(client.Flush(), MOBILEGL_OK);
ASSERT_EQ(apply.WaitForWork(2000), SessionWait::Reached);
ASSERT_TRUE(apply.ApplyOne([&](const RingRecordView& v) {
std::uint64_t value;
std::memcpy(&value, v.payload, sizeof value);
EXPECT_EQ(value, seq);
}));
apply.RetireThrough(seq);
ASSERT_EQ(server.FlushProgress(), MOBILEGL_OK);
ASSERT_EQ(publish.WaitForApplied(seq, 2000), SessionWait::Reached);
}
EXPECT_GT(commands.LocalHead(), 1024u);
EXPECT_EQ(commands.FreeBytes(), 1024u);
EXPECT_EQ(clientMemory.CmdControl()->stageHead.load(), 0u);
EXPECT_EQ(clientMemory.CmdControl()->stageRetiredTail.load(), 0u);
}
TEST_F(StreamPair, UnwantedReplyDoesNotBlockLaterReplyAndAppliedPublication) {
Queue(1, 1);
Queue(2, 2);
ASSERT_EQ(client.Flush(), MOBILEGL_OK);
ASSERT_EQ(apply.WaitForWork(2000), SessionWait::Reached);
ASSERT_TRUE(apply.ApplyOne([&](const RingRecordView&) {
const std::uint64_t value = 11;
EXPECT_EQ(server.PostReply(1, 0, &value, sizeof value), MOBILEGL_OK);
}));
ASSERT_TRUE(apply.ApplyOne([&](const RingRecordView&) {
const std::uint64_t value = 22;
EXPECT_EQ(server.PostReply(2, 0, &value, sizeof value), MOBILEGL_OK);
}));
apply.RetireThrough(2);
ASSERT_EQ(server.FlushProgress(), MOBILEGL_OK);
ASSERT_EQ(publish.WaitForApplied(2, 2000), SessionWait::Reached);
std::int32_t status;
const void* p = nullptr;
std::uint64_t size = 0;
ASSERT_EQ(client.ReadReply(2, &status, &p, &size), MOBILEGL_OK);
ASSERT_EQ(size, sizeof(std::uint64_t));
std::uint64_t value;
std::memcpy(&value, p, sizeof value);
EXPECT_EQ(value, 22u);
}
TEST_F(StreamPair, EventDrainReturnsCreditAndClearsRemoteFullLatch) {
EventRingProducer events(serverMemory.EventControl(), serverMemory.CmdControl(), serverMemory.EventRingBase(),
1024);
EventRingConsumer read(clientMemory.EventControl(), clientMemory.CmdControl(), clientMemory.EventRingBase(),
1024, clientMemory.EventSegmentBase());
read.SetLink(&client);
auto* p = events.Reserve(kEventGlError, 64);
ASSERT_NE(p, nullptr);
std::memset(p, 0, 64);
events.Ring().Publish();
serverMemory.CmdControl()->eventRingFull.store(1);
ASSERT_EQ(server.FlushProgress(), MOBILEGL_OK);
ASSERT_TRUE(Eventually([&] { return clientMemory.CmdControl()->eventRingFull.load() != 0; }));
RingRecordView record;
ASSERT_TRUE(read.Pop(record));
read.Drained();
ASSERT_TRUE(Eventually([&] { return serverMemory.CmdControl()->eventRingFull.load() == 0; }));
EXPECT_EQ(events.Ring().FreeBytes(), 1024u);
}
TEST_F(StreamPair, PeerEofKillsBothLocalBellsAndWakesAWaiter) {
server.Detach();
ASSERT_TRUE(Eventually([&] { return client.PeerHungUp(); }));
EXPECT_TRUE(client.ProducerBell().Dead());
EXPECT_TRUE(client.ConsumerBell().Dead());
EXPECT_EQ(publish.WaitForApplied(10, 2000), SessionWait::ShutDown);
}
void WriteFrame(int fd, unsigned kind, std::uint64_t a, const void* payload, std::uint32_t size) {
unsigned char h[28]{};
auto put = [&](unsigned at, std::uint64_t value, unsigned width) {
for (unsigned i = 0; i < width; ++i)
h[at + i] = value >> (8 * i);
};
put(0, 0x444c474d, 4);
put(4, size, 4);
h[8] = kind;
put(12, a, 8);
ASSERT_EQ(::send(fd, h, sizeof h, MSG_NOSIGNAL), sizeof h);
if (size) ASSERT_EQ(::send(fd, payload, size, MSG_NOSIGNAL), size);
}
TEST(StreamLinkDeath, StageOutsideNegotiatedWindowIsNamedProtocolCorruption) {
EXPECT_DEATH(
{
SessionSegments memory;
SessionSegmentSizes sizes;
sizes.StageBytes = 64;
memory.CreatePrivate(sizes, MemoryRole::Server);
int sockets[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sockets);
StreamLink link;
link.Attach(sockets[0], memory, TransportRoleTag::ServerConsumer);
unsigned char byte = 1;
WriteFrame(sockets[1], 2, 64, &byte, 1);
std::this_thread::sleep_for(std::chrono::seconds(1));
},
"ProtocolCorruption.*window");
}
TEST(StreamLinkDeath, RegressingProgressIsNamedProtocolCorruption) {
EXPECT_DEATH(
{
SessionSegments memory;
SessionSegmentSizes sizes;
memory.CreatePrivate(sizes, MemoryRole::Client);
memory.CmdControl()->submittedSeq.store(3);
int sockets[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sockets);
StreamLink link;
link.Attach(sockets[0], memory, TransportRoleTag::ClientProducer);
unsigned char progress[64]{};
progress[0] = 2;
WriteFrame(sockets[1], 4, 0, progress, 64);
progress[0] = 1;
WriteFrame(sockets[1], 4, 0, progress, 64);
std::this_thread::sleep_for(std::chrono::seconds(1));
},
"ProtocolCorruption.*backwards");
}
} // namespace
@@ -279,6 +279,7 @@ TEST(SurfaceControlFrameTest, TheReplyHalfRoundTripsAsASurfaceReply) {
frame.ok = true;
frame.eglMajor = 1;
frame.eglMinor = 5;
frame.eventHead = 0x123456789ull;
flatbuffers::FlatBufferBuilder builder(256);
EncodeSurfaceReplyFrame(frame, &builder);
@@ -294,6 +295,7 @@ TEST(SurfaceControlFrameTest, TheReplyHalfRoundTripsAsASurfaceReply) {
EXPECT_TRUE(back.ok);
EXPECT_EQ(back.eglMajor, 1);
EXPECT_EQ(back.eglMinor, 5);
EXPECT_EQ(back.eventHead, frame.eventHead) << "control replies must fence data events";
}
TEST(SurfaceControlFrameTest, MalformedAndInprocOnlyFramesRefuseToEncode) {
+29
View File
@@ -12,12 +12,38 @@
namespace MobileGL {
namespace MG_Util::Debug {
std::mutex& LogMutex();
static FILE* s_logFile = nullptr;
#if MOBILEGL_BUILD_DISAGGREGATED
// The server role's own sink. Opened lazily, on the first line a server-role thread
// writes, so a process that never plays the role never creates the file.
static FILE* s_serverLogFile = nullptr;
static LogForwarder s_forwarder = nullptr;
static void* s_forwarderUser = nullptr;
void SetLogForwarder(LogForwarder forwarder, void* user) {
std::lock_guard<std::mutex> lock(LogMutex());
s_forwarder = forwarder;
s_forwarderUser = user;
}
void WithLogBarrier(void (*action)(void*), void* user) {
std::lock_guard<std::mutex> lock(LogMutex());
action(user);
}
void WritePeerLog(const char* message) {
std::lock_guard<std::mutex> lock(LogMutex());
const char* path = std::getenv("MOBILEGL_LOG_FILE_PATH");
if (!path || !*path) path = MOBILEGL_LOG_FILE_PATH;
if (!path || !*path) return;
if (!s_serverLogFile) s_serverLogFile = std::fopen(RoleLogPath(path, LogRole::Server).c_str(), "w");
if (s_serverLogFile) {
std::fputs(message, s_serverLogFile);
std::fflush(s_serverLogFile);
}
}
// THE PROCESS DEFAULT, read once. A spawned server image is the server on every thread -
// there is no other role in it - and MOBILEGL_IPC_ROLE is set by the launcher before
@@ -256,6 +282,9 @@ namespace MobileGL {
#endif
WriteToFile(out.c_str());
#if MOBILEGL_BUILD_DISAGGREGATED
if (s_forwarder && ThreadIsServerRole()) s_forwarder(s_forwarderUser, out.c_str());
#endif
va_end(args);
}
+4
View File
@@ -136,6 +136,10 @@ namespace MobileGL {
void Close();
#if MOBILEGL_BUILD_DISAGGREGATED
using LogForwarder = void (*)(void*, const char*);
void SetLogForwarder(LogForwarder forwarder, void* user);
void WritePeerLog(const char* message);
void WithLogBarrier(void (*action)(void*), void* user);
// P6: ONE LOG PER ROLE, and the role is a per-THREAD fact because under inproc both
// roles live in one process. The client keeps MOBILEGL_LOG_FILE_PATH unchanged so
// every existing reader keeps its path; the server's lines go to the same path with
@@ -2,7 +2,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application>
<service
android:name=".MobileGLServerService"
android:exported="true"
android:foregroundServiceType="dataSync"
android:process=":mglsrv" />
<activity
android:name=".PluginActivity"
tools:node="remove" />
@@ -0,0 +1,100 @@
package top.mobilegl.plugin;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.PowerManager;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.Map;
/** Trace-only entry point for a TCP supervisor; EGL belongs to its session children. */
public final class MobileGLServerService extends Service {
private static final String TAG = "MobileGLServer";
private static final String CHANNEL = "mobilegl-server";
private volatile Process supervisor;
private PowerManager.WakeLock wakeLock;
@Override public IBinder onBind(Intent intent) { return null; }
@Override public void onCreate() {
super.onCreate();
NotificationManager notifications = getSystemService(NotificationManager.class);
notifications.createNotificationChannel(new NotificationChannel(
CHANNEL, "MobileGL TCP server", NotificationManager.IMPORTANCE_LOW));
startForeground(40613, new Notification.Builder(this, CHANNEL)
.setSmallIcon(android.R.drawable.stat_notify_sync)
.setContentTitle("MobileGL TCP server")
.setContentText("Serving remote rendering sessions")
.setOngoing(true).build());
wakeLock = getSystemService(PowerManager.class).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "MobileGL:TCPServer");
wakeLock.acquire();
}
// Keep the replay runner's KEY=VALUE;KEY grammar, including unset and empty values.
static void applyEnvironment(Map<String, String> env, String overrides) {
if (overrides == null) return;
for (String entry : overrides.split(";", -1)) {
int separator = entry.indexOf('=');
if (entry.isEmpty() || separator == 0) continue;
if (separator < 0) env.remove(entry);
else env.put(entry.substring(0, separator), entry.substring(separator + 1));
}
}
@Override public synchronized int onStartCommand(Intent intent, int flags, int startId) {
if (supervisor != null && supervisor.isAlive()) {
Log.w(TAG, "supervisor already running; stop the service before reconfiguring");
return START_NOT_STICKY;
}
String endpoint = intent == null ? null : intent.getStringExtra("listen");
if (endpoint == null) endpoint = "tcp://127.0.0.1:40613";
try {
File executable = new File(getApplicationInfo().nativeLibraryDir, "libMobileGLServer.so");
ProcessBuilder builder = new ProcessBuilder(executable.getAbsolutePath(), endpoint, "--serve");
builder.directory(getFilesDir()).redirectErrorStream(true);
Map<String, String> env = builder.environment();
applyEnvironment(env, intent == null ? null : intent.getStringExtra("env"));
env.put("MOBILEGL_IPC_ROLE", "server");
env.put("MOBILEGL_IPC_DIAL", "no");
env.put("MOBILEGL_IPC_LOG_FORWARD", "1");
env.remove("MOBILEGL_IPC_CONTROL");
env.put("MOBILEGL_IPC_TOKEN", intent == null || intent.getStringExtra("token") == null
? "" : intent.getStringExtra("token"));
env.putIfAbsent("MOBILEGL_LOG_FILE_PATH", new File(getFilesDir(), "mgl.log").getAbsolutePath());
env.put("LD_LIBRARY_PATH", getApplicationInfo().nativeLibraryDir);
supervisor = builder.start();
final Process child = supervisor;
new Thread(() -> {
try (BufferedReader output = new BufferedReader(new InputStreamReader(child.getInputStream()))) {
String line;
while ((line = output.readLine()) != null) Log.i(TAG, line);
Log.i(TAG, "supervisor exited " + child.waitFor());
} catch (Exception error) {
Log.e(TAG, "supervisor output failed", error);
} finally {
stopSelf(startId);
}
}, "mgl-supervisor-log").start();
} catch (Exception error) {
Log.e(TAG, "cannot start TCP supervisor", error);
stopSelf(startId);
}
return START_NOT_STICKY;
}
@Override public void onDestroy() {
Process child = supervisor;
if (child != null) child.destroy();
if (wakeLock != null && wakeLock.isHeld()) wakeLock.release();
stopForeground(STOP_FOREGROUND_REMOVE);
super.onDestroy();
}
}
+45 -8
View File
@@ -17,6 +17,11 @@ skipped are three different answers and the caller needs all three.
"""
import sys
import re
import argparse
import importlib.util
import json
from pathlib import Path
import xml.etree.ElementTree as ET
@@ -25,8 +30,33 @@ import xml.etree.ElementTree as ET
ARM_PREFIXES = {
'--require-split-ran': 'DirectGLES.Split.',
'--require-spawn-ran': 'DirectGLES.Spawn.',
'--require-tcp-ran': 'DirectGLES.Tcp.',
'--require-tcp-device-ran': 'DirectGLES.TcpDevice.',
}
TCP_ARM = re.compile(r'control=tcp data=stream server=\S+ pid=[1-9][0-9]*\b')
def require_tcp_proof(path, prefix, discovery):
helper_path = Path(__file__).resolve().parents[2] / 'MobileGL/MG_IntegrationTest/Harness/split_log_paths.py'
spec = importlib.util.spec_from_file_location('split_log_paths', helper_path)
helper = importlib.util.module_from_spec(spec)
spec.loader.exec_module(helper)
logs = helper.marker_log_paths(json.loads(Path(discovery).read_text()))
for case in ET.parse(path).getroot().iter('testcase'):
if not case.get('name', '').startswith(prefix):
continue
if case.find('skipped') is not None or case.get('status') in ('notrun', 'disabled'):
continue
# The console sink is compiled out of CI builds, so system-out cannot
# establish the transport. Read this entry's private role logs instead.
name = case.get('name')
if name not in logs:
raise ValueError(f'{name}: discovery has no private log path')
output = helper.read_role_logs(logs[name])
if not TCP_ARM.search(output):
raise ValueError(f"{case.get('name')}: missing TCP/stream endpoint and server pid arm proof")
def tally(path, prefix=None):
passed = failed = skipped = 0
@@ -43,18 +73,25 @@ def tally(path, prefix=None):
def main():
if len(sys.argv) not in (2, 3) or (len(sys.argv) == 3 and sys.argv[2] not in ARM_PREFIXES):
print("usage: junit_tally.py <junit.xml> [" + " | ".join(sorted(ARM_PREFIXES)) + "]",
file=sys.stderr)
return 2
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('junit')
arms = parser.add_mutually_exclusive_group()
for flag, prefix in ARM_PREFIXES.items():
arms.add_argument(flag, dest='prefix', action='store_const', const=prefix)
parser.add_argument('--discovery', help='CTest --show-only=json-v1 output (required for TCP proof)')
args = parser.parse_args()
if args.prefix in ('DirectGLES.Tcp.', 'DirectGLES.TcpDevice.') and not args.discovery:
parser.error('TCP arm proof requires --discovery to locate each private role log')
try:
prefix = ARM_PREFIXES[sys.argv[2]] if len(sys.argv) == 3 else None
passed, failed, skipped = tally(sys.argv[1], prefix=prefix)
prefix = args.prefix
passed, failed, skipped = tally(args.junit, prefix=prefix)
if prefix in ('DirectGLES.Tcp.', 'DirectGLES.TcpDevice.'):
require_tcp_proof(args.junit, prefix, args.discovery)
except Exception as exc: # a malformed file is not "zero of everything"
print(f"junit_tally: cannot parse {sys.argv[1]}: {exc}", file=sys.stderr)
print(f"junit_tally: cannot prove {args.junit}: {exc}", file=sys.stderr)
return 1
print(f"{passed} {failed} {skipped}")
if len(sys.argv) == 3 and (passed == 0 or failed):
if prefix is not None and (passed == 0 or failed):
print(f'{prefix} baseline FAILED: no successful entries with that arm prefix, or an '
f'already-red selection', file=sys.stderr)
return 1
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Summarize P65LinkMetrics frame logs without confusing traffic with link throughput.
Example: p65_link_metrics.py --arm spawn:/tmp/spawn.client.log \
--arm tcp-credit1:/tmp/tcp.client.log --warmup-frames 5 --output /tmp/p65-metrics.json
Enable MOBILEGL_PIPE_STATS=1 in each measured arm. RTT percentiles are upper
bounds of fixed power-of-two microsecond buckets, not exact sample quantiles.
The separately measured 64 MiB Stage burst throughput can be supplied with
--link-mib-per-second; ordinary frame traffic is never called link throughput.
"""
from __future__ import annotations
import argparse
import json
import math
import re
from pathlib import Path
FIELDS = re.compile(r"(\w+)=([^\s]+)")
def summarize(path: Path, warmup: int, throughput: float | None = None) -> dict:
frames = []
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
if "P65LinkMetrics " not in line:
continue
record = dict(FIELDS.findall(line.split("P65LinkMetrics ", 1)[1]))
if record.get("kind") != "frame" or int(record["frame"]) <= warmup:
continue
frames.append(record)
if not frames:
raise ValueError(f"{path}: no measured frames after warmup={warmup}")
histogram = [0] * 32
count = samples = stage = wall = cpu = 0
weighted_rtt_us = 0.0
for frame in frames:
buckets = [int(n) for n in frame["rtt_hist_us_pow2"].split(",")]
if len(buckets) != 32 or sum(buckets) != int(frame["rtt_samples"]):
raise ValueError(f"{path}: malformed RTT histogram in frame {frame['frame']}")
histogram = [a + b for a, b in zip(histogram, buckets)]
count += int(frame["wait_replies"])
n = int(frame["rtt_samples"])
samples += n
weighted_rtt_us += float(frame["rtt_mean_us"]) * n
stage += int(frame["stage_bytes"])
wall += int(frame["wall_ns"])
cpu += int(frame["client_thread_cpu_ns"])
def percentile(q: float) -> int:
target = math.ceil(samples * q)
if target == 0:
return 0
seen = 0
for i, bucket in enumerate(histogram):
seen += bucket
if seen >= target:
return 1 << i
raise AssertionError("histogram count is inconsistent")
result = {
"log": str(path), "frames": len(frames), "warmup_frames": warmup,
"wait_replies": count, "wait_replies_per_frame": count / len(frames),
"rtt_samples": samples,
"rtt_mean_us": weighted_rtt_us / samples if samples else None,
"rtt_p50_upper_us": percentile(0.5), "rtt_p99_upper_us": percentile(0.99),
"rtt_histogram_upper_us": [1 << i for i in range(32)],
"rtt_histogram_counts": histogram,
"stage_bytes": stage, "stage_bytes_per_frame": stage / len(frames),
"frame_traffic_mib_per_second": stage * 1e9 / wall / (1024 ** 2) if wall else None,
"fps": len(frames) * 1e9 / wall if wall else None,
"client_thread_cpu_ms_per_frame": cpu / len(frames) / 1e6,
}
if throughput is not None:
traffic = result["frame_traffic_mib_per_second"]
result["measured_link_mib_per_second"] = throughput
result["link_bandwidth_fraction"] = traffic / throughput if traffic is not None else None
return result
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--arm", action="append", required=True, metavar="LABEL:LOG")
parser.add_argument("--warmup-frames", type=int, default=0)
parser.add_argument("--link-mib-per-second", type=float)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
if args.warmup_frames < 0 or (args.link_mib_per_second is not None and args.link_mib_per_second <= 0):
parser.error("warmup must be nonnegative and measured throughput must be positive")
report = {}
for arm in args.arm:
label, sep, filename = arm.partition(":")
if not sep or not label or label in report:
parser.error("each --arm must have a unique LABEL:LOG")
try:
report[label] = summarize(Path(filename), args.warmup_frames, args.link_mib_per_second)
except (OSError, ValueError, KeyError) as error:
parser.error(str(error))
if "spawn" in report:
baseline = report["spawn"]["client_thread_cpu_ms_per_frame"]
for label, arm in report.items():
if label != "spawn":
arm["client_thread_cpu_delta_vs_spawn_ms"] = arm["client_thread_cpu_ms_per_frame"] - baseline
text = json.dumps(report, ensure_ascii=False, indent=2) + "\n"
if args.output:
args.output.write_text(text, encoding="utf-8")
else:
print(text, end="")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+16 -3
View File
@@ -29,14 +29,16 @@ CASE = re.compile(r"DirectGLES\.[A-Za-z0-9]+\.(?:F1\.|Ct\.)?([A-Za-z0-9_]+Scenar
def lane_cases(build_dir, label):
out = subprocess.run(["ctest", "-N", "-L", label], cwd=build_dir,
capture_output=True, text=True).stdout
out = subprocess.run(["ctest", "-N", "-L", "^" + label + "$"], cwd=build_dir,
capture_output=True, text=True, check=True).stdout
return {m.group(1) for m in CASE.finditer(out)}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("build_dir")
parser.add_argument("--require-device", action="store_true",
help="Also require the configured integration-tcp-device lane")
args = parser.parse_args()
split = lane_cases(args.build_dir, "integration-split")
@@ -63,7 +65,18 @@ def main():
print("::error::cases in integration-spawn that integration-split does not run: " +
", ".join(extra) + ". The split arm is the control; a case only the spawn arm runs "
"has no baseline to be compared against.", file=sys.stderr)
return 1 if (missing or extra) else 0
failed = bool(missing or extra)
labels = ["integration-tcp"]
if args.require_device:
labels.append("integration-tcp-device")
for label in labels:
cases = lane_cases(args.build_dir, label)
missing, extra = sorted(spawn - cases), sorted(cases - spawn)
print(f"{label} parity: spawn {len(spawn)}, tcp {len(cases)}")
if not cases or missing or extra:
print(f"::error::{label}: missing={missing}, extra={extra}", file=sys.stderr)
failed = True
return 1 if failed else 0
if __name__ == "__main__":
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""Executable negative controls for TCP lane accounting and supervisor lifecycle."""
import importlib.util
import json
import os
from pathlib import Path
import socket
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[2]
def module(name):
spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(name + '.py'))
loaded = importlib.util.module_from_spec(spec)
spec.loader.exec_module(loaded)
return loaded
class Accounting(unittest.TestCase):
def test_each_tcp_case_needs_a_real_endpoint_and_pid(self):
tally = module('junit_tally')
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
name = 'DirectGLES.Tcp.TriangleScenario.Draw'
junit, discovery, base = root / 'junit.xml', root / 'lane.json', root / 'case.log'
junit.write_text(f'<testsuite><testcase name="{name}" /></testsuite>')
discovery.write_text(json.dumps({'tests': [{'name': name, 'properties': [
{'name': 'ENVIRONMENT', 'value': ['MOBILEGL_LOG_FILE_PATH=' + str(base)]}]}]}))
role = root / 'case.client.log'
role.write_text('control=tcp data=stream server=127.0.0.1:40613 pid=42\n')
tally.require_tcp_proof(junit, 'DirectGLES.Tcp.', discovery)
for bad in ('', 'control=tcp data=shm server=127.0.0.1:40613 pid=42',
'control=tcp data=stream server=127.0.0.1:40613 pid=0',
'spawn ARMED - the server role runs in pid 42'):
role.write_text(bad)
with self.assertRaises(ValueError):
tally.require_tcp_proof(junit, 'DirectGLES.Tcp.', discovery)
def test_equal_counts_do_not_hide_a_missing_case(self):
parity = module('spawn_lane_parity')
a, b = 'TriangleScenario.Draw', 'TriangleScenario.Read'
with patch.object(parity, 'lane_cases', side_effect=[{a}, {a}, {b}]), \
patch.object(sys, 'argv', ['spawn_lane_parity.py', 'unused']):
self.assertEqual(parity.main(), 1)
with patch.object(parity, 'lane_cases', return_value={a}), \
patch.object(sys, 'argv', ['spawn_lane_parity.py', 'unused']):
self.assertEqual(parity.main(), 0)
@unittest.skipUnless(sys.platform.startswith('linux'), 'supervisor fixture uses Linux /proc')
class Supervisor(unittest.TestCase):
def test_readiness_does_not_consume_a_connection_and_stop_owns_only_its_pid(self):
fixture = module('tcp_server_fixture')
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
server = root / 'server'
server.write_text('#!' + sys.executable + '\n'
'import socket,sys\n'
'port=int(sys.argv[1].rsplit(":",1)[1])\n'
's=socket.socket();s.bind(("127.0.0.1",port));s.listen()\n'
'i=0\n'
'while True:\n'
' c,_=s.accept();i+=1;c.sendall(str(i).encode());c.close()\n')
server.chmod(0o755)
with socket.socket() as reservation:
reservation.bind(('127.0.0.1', 0))
port = reservation.getsockname()[1]
state = root / 'state.json'
subprocess.run([sys.executable, str(Path(fixture.__file__)), 'start',
'--server', str(server), '--endpoint', f'tcp://127.0.0.1:{port}',
'--state', str(state)], check=True)
try:
with socket.create_connection(('127.0.0.1', port)) as peer:
self.assertEqual(peer.recv(10), b'1')
# An unrelated live PID with a different start token is not ours.
unrelated = root / 'unrelated.json'
unrelated.write_text(json.dumps({'pid': os.getpid(), 'start': 'not-this-process'}))
fixture.stop(unrelated)
finally:
fixture.stop(state)
with self.assertRaises(OSError):
socket.create_connection(('127.0.0.1', port), timeout=.2)
if __name__ == '__main__':
unittest.main()
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""Run the four real arming scenarios with server log forwarding enabled and disabled."""
import argparse
import errno
import json
import os
from pathlib import Path
import re
import signal
import socket
import subprocess
import time
import xml.etree.ElementTree as ET
CASES = {
'unlocated': ('DirectGLES', 'UnlocatedIoBlockScenario.TheEmulationIsActuallyArmedWhenTheEnvironmentPinsItOn',
{'MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS': '1'}, 'WITHOUT their layout(location) qualifier'),
'primitives': ('DirectVulkan', 'PrimitivesGeneratedNoXfbScenario.TheRerouteIsActuallyArmedWhenTheEnvironmentPinsItOn',
{'MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE': '1'}, 'PRIMITIVES_GENERATED reroute engaged'),
'point-size': ('DirectGLES', 'PointSizeDemotionScenario.TheDemotionIsActuallyArmedWhenTheEnvironmentPinsItOn',
{'MOBILEGL_POINT_SIZE_DEMOTION': '1'}, 'demoted to an ordinary varying'),
'verify': ('DirectGLES', 'PipeVerifyArmingScenario.Armed',
{'MOBILEGL_PIPE_VERIFY': '1', 'MGITEST_PIPE_ARMING_LANE': '1'}, 'MGPipe: verify armed'),
}
def read(path):
return path.read_text(errors='replace') if path.exists() else ''
def run_arm(build, out, case, forward):
backend, test, knobs, marker = CASES[case]
out.mkdir(parents=True, exist_ok=True)
with socket.socket() as reserve:
reserve.bind(('127.0.0.1', 0))
port = reserve.getsockname()[1]
endpoint = f'tcp://127.0.0.1:{port}'
env = dict(os.environ, MOBILEGL_TRANSPORT='spawn', MOBILEGL_BACKEND_TYPE=backend,
MOBILEGL_IPC_REQUIRE_SAME_BUILD='1', MOBILEGL_IPC_TOKEN='', EGL_PLATFORM='surfaceless',
LIBGL_ALWAYS_SOFTWARE='1', MOBILEGL_ITEST_REQUIRE_GPU='1', **knobs)
for name in ('MOBILEGL_PIPE_VERIFY_CORRUPT', 'MOBILEGL_IPC_CONTROL', 'MOBILEGL_IPC_DIAL'):
env.pop(name, None)
if Path('/usr/share/glvnd/egl_vendor.d/50_mesa.json').exists():
env['__EGL_VENDOR_LIBRARY_FILENAMES'] = '/usr/share/glvnd/egl_vendor.d/50_mesa.json'
if Path('/usr/share/vulkan/icd.d/lvp_icd.json').exists():
env['VK_ICD_FILENAMES'] = '/usr/share/vulkan/icd.d/lvp_icd.json'
peer_env = dict(env, MOBILEGL_IPC_ROLE='server', MOBILEGL_IPC_DIAL='no',
MOBILEGL_IPC_LOG_FORWARD=str(int(forward)), MOBILEGL_LOG_FILE_PATH=str(out / 'peer.log'))
with (out / 'supervisor.log').open('wb') as output:
server = subprocess.Popen([str(build / 'libMobileGLServer.so'), endpoint, '--serve'],
env=peer_env, stdout=output, stderr=output, start_new_session=True)
try:
for _ in range(100):
if server.poll() is not None:
raise RuntimeError('supervisor exited before listening: ' + read(out / 'supervisor.log'))
with socket.socket() as probe:
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
probe.bind(('127.0.0.1', port))
except OSError as error:
if error.errno == errno.EADDRINUSE:
break
raise
time.sleep(.05)
else:
raise RuntimeError('supervisor did not listen')
client_env = dict(env, MOBILEGL_IPC_CONTROL=endpoint, MOBILEGL_IPC_DATA='stream',
MOBILEGL_IPC_LOG_FORWARD='1', MOBILEGL_LOG_FILE_PATH=str(out / 'client.log'),
MGITEST_SPLIT_LANE='1', MGITEST_TCP_LANE='1')
result = subprocess.run([str(build / 'MobileGL/MG_IntegrationTest/MobileGLIntegrationTest'),
'--gtest_filter=' + test, '--gtest_output=xml:' + str(out / 'result.xml')],
env=client_env, capture_output=True, text=True, timeout=120)
(out / 'stdout.log').write_text(result.stdout + result.stderr)
finally:
try:
os.killpg(server.pid, signal.SIGTERM)
except ProcessLookupError:
pass
server.wait(timeout=10)
passed = failed = skipped = 0
if (out / 'result.xml').exists():
for entry in ET.parse(out / 'result.xml').getroot().iter('testcase'):
if entry.find('skipped') is not None or entry.get('status') == 'notrun':
skipped += 1
elif entry.find('failure') is not None:
failed += 1
else:
passed += 1
client_log = read(out / 'client.client.log')
return {'case': test, 'returncode': result.returncode, 'passed': passed, 'failed': failed, 'skipped': skipped,
'tcp_armed': bool(re.search(r'control=tcp data=stream server=\S+ pid=[1-9][0-9]*', client_log)),
'marker_in_client': marker in client_log,
'marker_in_server': marker in read(out / 'peer.server.log'),
'marker_forwarded': marker in read(out / 'client.server.log'),
'output': str(out)}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--build-dir', type=Path, required=True)
parser.add_argument('--verify-build-dir', type=Path)
parser.add_argument('--out', type=Path, required=True)
parser.add_argument('--case', action='append', choices=CASES,
help='Explicit subset; the default runs all four and requires a verify build')
args = parser.parse_args()
selected = args.case or list(CASES)
if 'verify' in selected and not args.verify_build_dir:
parser.error('the verify scenario requires --verify-build-dir configured with MOBILEGL_PIPE_VERIFY=ON')
args.out.mkdir(parents=True, exist_ok=True)
results = {}
for case in selected:
build = args.verify_build_dir if case == 'verify' else args.build_dir
arms = {}
for forward in (True, False):
name = 'forward-on' if forward else 'forward-off'
try:
arms[name] = run_arm(build, args.out / case / name, case, forward)
except Exception as error:
arms[name] = {'error': str(error)}
on, off = arms['forward-on'], arms['forward-off']
if case == 'verify':
# PipeFill owns this marker in the client. Disabling peer forwarding
# must leave it visible; requiring red here would test a false design.
arms['expectation'] = 'client-owned marker remains green with peer forwarding disabled'
arms['proved'] = all(arm.get('passed') == 1 and arm.get('returncode') == 0
and arm.get('tcp_armed') and arm.get('marker_in_client')
for arm in (on, off))
else:
arms['expectation'] = 'server-owned marker passes with forwarding and fails without it'
arms['proved'] = bool(on.get('passed') == 1 and on.get('returncode') == 0
and on.get('tcp_armed') and on.get('marker_in_server') and on.get('marker_forwarded')
and off.get('failed') == 1 and not off.get('skipped') and off.get('tcp_armed')
and off.get('marker_in_server') and not off.get('marker_forwarded')
and not off.get('marker_in_client'))
results[case] = arms
print(case + ': ' + ('PASS' if arms['proved'] else 'NOT PROVEN'), flush=True)
(args.out / 'summary.json').write_text(json.dumps(results, indent=2))
return 0 if all(result['proved'] for result in results.values()) else 1
if __name__ == '__main__':
raise SystemExit(main())
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Start/stop the loopback supervisor used by the integration-tcp CTest fixture."""
import argparse
import errno
import ipaddress
import json
import os
from pathlib import Path
import signal
import socket
import subprocess
import time
from urllib.parse import urlsplit
def process_start(pid):
try:
return Path(f'/proc/{pid}/stat').read_text().rsplit(')', 1)[1].split()[19]
except FileNotFoundError:
return None
def stop(state):
if not state.exists():
return
saved = json.loads(state.read_text())
pid = saved['pid']
# PID reuse must never let a stale fixture file kill an unrelated process.
if process_start(pid) == saved['start']:
os.killpg(pid, signal.SIGTERM)
for _ in range(50):
if process_start(pid) != saved['start']:
break
time.sleep(.05)
else:
try:
os.killpg(pid, signal.SIGKILL)
except ProcessLookupError:
pass
state.unlink()
def start(args):
endpoint = urlsplit(args.endpoint)
if endpoint.scheme != 'tcp' or not ipaddress.ip_address(endpoint.hostname).is_loopback:
raise ValueError('the local fixture requires a numeric loopback TCP endpoint')
family = socket.AF_INET6 if ':' in endpoint.hostname else socket.AF_INET
address = (endpoint.hostname, endpoint.port)
# Do not connect as a readiness probe: it would consume the first half of
# AcceptPair, so the next real client would be paired with the probe.
with socket.socket(family) as probe:
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
probe.bind(address)
if args.state.exists():
raise RuntimeError(f'stale fixture state; run stop first: {args.state}')
env = dict(os.environ, MOBILEGL_IPC_ROLE='server', MOBILEGL_IPC_DIAL='no',
MOBILEGL_IPC_TOKEN='', MOBILEGL_IPC_LOG_FORWARD='1')
for key in ('MOBILEGL_IPC_CONTROL', 'MOBILEGL_IPC_ENDPOINT', 'MOBILEGL_BACKEND_TYPE'):
env.pop(key, None)
log_path = args.state.with_suffix('.log')
with log_path.open('wb') as log:
child = subprocess.Popen([args.server, args.endpoint, '--serve'], env=env,
stdin=subprocess.DEVNULL, stdout=log, stderr=log,
start_new_session=True)
args.state.write_text(json.dumps({'pid': child.pid, 'start': process_start(child.pid)}))
try:
for _ in range(100):
if child.poll() is not None:
raise RuntimeError(f'supervisor exited {child.returncode}: {log_path.read_text(errors="replace")}')
with socket.socket(family) as probe:
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
probe.bind(address)
except OSError as error:
if error.errno == errno.EADDRINUSE:
print(f'TCP supervisor pid={child.pid} endpoint={args.endpoint}')
return
raise
time.sleep(.05)
raise RuntimeError(f'supervisor did not listen: {log_path}')
except BaseException:
stop(args.state)
raise
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('command', choices=('start', 'stop'))
parser.add_argument('--state', type=Path, required=True)
parser.add_argument('--server')
parser.add_argument('--endpoint')
args = parser.parse_args()
if args.command == 'start':
if not args.server or not args.endpoint:
parser.error('start requires --server and --endpoint')
start(args)
else:
stop(args.state)
if __name__ == '__main__':
main()
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""Exercise a real TCP supervisor's Busy, token, version, and build-policy controls."""
import argparse
from contextlib import contextmanager
import importlib
import json
import os
from pathlib import Path
import re
import signal
import socket
import struct
import subprocess
import sys
import tempfile
import time
ROOT = Path(__file__).resolve().parents[2]
def read_exact(peer, size):
result = bytearray()
while len(result) < size:
chunk = peer.recv(size - len(result))
if not chunk:
raise RuntimeError('control channel closed before a complete frame')
result.extend(chunk)
return result
def receive(peer, schema):
magic, size = struct.unpack('<4sI', read_exact(peer, 8))
if magic != b'MGLF' or size > 64 * 1024 * 1024:
raise RuntimeError('invalid control frame')
data = read_exact(peer, size)
envelope = schema['CtrlEnvelope'].CtrlEnvelope.GetRootAs(data, 0)
table = envelope.Msg()
kind = envelope.MsgType()
if kind == schema['CtrlMsg'].CtrlMsg.Refuse:
refusal = schema['Refuse'].Refuse()
refusal.Init(table.Bytes, table.Pos)
return {'code': refusal.Code(), 'expected': refusal.Expected(),
'detail': (refusal.Detail() or b'').decode()}
if kind == schema['CtrlMsg'].CtrlMsg.Welcome:
welcome = schema['Welcome'].Welcome()
welcome.Init(table.Bytes, table.Pos)
return {'welcome': welcome.ServerPid(), 'fingerprint': welcome.WireFingerprint()}
raise RuntimeError(f'unexpected first control reply {kind}')
def hello(schema, flatbuffers, fingerprint=0, major=1, token='p65-smoke-token'):
builder = flatbuffers.Builder(512)
build = builder.CreateString('intentionally-different-build-for-p65-control')
auth = builder.CreateString(token)
terms = schema['LinkTerms']
terms.Start(builder)
terms.AddDataPlane(builder, 1)
link = terms.End(builder)
message = schema['Hello']
message.Start(builder)
message.AddAbiMajor(builder, major)
message.AddBuildFingerprint(builder, build)
message.AddBackendType(builder, 0) # DirectGLES
message.AddPid(builder, os.getpid())
message.AddWireFingerprint(builder, fingerprint)
message.AddLinkTerms(builder, link)
message.AddToken(builder, auth)
message.AddDialMode(builder, 2) # Connect
body = message.End(builder)
envelope = schema['CtrlEnvelope']
envelope.Start(builder)
envelope.AddMsgType(builder, schema['CtrlMsg'].CtrlMsg.Hello)
envelope.AddMsg(builder, body)
builder.Finish(envelope.End(builder), b'MGLC')
payload = bytes(builder.Output())
return struct.pack('<4sI', b'MGLF', len(payload)) + payload
@contextmanager
def supervisor(server, log, same_build=False):
with socket.socket() as reserve:
reserve.bind(('127.0.0.1', 0))
port = reserve.getsockname()[1]
env = dict(os.environ, MOBILEGL_IPC_ROLE='server', MOBILEGL_IPC_DIAL='no',
MOBILEGL_IPC_TOKEN='p65-smoke-token', MOBILEGL_IPC_REQUIRE_SAME_BUILD=str(int(same_build)),
MOBILEGL_IPC_LOG_FORWARD='0', MOBILEGL_LOG_FILE_PATH=str(log.with_suffix('.library.log')))
for name in ('MOBILEGL_IPC_CONTROL', 'MOBILEGL_IPC_ENDPOINT', 'MOBILEGL_BACKEND_TYPE'):
env.pop(name, None)
with log.open('wb') as output:
child = subprocess.Popen([server, f'tcp://127.0.0.1:{port}', '--serve'], env=env,
stdout=output, stderr=output, start_new_session=True)
try:
yield port, child
finally:
try:
os.killpg(child.pid, signal.SIGTERM)
except ProcessLookupError:
pass
child.wait(timeout=10)
@contextmanager
def pair(port):
deadline = time.monotonic() + 5
while True:
try:
control = socket.create_connection(('127.0.0.1', port), timeout=3)
break
except ConnectionRefusedError:
if time.monotonic() >= deadline:
raise
time.sleep(.02)
try:
data = socket.create_connection(('127.0.0.1', port), timeout=3)
try:
yield control
finally:
data.close()
finally:
control.close()
def exchange(port, schema, message):
# A prior rejected child may still be reaching _exit when the next pair
# arrives. Only Busy may be retried here, and never in the Busy assertion.
for _ in range(30):
with pair(port) as control:
control.sendall(message)
reply = receive(control, schema)
if reply.get('code') != 7:
return reply
time.sleep(.05)
raise RuntimeError('supervisor remained Busy after the previous peer closed')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--server', required=True)
parser.add_argument('--flatc', default='flatc')
parser.add_argument('--out', type=Path, required=True)
args = parser.parse_args()
args.out.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix='mgl-tcp-schema-') as directory:
subprocess.run([args.flatc, '--python', '-o', directory,
str(ROOT / 'MobileGL/MG_Remote/Protocol/protocol.fbs')], check=True)
sys.path[:0] = [directory, str(ROOT / '3rdparty/flatbuffers/python')]
import flatbuffers
schema = {name: importlib.import_module('MobileGL.Wire.' + name)
for name in ('CtrlEnvelope', 'CtrlMsg', 'Hello', 'LinkTerms', 'Refuse', 'Welcome')}
evidence = {}
with supervisor(args.server, args.out / 'supervisor.log') as (port, process):
with pair(port) as held:
with pair(port) as second:
busy = receive(second, schema)
assert busy.get('code') == 7, busy
evidence['busy'] = busy
token = exchange(port, schema, hello(schema, flatbuffers, token='wrong-token'))
assert token.get('code') == 4, token
evidence['wrong_token'] = token
version = exchange(port, schema, hello(schema, flatbuffers, major=99))
assert version.get('code') == 1, version
evidence['wire_major_99'] = version
layout = exchange(port, schema, hello(schema, flatbuffers))
assert layout.get('code') == 2 and layout.get('expected', 0), layout
fingerprint = layout['expected']
evidence['wrong_layout'] = layout
accepted = exchange(port, schema, hello(schema, flatbuffers, fingerprint=fingerprint))
assert accepted.get('welcome', 0) and accepted['welcome'] != os.getpid(), accepted
assert accepted.get('fingerprint') == fingerprint, accepted
evidence['different_build_connect'] = accepted
assert process.poll() is None, 'a rejected session killed the supervisor'
with supervisor(args.server, args.out / 'same-build-supervisor.log', same_build=True) as (port, _):
strict = exchange(port, schema, hello(schema, flatbuffers, fingerprint=fingerprint))
assert strict.get('code') == 3, strict
evidence['same_build_required'] = strict
(args.out / 'protocol-controls.json').write_text(json.dumps(evidence, indent=2))
print(json.dumps(evidence, indent=2))
if __name__ == '__main__':
main()
+28
View File
@@ -692,6 +692,33 @@ def banner(name, title, sources):
return GENERATED_BANNER.format(name=name, title=title, sources=sources)
def gen_wire_layout(calls, payloads):
"""Compiler-evaluated wire layout, including same-width member reorders."""
out = [banner("PipeWireLayout.inc", "P6.5: wire layout and ordered catalogue.",
"PipeFields.def and PipeCalls.def")]
out += ["struct WireLayoutMember { const char* Name; Uint64 Offset; Uint64 Size; };",
"inline constexpr WireLayoutMember kMGPipeWireLayoutMembers[] = {"]
fields = parse_field_lists()
for payload in payloads:
out.append(' {"%s", 0, sizeof(%s)},' % (payload, payload))
for field in fields[payload]:
out.append(' {"%s.%s", offsetof(%s, %s), sizeof(((%s*)nullptr)->%s)},' %
(payload, field, payload, field, payload, field))
for payload, names in (("MGPipeHandle", ("Slot", "Gen")),
("MGPWireRecHeader", ("Op", "Flags", "Size"))):
out.append(' {"%s", 0, sizeof(%s)},' % (payload, payload))
for field in names:
out.append(' {"%s.%s", offsetof(%s, %s), sizeof(((%s*)nullptr)->%s)},' %
(payload, field, payload, field, payload, field))
out += ["};", "struct WireCallLayout { const char* Name; const char* Payload; Uint64 Op; Uint64 Flags; Uint64 Wait; };",
"inline constexpr WireCallLayout kMGPipeWireCallLayouts[] = {"]
for call in calls:
out.append(' {"%s", "%s", static_cast<Uint64>(MGPWireOp::%s), static_cast<Uint64>(%s), static_cast<Uint64>(%s)},' %
(call.Name, call.Payload, call.Name, " | ".join(call.Flags), call.Wait))
out += ["};", ""]
return "\n".join(out)
def gen_tables(calls):
screen = [c for c in calls if c.IsScreen]
context = [c for c in calls if not c.IsScreen]
@@ -1698,6 +1725,7 @@ def main():
write(os.path.join(GENERATED_DIR, "PipeThunks.inc"), gen_thunks(calls), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeWire.inc"),
gen_wire(calls, parse_field_lists().get("ResidualValueBlock")), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeWireLayout.inc"), gen_wire_layout(calls, payloads), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeVerify.inc"), gen_verify(payloads), args.check, changed)
write(os.path.join(GENERATED_DIR, "PipeFilled.inc"), gen_filled(accessors, calls, sticky, emitted),
args.check, changed)
+14 -1
View File
@@ -319,6 +319,8 @@ else()
endif()
enable_testing()
set(MOBILEGL_TRACE_TCP_ENDPOINT "" CACHE STRING "Optional TCP supervisor endpoint for the CI split trace subset")
set(MOBILEGL_TRACE_TCP_TOKEN "" CACHE STRING "Token for the TCP trace supervisor")
function(add_trace_replay_test CASE_NAME BACKEND)
set(oneValueArgs
TRACE_ARCHIVE
@@ -340,7 +342,9 @@ function(add_trace_replay_test CASE_NAME BACKEND)
# entry is a second arm of.
VARIANT
TRANSPORT
IPC_SERVER_PATH)
IPC_SERVER_PATH
IPC_CONTROL
IPC_TOKEN)
cmake_parse_arguments(TRACE_CASE "" "${oneValueArgs}" "" ${ARGN})
foreach(required TRACE_ARCHIVE GOLDEN TARGET_CALL WIDTH HEIGHT)
if(NOT TRACE_CASE_${required})
@@ -417,6 +421,8 @@ function(add_trace_replay_test CASE_NAME BACKEND)
-DTRACE_COHERENT_AS_FLUSH=${TRACE_CASE_COHERENT_AS_FLUSH}
-DTRACE_TRANSPORT=${TRACE_CASE_TRANSPORT}
-DTRACE_IPC_SERVER_PATH=${TRACE_CASE_IPC_SERVER_PATH}
-DTRACE_IPC_CONTROL=${TRACE_CASE_IPC_CONTROL}
-DTRACE_IPC_TOKEN=${TRACE_CASE_IPC_TOKEN}
-DTRACE_OUTPUT_DIR=${CMAKE_CURRENT_BINARY_DIR}/${CASE_NAME}/${trace_case_output_tag}
-DTRACE_ARTIFACT_DIR=${CMAKE_CURRENT_BINARY_DIR}/${CASE_NAME}/${trace_case_artifact_tag}
-P ${MOBILEGL_TRACE_ROOT}/run_trace_case.cmake)
@@ -440,6 +446,9 @@ function(add_trace_replay_test CASE_NAME BACKEND)
ENVIRONMENT "LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330"
LABELS "${trace_case_labels}")
endif()
if(TRACE_CASE_IPC_CONTROL)
set_tests_properties(${trace_case_name} PROPERTIES RESOURCE_LOCK mobilegl-tcp-trace)
endif()
endfunction()
function(add_trace_replay_test_for_backends CASE_NAME)
@@ -491,6 +500,10 @@ function(add_trace_replay_split_test CASE_NAME)
IPC_SERVER_PATH "${CMAKE_BINARY_DIR}/libMobileGLServer.so" ${ARGN})
add_trace_replay_test(${CASE_NAME} ${backend} VARIANT SPAWN TRANSPORT spawn
IPC_SERVER_PATH "${CMAKE_BINARY_DIR}/libMobileGLServer.so" ${ARGN})
if(MOBILEGL_TRACE_TCP_ENDPOINT)
add_trace_replay_test(${CASE_NAME} ${backend} VARIANT TCP TRANSPORT spawn
IPC_CONTROL "${MOBILEGL_TRACE_TCP_ENDPOINT}" IPC_TOKEN "${MOBILEGL_TRACE_TCP_TOKEN}" ${ARGN})
endif()
endforeach()
endfunction()
+18 -1
View File
@@ -51,6 +51,13 @@ if(DEFINED TRACE_IPC_SERVER_PATH AND NOT "${TRACE_IPC_SERVER_PATH}" STREQUAL "")
# "parsed and ignored" stop being the same observation.
set(ENV{MOBILEGL_IPC_SERVER_PATH} "${TRACE_IPC_SERVER_PATH}")
endif()
if(DEFINED TRACE_IPC_CONTROL AND NOT "${TRACE_IPC_CONTROL}" STREQUAL "")
set(ENV{MOBILEGL_IPC_CONTROL} "${TRACE_IPC_CONTROL}")
set(ENV{MOBILEGL_IPC_DATA} stream)
set(ENV{MOBILEGL_IPC_TOKEN} "${TRACE_IPC_TOKEN}")
set(ENV{MOBILEGL_IPC_REQUIRE_SAME_BUILD} 1)
set(ENV{MOBILEGL_IPC_LOG_FORWARD} 1)
endif()
if(EXISTS "${TRACE_OUTPUT_DIR}")
file(REMOVE_RECURSE "${TRACE_OUTPUT_DIR}")
@@ -294,7 +301,17 @@ if(DEFINED ENV{MOBILEGL_TRANSPORT} AND NOT "$ENV{MOBILEGL_TRANSPORT}" STREQUAL "
set(split_expected_marker "MOBILEGL_TRANSPORT=inproc - the MGPipe record stream")
elseif("$ENV{MOBILEGL_TRANSPORT}" STREQUAL "spawn")
set(split_expected_marker "MOBILEGL_TRANSPORT=spawn - the MGPipe record stream")
set(split_expected_second "spawn ARMED - the server role runs in pid ")
if("$ENV{MOBILEGL_IPC_CONTROL}" MATCHES "^tcp://")
set(split_expected_second "control=tcp data=stream server=")
if(NOT split_log MATCHES "control=tcp data=stream server=[^ \r\n]+ pid=[1-9][0-9]*")
message(FATAL_ERROR "${split_case}: TCP session has no endpoint and server pid arm proof")
endif()
if(NOT EXISTS "${mobilegl_server_log}")
message(FATAL_ERROR "${split_case}: TCP session has no forwarded server log")
endif()
else()
set(split_expected_second "spawn ARMED - the server role runs in pid ")
endif()
else()
message(FATAL_ERROR
"MOBILEGL_TRANSPORT=$ENV{MOBILEGL_TRANSPORT} is set for ${split_case} and this "
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""Start or stop the trace APK's foreground TCP supervisor through adb."""
import argparse
import shlex
import subprocess
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('action', choices=('start', 'stop'))
parser.add_argument('--adb', default='adb')
parser.add_argument('--serial', default='2f7cbe2e')
parser.add_argument('--package', default='top.mobilegl.plugin.trace')
parser.add_argument('--listen', default='tcp://127.0.0.1:40613')
parser.add_argument('--token', default='')
parser.add_argument('--env', action='append', default=[], help='KEY=VALUE;KEY overrides (repeatable)')
parser.add_argument('--forward', action='store_true', help='Forward local adb port 40613 to the device')
args = parser.parse_args()
adb = [args.adb, '-s', args.serial]
if args.action == 'stop':
command = ['am', 'force-stop', args.package]
else:
command = ['am', 'start-foreground-service', '-n', args.package + '/top.mobilegl.plugin.MobileGLServerService',
'--es', 'listen', args.listen, '--es', 'token', args.token,
'--es', 'env', ';'.join(args.env)]
subprocess.run(adb + ['shell', shlex.join(command)], check=True)
if args.action == 'start' and args.forward:
subprocess.run(adb + ['forward', 'tcp:40613', 'tcp:40613'], check=True)
if args.action == 'start':
print('Read readiness: adb -s ' + args.serial + ' logcat -s MobileGLServer')
print('Client: MOBILEGL_TRANSPORT=spawn MOBILEGL_IPC_DATA=stream '
'MOBILEGL_IPC_REQUIRE_SAME_BUILD=1 MOBILEGL_IPC_CONTROL=tcp://<device-address>:40613')
if __name__ == '__main__':
main()