diff --git a/CMakeLists.txt b/CMakeLists.txt index 66ad6c65..c8c41dbf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 0329ee05..5a98476b 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -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: | 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. diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 73710986..60a68c2a 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -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 diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 2d25fe68..94639b31 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -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; diff --git a/MobileGL/MG_Impl/Pipe/ResourceTracker.h b/MobileGL/MG_Impl/Pipe/ResourceTracker.h index 28ca4305..b77a57cb 100644 --- a/MobileGL/MG_Impl/Pipe/ResourceTracker.h +++ b/MobileGL/MG_Impl/Pipe/ResourceTracker.h @@ -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 " diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 2ce0de2a..bb8e4f8a 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -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") diff --git a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp index 74dab901..508f8990 100644 --- a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp +++ b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp @@ -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; } diff --git a/MobileGL/MG_IntegrationTest/Harness/PipeStatsWindow.h b/MobileGL/MG_IntegrationTest/Harness/PipeStatsWindow.h index d5cc9f8c..24582fdb 100644 --- a/MobileGL/MG_IntegrationTest/Harness/PipeStatsWindow.h +++ b/MobileGL/MG_IntegrationTest/Harness/PipeStatsWindow.h @@ -36,6 +36,10 @@ #include #include +#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); } diff --git a/MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h b/MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h index e3055c04..460a392f 100644 --- a/MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h +++ b/MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h @@ -23,7 +23,11 @@ #include #include +#include #include +#if defined(__linux__) +#include +#endif #include @@ -152,6 +156,18 @@ namespace MGITest { RecordProperty("split_transport", state.transportName); RecordProperty("split_implemented_verbs", static_cast(state.implementedVerbs)); RecordProperty("split_emit_seq_at_setup", static_cast(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(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 - diff --git a/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py b/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py index acf7e277..cdddc66e 100644 --- a/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py +++ b/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py @@ -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: diff --git a/MobileGL/MG_Pipe/MGPipeTypes.h b/MobileGL/MG_Pipe/MGPipeTypes.h index fe97cb47..918d22b8 100644 --- a/MobileGL/MG_Pipe/MGPipeTypes.h +++ b/MobileGL/MG_Pipe/MGPipeTypes.h @@ -175,10 +175,8 @@ namespace MobileGL::MG_Pipe { MGPBlobRef RendererInfo; }; static_assert(std::is_trivially_copyable_v, "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"); diff --git a/MobileGL/MG_Pipe/PipeWireLayout.h b/MobileGL/MG_Pipe/PipeWireLayout.h new file mode 100644 index 00000000..dfb7cfcf --- /dev/null +++ b/MobileGL/MG_Pipe/PipeWireLayout.h @@ -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(*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 diff --git a/MobileGL/MG_Pipe/generated/PipeWireLayout.inc b/MobileGL/MG_Pipe/generated/PipeWireLayout.inc new file mode 100644 index 00000000..88321522 --- /dev/null +++ b/MobileGL/MG_Pipe/generated/PipeWireLayout.inc @@ -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(MGPWireOp::GetCaps), static_cast(kReplySlot | kHasBlob), static_cast(kWaitReply)}, + {"ResourceCreate", "MGPResourceDesc", static_cast(MGPWireOp::ResourceCreate), static_cast(kReplySlot), static_cast(kWaitReply)}, + {"ResourceRespecify", "MGPResourceDesc", static_cast(MGPWireOp::ResourceRespecify), static_cast(kNeedsAck | kReplySlot), static_cast(kWaitReply)}, + {"ResourceDestroy", "MGPHandleOnly", static_cast(MGPWireOp::ResourceDestroy), static_cast(kNone), static_cast(kWaitNone)}, + {"MapPersistent", "MGPHandleOnly", static_cast(MGPWireOp::MapPersistent), static_cast(kReplySlot | kOptional), static_cast(kWaitReply)}, + {"UnmapPersistent", "MGPHandleOnly", static_cast(MGPWireOp::UnmapPersistent), static_cast(kOptional), static_cast(kWaitNone)}, + {"FenceCreate", "MGPHandleOnly", static_cast(MGPWireOp::FenceCreate), static_cast(kNone), static_cast(kWaitNone)}, + {"FenceStatus", "MGPHandleOnly", static_cast(MGPWireOp::FenceStatus), static_cast(kReplySlot), static_cast(kWaitReply)}, + {"FenceWait", "MGPFenceWait", static_cast(MGPWireOp::FenceWait), static_cast(kReplySlot), static_cast(kWaitReply)}, + {"FenceDestroy", "MGPHandleOnly", static_cast(MGPWireOp::FenceDestroy), static_cast(kNone), static_cast(kWaitNone)}, + {"QueryCreate", "MGPQueryDesc", static_cast(MGPWireOp::QueryCreate), static_cast(kNone), static_cast(kWaitNone)}, + {"QueryBegin", "MGPQueryDesc", static_cast(MGPWireOp::QueryBegin), static_cast(kNone), static_cast(kWaitNone)}, + {"QueryEnd", "MGPQueryDesc", static_cast(MGPWireOp::QueryEnd), static_cast(kNone), static_cast(kWaitNone)}, + {"QueryAvailable", "MGPHandleOnly", static_cast(MGPWireOp::QueryAvailable), static_cast(kReplySlot), static_cast(kWaitReply)}, + {"QueryResult", "MGPQueryResultRequest", static_cast(MGPWireOp::QueryResult), static_cast(kReplySlot), static_cast(kWaitReply)}, + {"QueryDestroy", "MGPHandleOnly", static_cast(MGPWireOp::QueryDestroy), static_cast(kNone), static_cast(kWaitNone)}, + {"CreateRenderState", "MGPRenderStateDesc", static_cast(MGPWireOp::CreateRenderState), static_cast(kHasBlob), static_cast(kWaitNone)}, + {"BindRenderState", "MGPBindRenderState", static_cast(MGPWireOp::BindRenderState), static_cast(kNone), static_cast(kWaitNone)}, + {"DeleteRenderState", "MGPHandleOnly", static_cast(MGPWireOp::DeleteRenderState), static_cast(kNone), static_cast(kWaitNone)}, + {"CreateVertexElements", "MGPVertexElements", static_cast(MGPWireOp::CreateVertexElements), static_cast(kHasBlob), static_cast(kWaitNone)}, + {"BindVertexElements", "MGPHandleOnly", static_cast(MGPWireOp::BindVertexElements), static_cast(kNone), static_cast(kWaitNone)}, + {"DeleteVertexElements", "MGPHandleOnly", static_cast(MGPWireOp::DeleteVertexElements), static_cast(kNone), static_cast(kWaitNone)}, + {"CreateSamplerState", "MGPSamplerDesc", static_cast(MGPWireOp::CreateSamplerState), static_cast(kHasBlob), static_cast(kWaitNone)}, + {"DeleteSamplerState", "MGPHandleOnly", static_cast(MGPWireOp::DeleteSamplerState), static_cast(kNone), static_cast(kWaitNone)}, + {"CreateSamplerView", "MGPSamplerView", static_cast(MGPWireOp::CreateSamplerView), static_cast(kNone), static_cast(kWaitNone)}, + {"DeleteSamplerView", "MGPHandleOnly", static_cast(MGPWireOp::DeleteSamplerView), static_cast(kNone), static_cast(kWaitNone)}, + {"CreateShaderState", "MGPProgramDesc", static_cast(MGPWireOp::CreateShaderState), static_cast(kHasBlob), static_cast(kWaitNone)}, + {"BindShaderState", "MGPHandleOnly", static_cast(MGPWireOp::BindShaderState), static_cast(kNone), static_cast(kWaitNone)}, + {"DeleteShaderState", "MGPHandleOnly", static_cast(MGPWireOp::DeleteShaderState), static_cast(kNone), static_cast(kWaitNone)}, + {"SetDynamicState", "MGPDynamicState", static_cast(MGPWireOp::SetDynamicState), static_cast(kHasBlob), static_cast(kWaitNone)}, + {"SetFramebufferState", "MGPFramebufferState", static_cast(MGPWireOp::SetFramebufferState), static_cast(kNone), static_cast(kWaitNone)}, + {"SetVertexBuffers", "MGPVertexBuffers", static_cast(MGPWireOp::SetVertexBuffers), static_cast(kVarTail), static_cast(kWaitNone)}, + {"SetIndexBuffer", "MGPIndexBuffer", static_cast(MGPWireOp::SetIndexBuffer), static_cast(kNone), static_cast(kWaitNone)}, + {"SetIndirectBuffers", "MGPIndirectBuffers", static_cast(MGPWireOp::SetIndirectBuffers), static_cast(kNone), static_cast(kWaitNone)}, + {"SetSamplerViews", "MGPSamplerViews", static_cast(MGPWireOp::SetSamplerViews), static_cast(kVarTail), static_cast(kWaitNone)}, + {"BindSamplerStates", "MGPSamplerStates", static_cast(MGPWireOp::BindSamplerStates), static_cast(kVarTail), static_cast(kWaitNone)}, + {"SetShaderImages", "MGPShaderImages", static_cast(MGPWireOp::SetShaderImages), static_cast(kVarTail), static_cast(kWaitNone)}, + {"SetShaderBuffers", "MGPShaderBuffers", static_cast(MGPWireOp::SetShaderBuffers), static_cast(kVarTail | kHostSpan), static_cast(kWaitNone)}, + {"SetStreamOutputTargets", "MGPStreamOutputTargets", static_cast(MGPWireOp::SetStreamOutputTargets), static_cast(kVarTail), static_cast(kWaitNone)}, + {"SetGlobalConstants", "MGPGlobalConstants", static_cast(MGPWireOp::SetGlobalConstants), static_cast(kHasBlob), static_cast(kWaitNone)}, + {"SetVertexAttribDefaults", "MGPVertexAttribDefaults", static_cast(MGPWireOp::SetVertexAttribDefaults), static_cast(kVarTail), static_cast(kWaitNone)}, + {"SetPixelPackState", "MGPPixelPackState", static_cast(MGPWireOp::SetPixelPackState), static_cast(kNone), static_cast(kWaitNone)}, + {"SetPatchState", "MGPPatchState", static_cast(MGPWireOp::SetPatchState), static_cast(kNone), static_cast(kWaitNone)}, + {"SetDrawProgram", "MGPHandleOnly", static_cast(MGPWireOp::SetDrawProgram), static_cast(kNone), static_cast(kWaitNone)}, + {"SetDispatchProgram", "MGPHandleOnly", static_cast(MGPWireOp::SetDispatchProgram), static_cast(kNone), static_cast(kWaitNone)}, + {"SetResidualValueState", "MGPResidualValueState", static_cast(MGPWireOp::SetResidualValueState), static_cast(kHasBlob), static_cast(kWaitNone)}, + {"SetTextureParams", "MGPTextureParams", static_cast(MGPWireOp::SetTextureParams), static_cast(kReplySlot), static_cast(kWaitReply)}, + {"ResourceSubData", "MGPSubData", static_cast(MGPWireOp::ResourceSubData), static_cast(kHasBlob | kVarTail | kReplySlot), static_cast(kWaitReply)}, + {"BufferSubDataResident", "MGPSubData", static_cast(MGPWireOp::BufferSubDataResident), static_cast(kHasBlob | kOptional), static_cast(kWaitNone)}, + {"ResourceSubDataComplete", "MGPSubDataComplete", static_cast(MGPWireOp::ResourceSubDataComplete), static_cast(kNone), static_cast(kWaitNone)}, + {"ResourceFlushRange", "MGPFlushRange", static_cast(MGPWireOp::ResourceFlushRange), static_cast(kNone), static_cast(kWaitNone)}, + {"ResourceReadback", "MGPReadback", static_cast(MGPWireOp::ResourceReadback), static_cast(kReplySlot), static_cast(kWaitReply)}, + {"ResourceCopyRegion", "MGPCopyRegion", static_cast(MGPWireOp::ResourceCopyRegion), static_cast(kNone), static_cast(kWaitApplied)}, + {"GenerateMipmap", "MGPMipPlan", static_cast(MGPWireOp::GenerateMipmap), static_cast(kNone), static_cast(kWaitApplied)}, + {"GetTextureImage", "MGPReadbackInfo", static_cast(MGPWireOp::GetTextureImage), static_cast(kReplySlot), static_cast(kWaitReply)}, + {"Blit", "MGPBlit", static_cast(MGPWireOp::Blit), static_cast(kNone), static_cast(kWaitNone)}, + {"Clear", "MGPClear", static_cast(MGPWireOp::Clear), static_cast(kNone), static_cast(kWaitNone)}, + {"ReadPixels", "MGPReadbackInfo", static_cast(MGPWireOp::ReadPixels), static_cast(kReplySlot), static_cast(kWaitReply)}, + {"DrawVbo", "MGPDrawInfo", static_cast(MGPWireOp::DrawVbo), static_cast(kHostSpan | kVarTail), static_cast(kWaitNone)}, + {"LaunchGrid", "MGPGridInfo", static_cast(MGPWireOp::LaunchGrid), static_cast(kNone), static_cast(kWaitNone)}, + {"MemoryBarrier", "MGPMemoryBarrier", static_cast(MGPWireOp::MemoryBarrier), static_cast(kNone), static_cast(kWaitNone)}, + {"BeginStreamOutput", "MGPStreamOutputBegin", static_cast(MGPWireOp::BeginStreamOutput), static_cast(kNone), static_cast(kWaitApplied)}, + {"EndStreamOutput", "MGPXfbAccounting", static_cast(MGPWireOp::EndStreamOutput), static_cast(kNone), static_cast(kWaitApplied)}, + {"PauseStreamOutput", "MGPStreamOutputControl", static_cast(MGPWireOp::PauseStreamOutput), static_cast(kNone), static_cast(kWaitApplied)}, + {"ResumeStreamOutput", "MGPStreamOutputControl", static_cast(MGPWireOp::ResumeStreamOutput), static_cast(kNone), static_cast(kWaitApplied)}, + {"Flush", "MGPFlush", static_cast(MGPWireOp::Flush), static_cast(kNone), static_cast(kWaitNone)}, + {"Present", "MGPPresent", static_cast(MGPWireOp::Present), static_cast(kNone), static_cast(kWaitPresent)}, + {"SetSwapInterval", "MGPSwapInterval", static_cast(MGPWireOp::SetSwapInterval), static_cast(kOptional), static_cast(kWaitNone)}, + {"QueryTimestamp", "MGPTimestampRequest", static_cast(MGPWireOp::QueryTimestamp), static_cast(kReplySlot), static_cast(kWaitReply)}, + {"QueryCounter", "MGPQueryDesc", static_cast(MGPWireOp::QueryCounter), static_cast(kNone), static_cast(kWaitNone)}, + {"FenceWaitServer", "MGPFenceWait", static_cast(MGPWireOp::FenceWaitServer), static_cast(kNone), static_cast(kWaitNone)}, + {"BindShaderImage", "MGPImageBind", static_cast(MGPWireOp::BindShaderImage), static_cast(kNone), static_cast(kWaitNone)}, + {"PatchParameter", "MGPPatchParameter", static_cast(MGPWireOp::PatchParameter), static_cast(kNone), static_cast(kWaitNone)}, + {"BindStreamOutput", "MGPStreamOutputBind", static_cast(MGPWireOp::BindStreamOutput), static_cast(kNone), static_cast(kWaitApplied)}, + {"SetStorageBlockBinding", "MGPStorageBlockBinding", static_cast(MGPWireOp::SetStorageBlockBinding), static_cast(kHasBlob), static_cast(kWaitApplied)}, + {"CopyFramebufferToTexture", "MGPCopyFromFramebuffer", static_cast(MGPWireOp::CopyFramebufferToTexture), static_cast(kNone), static_cast(kWaitApplied)}, + {"ApplierReset", "MGPApplierReset", static_cast(MGPWireOp::ApplierReset), static_cast(kNone), static_cast(kWaitApplied)}, + {"ObjectDeath", "MGPHandleOnly", static_cast(MGPWireOp::ObjectDeath), static_cast(kNone), static_cast(kWaitNone)}, + {"SetContextValues", "MGPContextValues", static_cast(MGPWireOp::SetContextValues), static_cast(kNone), static_cast(kWaitNone)}, + {"SetProgramBindings", "MGPProgramBindings", static_cast(MGPWireOp::SetProgramBindings), static_cast(kVarTail | kHostSpan), static_cast(kWaitNone)}, + {"DeleteStreamOutput", "MGPStreamOutputBind", static_cast(MGPWireOp::DeleteStreamOutput), static_cast(kNone), static_cast(kWaitNone)}, +}; diff --git a/MobileGL/MG_Remote/CapsCodec.cpp b/MobileGL/MG_Remote/CapsCodec.cpp index ea2019a4..df958267 100644 --- a/MobileGL/MG_Remote/CapsCodec.cpp +++ b/MobileGL/MG_Remote/CapsCodec.cpp @@ -35,6 +35,7 @@ #include "CapsCodec.h" #include "Protocol/mg_protocol_base.h" +#include #include #include @@ -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(MG_Backend::kFormatCapabilityTargetCount); - inputs.FormatCapabilityFormats = static_cast(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(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(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(&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 diff --git a/MobileGL/MG_Remote/CapsCodec.h b/MobileGL/MG_Remote/CapsCodec.h index 5fe14c9f..7f326c7d 100644 --- a/MobileGL/MG_Remote/CapsCodec.h +++ b/MobileGL/MG_Remote/CapsCodec.h @@ -20,13 +20,7 @@ // RendererInfo -> RendererInfo (MG_Util/Types.h:317): three Strings, an // Optional, and a Vector 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 @@ -90,19 +84,11 @@ namespace MobileGL::MG_Remote { Bool EncodeRendererInfo(const RendererInfo& info, Vector& 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 diff --git a/MobileGL/MG_Remote/Client/ClientSession.cpp b/MobileGL/MG_Remote/Client/ClientSession.cpp index fb771d24..8636078d 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.cpp +++ b/MobileGL/MG_Remote/Client/ClientSession.cpp @@ -11,6 +11,8 @@ #include "ClientSession.h" #include +#include +#include #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(MOBILEGL_PROTOCOL_ABI_MAJOR), - static_cast(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(ours), - static_cast(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(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(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(welcome->abiMajor()), - static_cast(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(MG_Config::ActiveBackendType)) { + const auto result = RefuseHandshake(*m_transport, ::MobileGL::Wire::RefuseCode::Backend, + "server backend differs from request", static_cast(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 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(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(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(welcome->abiMajor()), - static_cast(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(MG_Config::ActiveBackendType)) { + const auto result = RefuseHandshake(*m_transport, ::MobileGL::Wire::RefuseCode::Backend, + "server backend differs from request", static_cast(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(*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(user)->RunRemoteSurfaceControlFrame(frame); } + MobileGLResult ClientSession::ReceiveControlFrame(std::vector& 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 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 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 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 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(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(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(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 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 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 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(size)); + return true; } - Uint32 ClientSession::MaxReplyBytes() const { return m_replies.MaxReplyBytes(); } + Uint32 ClientSession::MaxReplyBytes() const { return static_cast(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(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(); +} diff --git a/MobileGL/MG_Remote/Client/ClientSession.h b/MobileGL/MG_Remote/Client/ClientSession.h index 3ccfd5b5..177c39d5 100644 --- a/MobileGL/MG_Remote/Client/ClientSession.h +++ b/MobileGL/MG_Remote/Client/ClientSession.h @@ -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 #include // 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 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& frame, Uint32 timeoutMs); + std::unique_ptr m_controlInbox; + std::unique_ptr 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 m_clientTransport; std::unique_ptr 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, diff --git a/MobileGL/MG_Remote/Handshake.h b/MobileGL/MG_Remote/Handshake.h new file mode 100644 index 00000000..3b99e9af --- /dev/null +++ b/MobileGL/MG_Remote/Handshake.h @@ -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 +#include +#include + +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(expected), static_cast(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(refusal->expected()), + static_cast(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 ? "" : stamp); + if (!sameBuild && dial == ::MobileGL::Wire::DialMode::Connect) + MGLOG_W("MGPipe: compatible wire with different build: local=%s peer=%s", + BuildFingerprint(), stamp == nullptr ? "" : stamp); + return MOBILEGL_OK; + } +} // namespace MobileGL::MG_Remote diff --git a/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp b/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp index 2e362e3b..aa2ca1f2 100644 --- a/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp +++ b/MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp @@ -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) { diff --git a/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h b/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h index f7aedf7f..86e387a2 100644 --- a/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h +++ b/MobileGL/MG_Remote/Protocol/generated/protocol_generated.h @@ -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(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(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(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(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(e); return EnumNamesCtrlMsg()[index]; } @@ -439,6 +595,14 @@ template<> struct CtrlMsgTraits { static const CtrlMsg enum_value = CtrlMsg::LogLine; }; +template<> struct CtrlMsgTraits { + static const CtrlMsg enum_value = CtrlMsg::Refuse; +}; + +template<> struct CtrlMsgTraits { + static const CtrlMsg enum_value = CtrlMsg::LogFlush; +}; + template bool VerifyCtrlMsg(::flatbuffers::VerifierTemplate &verifier, const void *obj, CtrlMsg type); template @@ -538,6 +702,270 @@ inline ::flatbuffers::Offset 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(GetField(VT_DATAPLANE, 0)); + } + MobileGL::Wire::WireForm wireForm() const { + return static_cast(GetField(VT_WIREFORM, 0)); + } + uint64_t maxReplyBytes() const { + return GetField(VT_MAXREPLYBYTES, 0); + } + uint64_t cmdWindowBytes() const { + return GetField(VT_CMDWINDOWBYTES, 0); + } + uint64_t stageWindowBytes() const { + return GetField(VT_STAGEWINDOWBYTES, 0); + } + uint64_t eventWindowBytes() const { + return GetField(VT_EVENTWINDOWBYTES, 0); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_DATAPLANE, 1) && + VerifyField(verifier, VT_WIREFORM, 1) && + VerifyField(verifier, VT_MAXREPLYBYTES, 8) && + VerifyField(verifier, VT_CMDWINDOWBYTES, 8) && + VerifyField(verifier, VT_STAGEWINDOWBYTES, 8) && + VerifyField(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(LinkTerms::VT_DATAPLANE, static_cast(dataPlane), 0); + } + void add_wireForm(MobileGL::Wire::WireForm wireForm) { + fbb_.AddElement(LinkTerms::VT_WIREFORM, static_cast(wireForm), 0); + } + void add_maxReplyBytes(uint64_t maxReplyBytes) { + fbb_.AddElement(LinkTerms::VT_MAXREPLYBYTES, maxReplyBytes, 0); + } + void add_cmdWindowBytes(uint64_t cmdWindowBytes) { + fbb_.AddElement(LinkTerms::VT_CMDWINDOWBYTES, cmdWindowBytes, 0); + } + void add_stageWindowBytes(uint64_t stageWindowBytes) { + fbb_.AddElement(LinkTerms::VT_STAGEWINDOWBYTES, stageWindowBytes, 0); + } + void add_eventWindowBytes(uint64_t eventWindowBytes) { + fbb_.AddElement(LinkTerms::VT_EVENTWINDOWBYTES, eventWindowBytes, 0); + } + explicit LinkTermsBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset 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(GetField(VT_CODE, 0)); + } + const ::flatbuffers::String *detail() const { + return GetPointer(VT_DETAIL); + } + uint64_t expected() const { + return GetField(VT_EXPECTED, 0); + } + uint64_t actual() const { + return GetField(VT_ACTUAL, 0); + } + const ::flatbuffers::String *peerValue() const { + return GetPointer(VT_PEERVALUE); + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_CODE, 4) && + VerifyOffset(verifier, VT_DETAIL) && + verifier.VerifyString(detail()) && + VerifyField(verifier, VT_EXPECTED, 8) && + VerifyField(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(Refuse::VT_CODE, static_cast(code), 0); + } + void add_detail(::flatbuffers::Offset<::flatbuffers::String> detail) { + fbb_.AddOffset(Refuse::VT_DETAIL, detail); + } + void add_expected(uint64_t expected) { + fbb_.AddElement(Refuse::VT_EXPECTED, expected, 0); + } + void add_actual(uint64_t actual) { + fbb_.AddElement(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 Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset 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 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(VT_SEQ, 0); + } + bool ack() const { + return GetField(VT_ACK, 0) != 0; + } + template + bool Verify(::flatbuffers::VerifierTemplate &verifier) const { + return VerifyTableStart(verifier) && + VerifyField(verifier, VT_SEQ, 8) && + VerifyField(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(LogFlush::VT_SEQ, seq, 0); + } + void add_ack(bool ack) { + fbb_.AddElement(LogFlush::VT_ACK, static_cast(ack), 0); + } + explicit LogFlushBuilder(::flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + ::flatbuffers::Offset Finish() { + const auto end = fbb_.EndTable(start_); + auto o = ::flatbuffers::Offset(end); + return o; + } +}; + +inline ::flatbuffers::Offset 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(VT_ABIMAJOR, 0); @@ -571,6 +1003,18 @@ struct Hello FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { uint64_t abiFingerprint() const { return GetField(VT_ABIFINGERPRINT, 0); } + uint64_t wireFingerprint() const { + return GetField(VT_WIREFINGERPRINT, 0); + } + const MobileGL::Wire::LinkTerms *linkTerms() const { + return GetPointer(VT_LINKTERMS); + } + const ::flatbuffers::String *token() const { + return GetPointer(VT_TOKEN); + } + MobileGL::Wire::DialMode dialMode() const { + return static_cast(GetField(VT_DIALMODE, 0)); + } template bool Verify(::flatbuffers::VerifierTemplate &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(verifier, VT_ABIFINGERPRINT, 8) && + VerifyField(verifier, VT_WIREFINGERPRINT, 8) && + VerifyOffset(verifier, VT_LINKTERMS) && + verifier.VerifyTable(linkTerms()) && + VerifyOffset(verifier, VT_TOKEN) && + verifier.VerifyString(token()) && + VerifyField(verifier, VT_DIALMODE, 1) && verifier.EndTable(); } }; @@ -612,6 +1062,18 @@ struct HelloBuilder { void add_abiFingerprint(uint64_t abiFingerprint) { fbb_.AddElement(Hello::VT_ABIFINGERPRINT, abiFingerprint, 0); } + void add_wireFingerprint(uint64_t wireFingerprint) { + fbb_.AddElement(Hello::VT_WIREFINGERPRINT, wireFingerprint, 0); + } + void add_linkTerms(::flatbuffers::Offset 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(Hello::VT_DIALMODE, static_cast(dialMode), 0); + } explicit HelloBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { start_ = fbb_.StartTable(); @@ -631,15 +1093,23 @@ inline ::flatbuffers::Offset CreateHello( uint32_t backendType = 0, uint32_t pid = 0, ::flatbuffers::Offset<::flatbuffers::Vector> configBlob = 0, - uint64_t abiFingerprint = 0) { + uint64_t abiFingerprint = 0, + uint64_t wireFingerprint = 0, + ::flatbuffers::Offset 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 CreateHelloDirect( uint32_t backendType = 0, uint32_t pid = 0, const std::vector *configBlob = nullptr, - uint64_t abiFingerprint = 0) { + uint64_t abiFingerprint = 0, + uint64_t wireFingerprint = 0, + ::flatbuffers::Offset 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(*configBlob) : 0; + auto token__ = token ? _fbb.CreateString(token) : 0; return MobileGL::Wire::CreateHello( _fbb, abiMajor, @@ -667,7 +1142,11 @@ inline ::flatbuffers::Offset 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(VT_ABIMAJOR, 0); @@ -711,6 +1193,15 @@ struct Welcome FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { uint64_t abiFingerprint() const { return GetField(VT_ABIFINGERPRINT, 0); } + uint64_t wireFingerprint() const { + return GetField(VT_WIREFINGERPRINT, 0); + } + const MobileGL::Wire::LinkTerms *linkTerms() const { + return GetPointer(VT_LINKTERMS); + } + uint32_t backendType() const { + return GetField(VT_BACKENDTYPE, 0); + } template bool Verify(::flatbuffers::VerifierTemplate &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(verifier, VT_ABIFINGERPRINT, 8) && + VerifyField(verifier, VT_WIREFINGERPRINT, 8) && + VerifyOffset(verifier, VT_LINKTERMS) && + verifier.VerifyTable(linkTerms()) && + VerifyField(verifier, VT_BACKENDTYPE, 4) && verifier.EndTable(); } }; @@ -763,6 +1258,15 @@ struct WelcomeBuilder { void add_abiFingerprint(uint64_t abiFingerprint) { fbb_.AddElement(Welcome::VT_ABIFINGERPRINT, abiFingerprint, 0); } + void add_wireFingerprint(uint64_t wireFingerprint) { + fbb_.AddElement(Welcome::VT_WIREFINGERPRINT, wireFingerprint, 0); + } + void add_linkTerms(::flatbuffers::Offset linkTerms) { + fbb_.AddOffset(Welcome::VT_LINKTERMS, linkTerms); + } + void add_backendType(uint32_t backendType) { + fbb_.AddElement(Welcome::VT_BACKENDTYPE, backendType, 0); + } explicit WelcomeBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { start_ = fbb_.StartTable(); @@ -784,9 +1288,15 @@ inline ::flatbuffers::Offset CreateWelcome( ::flatbuffers::Offset replyPool = 0, ::flatbuffers::Offset eventRing = 0, ::flatbuffers::Offset<::flatbuffers::String> buildFingerprint = 0, - uint64_t abiFingerprint = 0) { + uint64_t abiFingerprint = 0, + uint64_t wireFingerprint = 0, + ::flatbuffers::Offset 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 CreateWelcomeDirect( ::flatbuffers::Offset replyPool = 0, ::flatbuffers::Offset eventRing = 0, const char *buildFingerprint = nullptr, - uint64_t abiFingerprint = 0) { + uint64_t abiFingerprint = 0, + uint64_t wireFingerprint = 0, + ::flatbuffers::Offset 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 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(VT_SEQ, 0); @@ -1228,6 +1745,9 @@ struct SurfaceReply FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { const MobileGL::Wire::DefaultFramebufferInfo *defaultFb() const { return GetPointer(VT_DEFAULTFB); } + uint64_t eventHead() const { + return GetField(VT_EVENTHEAD, 0); + } template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { return VerifyTableStart(verifier) && @@ -1237,6 +1757,7 @@ struct SurfaceReply FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { VerifyField(verifier, VT_EGLMINOR, 4) && VerifyOffset(verifier, VT_DEFAULTFB) && verifier.VerifyTable(defaultFb()) && + VerifyField(verifier, VT_EVENTHEAD, 8) && verifier.EndTable(); } }; @@ -1260,6 +1781,9 @@ struct SurfaceReplyBuilder { void add_defaultFb(::flatbuffers::Offset defaultFb) { fbb_.AddOffset(SurfaceReply::VT_DEFAULTFB, defaultFb); } + void add_eventHead(uint64_t eventHead) { + fbb_.AddElement(SurfaceReply::VT_EVENTHEAD, eventHead, 0); + } explicit SurfaceReplyBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { start_ = fbb_.StartTable(); @@ -1277,8 +1801,10 @@ inline ::flatbuffers::Offset CreateSurfaceReply( bool ok = false, int32_t eglMajor = 0, int32_t eglMinor = 0, - ::flatbuffers::Offset defaultFb = 0) { + ::flatbuffers::Offset 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(msg()) : nullptr; } + const MobileGL::Wire::Refuse *msg_as_Refuse() const { + return msg_type() == MobileGL::Wire::CtrlMsg::Refuse ? static_cast(msg()) : nullptr; + } + const MobileGL::Wire::LogFlush *msg_as_LogFlush() const { + return msg_type() == MobileGL::Wire::CtrlMsg::LogFlush ? static_cast(msg()) : nullptr; + } template bool Verify(::flatbuffers::VerifierTemplate &verifier) const { return VerifyTableStart(verifier) && @@ -1706,6 +2238,14 @@ template<> inline const MobileGL::Wire::LogLine *CtrlEnvelope::msg_as inline const MobileGL::Wire::Refuse *CtrlEnvelope::msg_as() const { + return msg_as_Refuse(); +} + +template<> inline const MobileGL::Wire::LogFlush *CtrlEnvelope::msg_as() const { + return msg_as_LogFlush(); +} + struct CtrlEnvelopeBuilder { typedef CtrlEnvelope Table; ::flatbuffers::FlatBufferBuilder &fbb_; @@ -1788,6 +2328,14 @@ inline bool VerifyCtrlMsg(::flatbuffers::VerifierTemplate &verifier, const vo auto ptr = reinterpret_cast(obj); return verifier.VerifyTable(ptr); } + case CtrlMsg::Refuse: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } + case CtrlMsg::LogFlush: { + auto ptr = reinterpret_cast(obj); + return verifier.VerifyTable(ptr); + } default: return true; } } diff --git a/MobileGL/MG_Remote/Protocol/protocol.fbs b/MobileGL/MG_Remote/Protocol/protocol.fbs index 46c7a8a6..b37bd6c7 100644 --- a/MobileGL/MG_Remote/Protocol/protocol.fbs +++ b/MobileGL/MG_Remote/Protocol/protocol.fbs @@ -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 { diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 49990cee..9cd5384f 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -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 " diff --git a/MobileGL/MG_Remote/Server/PipeApplier.h b/MobileGL/MG_Remote/Server/PipeApplier.h index 2d9b90e5..09fb56c5 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.h +++ b/MobileGL/MG_Remote/Server/PipeApplier.h @@ -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; diff --git a/MobileGL/MG_Remote/Server/ServerLoop.cpp b/MobileGL/MG_Remote/Server/ServerLoop.cpp index db1dd90b..6f6c32b5 100644 --- a/MobileGL/MG_Remote/Server/ServerLoop.cpp +++ b/MobileGL/MG_Remote/Server/ServerLoop.cpp @@ -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 diff --git a/MobileGL/MG_Remote/Server/ServerMain.cpp b/MobileGL/MG_Remote/Server/ServerMain.cpp index ca3841a7..c541ad08 100644 --- a/MobileGL/MG_Remote/Server/ServerMain.cpp +++ b/MobileGL/MG_Remote/Server/ServerMain.cpp @@ -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 // MG_Config::Transport - D1 pins it to Spawn in this process -#include // D1c: MGPipeSetServerProcessRole +#include +#include +#include #include -#include // step 1.6: the counters' latch, server-process half -#include // MG_ConfigLoader::Init - the child loads its own config (step 1.5) - +#include +#include #include #include #include #include #include #include - -#if defined(__unix__) || defined(__APPLE__) || defined(__ANDROID__) -# include -# include -#endif +#include +#include +#include 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(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(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 control) { const int selfPid = static_cast(::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 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(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 `.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(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(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 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 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(serverBell[0], serverBell[1], 1, true); + peerBell = std::make_unique(-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(accepted)); + WireLogError("MG_Remote server: pid=%d Accept failed (rc=%d)", selfPid, static_cast(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 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(size)); continue; } + if (result != MOBILEGL_OK) break; + flatbuffers::Verifier check(buffer.data(), static_cast(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(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(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(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(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(::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 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; +} diff --git a/MobileGL/MG_Remote/Server/ServerSession.cpp b/MobileGL/MG_Remote/Server/ServerSession.cpp index 1e197679..cd1869a9 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.cpp +++ b/MobileGL/MG_Remote/Server/ServerSession.cpp @@ -8,10 +8,12 @@ // P5 package s1: the server half of a session. +#include "../Transport/SocketTransport.h" #include "ServerSession.h" #include #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(ours), - static_cast(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(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* 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 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(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(MOBILEGL_PROTOCOL_ABI_MAJOR) || - hello->abiMinor() != static_cast(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(BackendType::BackendTypeCount) || + (m_backend != nullptr && hello->backendType() != static_cast(m_backend->GetBackendType()))) + return RefuseHandshake(transport, ::MobileGL::Wire::RefuseCode::Backend, + "backend request differs from server", m_backend == nullptr ? 0 : + static_cast(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); + 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(SelfProcessId()), - cmd, stage, reply, event, stamp, ourFingerprint); + cmd, stage, reply, event, stamp, ourFingerprint, ourFingerprint, negotiated, + m_backend == nullptr ? hello->backendType() : static_cast(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 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 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(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 { diff --git a/MobileGL/MG_Remote/Server/ServerSession.h b/MobileGL/MG_Remote/Server/ServerSession.h index 4aeabd87..777b0842 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.h +++ b/MobileGL/MG_Remote/Server/ServerSession.h @@ -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 +#include #include @@ -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 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* 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 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; diff --git a/MobileGL/MG_Remote/Server/SurfaceControlFrame.h b/MobileGL/MG_Remote/Server/SurfaceControlFrame.h index 42d33d9e..5107dc6f 100644 --- a/MobileGL/MG_Remote/Server/SurfaceControlFrame.h +++ b/MobileGL/MG_Remote/Server/SurfaceControlFrame.h @@ -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 && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && IsSurfaceControlValue && - IsSurfaceControlValue && IsSurfaceControlValue; + IsSurfaceControlValue && IsSurfaceControlValue && + IsSurfaceControlValue; } } // namespace Detail diff --git a/MobileGL/MG_Remote/Transport/ControlInbox.cpp b/MobileGL/MG_Remote/Transport/ControlInbox.cpp new file mode 100644 index 00000000..39b9fcb5 --- /dev/null +++ b/MobileGL/MG_Remote/Transport/ControlInbox.cpp @@ -0,0 +1,77 @@ +#include "ControlInbox.h" +#include "Doorbell.h" +#include "../Protocol/SurfaceOpCodec.h" +#include +#include + +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 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 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 frame; + if (result == MOBILEGL_ERR_BUFFER_TOO_SMALL && size <= 64ull * 1024 * 1024) { + frame.resize(static_cast(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 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& frame, std::uint32_t timeoutMs) { + std::unique_lock 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 lock(m_mutex); + return m_frames.empty() ? 0 : m_frames.front().size(); + } +} diff --git a/MobileGL/MG_Remote/Transport/ControlInbox.h b/MobileGL/MG_Remote/Transport/ControlInbox.h new file mode 100644 index 00000000..d32a602b --- /dev/null +++ b/MobileGL/MG_Remote/Transport/ControlInbox.h @@ -0,0 +1,31 @@ +#pragma once + +#include "ITransport.h" +#include +#include +#include +#include +#include + +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& 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> m_frames; + std::uint64_t m_bytes = 0; + bool m_stop = false; + MobileGLResult m_result = MOBILEGL_OK; + std::thread m_reader; + }; +} diff --git a/MobileGL/MG_Remote/Transport/EventRing.h b/MobileGL/MG_Remote/Transport/EventRing.h index 3911f592..eb6ed1d2 100644 --- a/MobileGL/MG_Remote/Transport/EventRing.h +++ b/MobileGL/MG_Remote/Transport/EventRing.h @@ -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. diff --git a/MobileGL/MG_Remote/Transport/ILink.h b/MobileGL/MG_Remote/Transport/ILink.h index 6dbf2cbe..913d76b5 100644 --- a/MobileGL/MG_Remote/Transport/ILink.h +++ b/MobileGL/MG_Remote/Transport/ILink.h @@ -72,10 +72,17 @@ #include #include #include +#include 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* CmdHead = nullptr; + std::atomic* SubmittedSeq = nullptr; + std::atomic* ConsumerParked = nullptr; + std::atomic* ProducerParked = nullptr; + std::atomic* 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 CreateSharedLink(TransportRoleTag role); + MobileGLResult CreateStreamLink(int fd, const SessionSegmentSizes& sizes, + TransportRoleTag role, std::unique_ptr& out); + } // namespace MobileGL::MG_Remote::Transport diff --git a/MobileGL/MG_Remote/Transport/LinkMetrics.cpp b/MobileGL/MG_Remote/Transport/LinkMetrics.cpp new file mode 100644 index 00000000..149ef29a --- /dev/null +++ b/MobileGL/MG_Remote/Transport/LinkMetrics.cpp @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: LGPL-3.0-only +#include "LinkMetrics.h" +#include +#include +#include +#include +#include +#include +#if !defined(_WIN32) +#include +#endif + +namespace MobileGL::MG_Remote::Transport { +namespace { + struct Window { + std::uint64_t waitReplies = 0, samples = 0, replyNs = 0, stageBytes = 0; + std::array 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::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(w.histogram[i])); + if (n < 0 || static_cast(n) >= sizeof(histogram) - at) break; + at += static_cast(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(metrics.frame), + static_cast(w.waitReplies), static_cast(w.samples), + w.samples == 0 ? 0.0 : double(w.replyNs) / double(w.samples) / 1000.0, + static_cast(QuantileUpperUs(w, 50)), + static_cast(QuantileUpperUs(w, 99)), + static_cast(w.stageBytes), static_cast(wallNs), + static_cast(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; +} +} diff --git a/MobileGL/MG_Remote/Transport/LinkMetrics.h b/MobileGL/MG_Remote/Transport/LinkMetrics.h new file mode 100644 index 00000000..fde142ea --- /dev/null +++ b/MobileGL/MG_Remote/Transport/LinkMetrics.h @@ -0,0 +1,13 @@ +// P6.5 transport measurements. Called only by the single client producer. +#pragma once +#include + +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(); +} diff --git a/MobileGL/MG_Remote/Transport/ReplySlot.h b/MobileGL/MG_Remote/Transport/ReplySlot.h index 67574741..658d974d 100644 --- a/MobileGL/MG_Remote/Transport/ReplySlot.h +++ b/MobileGL/MG_Remote/Transport/ReplySlot.h @@ -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). diff --git a/MobileGL/MG_Remote/Transport/Ring.cpp b/MobileGL/MG_Remote/Transport/Ring.cpp index 3fd2f6a6..866ebd2e 100644 --- a/MobileGL/MG_Remote/Transport/Ring.cpp +++ b/MobileGL/MG_Remote/Transport/Ring.cpp @@ -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(static_cast(*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; diff --git a/MobileGL/MG_Remote/Transport/SessionRings.h b/MobileGL/MG_Remote/Transport/SessionRings.h index 00af0dcc..4dd1cc5c 100644 --- a/MobileGL/MG_Remote/Transport/SessionRings.h +++ b/MobileGL/MG_Remote/Transport/SessionRings.h @@ -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 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 diff --git a/MobileGL/MG_Remote/Transport/ShmLink.cpp b/MobileGL/MG_Remote/Transport/ShmLink.cpp new file mode 100644 index 00000000..406891d0 --- /dev/null +++ b/MobileGL/MG_Remote/Transport/ShmLink.cpp @@ -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 CreateSharedLink(TransportRoleTag role) { + auto link = std::make_unique(); + 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(&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(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 diff --git a/MobileGL/MG_Remote/Transport/ShmLink.h b/MobileGL/MG_Remote/Transport/ShmLink.h new file mode 100644 index 00000000..5d6502ea --- /dev/null +++ b/MobileGL/MG_Remote/Transport/ShmLink.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LGPL-3.0-only +#pragma once +#include "ILink.h" +#include "SessionRings.h" +#include +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 m_reply; + }; +} // namespace MobileGL::MG_Remote::Transport diff --git a/MobileGL/MG_Remote/Transport/ShmSegment.cpp b/MobileGL/MG_Remote/Transport/ShmSegment.cpp index 356009d8..61873491 100644 --- a/MobileGL/MG_Remote/Transport/ShmSegment.cpp +++ b/MobileGL/MG_Remote/Transport/ShmSegment.cpp @@ -17,6 +17,7 @@ #include "RoleMemory.h" #include "SessionRings.h" +#include #include @@ -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(SIZE_MAX)) { + Close(); return MOBILEGL_ERR_INVALID_ARGUMENT; + } + m_private[i] = ::operator new(static_cast(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(bytes[i])); + m_mappedBytes += bytes[i]; + } + m_cmdControl = static_cast(m_private[0]); + m_cmdRingBase = static_cast(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(m_private[3]); + m_eventSegmentBase = m_private[3]; + m_eventRingBase = static_cast(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 { diff --git a/MobileGL/MG_Remote/Transport/SocketTransport.cpp b/MobileGL/MG_Remote/Transport/SocketTransport.cpp index 5271dca7..11c46ec2 100644 --- a/MobileGL/MG_Remote/Transport/SocketTransport.cpp +++ b/MobileGL/MG_Remote/Transport/SocketTransport.cpp @@ -16,6 +16,10 @@ #include #include #include +#include +#include +#include +#include #if defined(__unix__) || defined(__APPLE__) || defined(__ANDROID__) # define MOBILEGL_SOCKET_TRANSPORT_POSIX 1 @@ -25,6 +29,10 @@ # include # include # include +# include +# include +# include +# include #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(address)->sin_addr.s_addr) >> 24) == 127; + if (address->sa_family == AF_INET6) + return IN6_IS_ADDR_LOOPBACK(&reinterpret_cast(address)->sin6_addr); + return false; + } + + bool TcpSocket(int fd) { + sockaddr_storage address{}; + socklen_t size = sizeof(address); + return ::getsockname(fd, reinterpret_cast(&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( + deadline - std::chrono::steady_clock::now()).count(); + pollfd pfd{fd, POLLOUT, 0}; + int rc; + do { rc = ::poll(&pfd, 1, static_cast(std::max(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(written), static_cast(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*, diff --git a/MobileGL/MG_Remote/Transport/SocketTransport.h b/MobileGL/MG_Remote/Transport/SocketTransport.h index 4f648311..d921f276 100644 --- a/MobileGL/MG_Remote/Transport/SocketTransport.h +++ b/MobileGL/MG_Remote/Transport/SocketTransport.h @@ -43,6 +43,7 @@ // calls it, the peer's reads return 0 and its sends fail. #pragma once +#include #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 m_closed{false}; bool m_failed = false; // framing violated: latched, never recovers }; diff --git a/MobileGL/MG_Remote/Transport/StreamLink.cpp b/MobileGL/MG_Remote/Transport/StreamLink.cpp new file mode 100644 index 00000000..130fae9d --- /dev/null +++ b/MobileGL/MG_Remote/Transport/StreamLink.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#if !defined(_WIN32) +#include +#include +#include +#if defined(__linux__) +#include +#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& 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 m_peer{false}; + }; + } // namespace + + struct StreamLink::Impl { + std::unique_ptr owned = std::make_unique(); + SessionSegments* memory = nullptr; + TransportRoleTag role = TransportRoleTag::ClientProducer; + int fd = -1; + std::atomic closed{false}, stopping{false}; + StreamBell consumer, producer; + std::thread reader; + std::mutex writeMutex, replyMutex; + std::condition_variable replyReady; + std::vector stage; + std::vector 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(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(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(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(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(size)); + h[8] = static_cast(kind); + Put64(h + 12, a); + Put64(h + 20, b); + return Write(h, sizeof h) && (!size || Write(payload, static_cast(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(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& 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(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 p(size); + if (size && !Read(p.data(), size)) return; + if (kind == Stage) { + std::memcpy(static_cast(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(Get64(v + 48)), std::memory_order_release); + C().eventDropped.store(static_cast(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 lock(replyMutex); + if (a <= replySeq) Corrupt("reply sequence moved backwards"); + replySeq = a; + replyStatus = static_cast(static_cast(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(); + 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& out) { + auto link = std::make_unique(); + 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(&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(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 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(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 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 lock(x.writeMutex); + return x.Send(Reply, seq, static_cast(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 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 diff --git a/MobileGL/MG_Remote/Transport/StreamLink.h b/MobileGL/MG_Remote/Transport/StreamLink.h index 4f7db242..d17d3428 100644 --- a/MobileGL/MG_Remote/Transport/StreamLink.h +++ b/MobileGL/MG_Remote/Transport/StreamLink.h @@ -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 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 m_impl; + }; } // namespace MobileGL::MG_Remote::Transport diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp index ad5e27ce..f1c12cdc 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -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 #include "PipeWireCodec.h" #include @@ -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(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(id), {const_cast(base), size}); + } + } + void SegmentTable::Install(SegmentId seg, SegmentView view) { if (seg == kSegNone || static_cast(seg) > static_cast(kSegAdopt)) { WireProtocolFatalAt("SegmentTable::Install", static_cast(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(seg), {offset, size}, &bytes) + == MOBILEGL_OK ? bytes : nullptr; + } const SegmentView& view = m_views[static_cast(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(m_stageCapacity), static_cast(m_stageHead - m_stageTail), static_cast( - 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(size)); const Uint64 offset = static_cast(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; } diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.h b/MobileGL/MG_Remote/Wire/PipeWireCodec.h index a824eb27..efab82a6 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.h +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.h @@ -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; diff --git a/MobileGL/MG_Test/Wire/CMakeLists.txt b/MobileGL/MG_Test/Wire/CMakeLists.txt index db5cc562..6fa7cdf6 100644 --- a/MobileGL/MG_Test/Wire/CMakeLists.txt +++ b/MobileGL/MG_Test/Wire/CMakeLists.txt @@ -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) diff --git a/MobileGL/MG_Test/Wire/ProtocolSmokeTest.cpp b/MobileGL/MG_Test/Wire/ProtocolSmokeTest.cpp index 8800677a..677515eb 100644 --- a/MobileGL/MG_Test/Wire/ProtocolSmokeTest.cpp +++ b/MobileGL/MG_Test/Wire/ProtocolSmokeTest.cpp @@ -118,6 +118,8 @@ TEST(ProtocolSmokeTest, UnionTagsAreFrozenWireValues) { EXPECT_EQ(static_cast(CtrlMsg::AuxRequest), 8); EXPECT_EQ(static_cast(CtrlMsg::Fatal), 9); EXPECT_EQ(static_cast(CtrlMsg::LogLine), 10); + EXPECT_EQ(static_cast(CtrlMsg::Refuse), 11); + EXPECT_EQ(static_cast(CtrlMsg::LogFlush), 12); EXPECT_EQ(static_cast(SegmentKind::Cmd), 1); EXPECT_EQ(static_cast(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); +} diff --git a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp index 58b9d78c..f26d7ca2 100644 --- a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp @@ -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); diff --git a/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp b/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp index db518ce6..c203ce67 100644 --- a/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp +++ b/MobileGL/MG_Test/Wire/SessionHandshakeTest.cpp @@ -39,6 +39,8 @@ #include "Includes.h" #include +#include +#include #include #include #include @@ -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(MG_Backend::kFormatCapabilityTargetCount)); - EXPECT_EQ(inputs.FormatCapabilityFormats, - static_cast(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(MG_Pipe::MGPWireOp::kOpCount)); - EXPECT_EQ(inputs.AbiVersion, static_cast(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(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 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 ReadHandshakeFrame(Transport::ITransport& transport) { + Uint64 bytes = 0; + if (transport.ReceiveFrame({nullptr, 0}, &bytes, 1000) != MOBILEGL_ERR_BUFFER_TOO_SMALL) return {}; + std::vector 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 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 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); } // --------------------------------------------------------------------------- diff --git a/MobileGL/MG_Test/Wire/SocketTransportTest.cpp b/MobileGL/MG_Test/Wire/SocketTransportTest.cpp index f5a7ff15..7af882d2 100644 --- a/MobileGL/MG_Test/Wire/SocketTransportTest.cpp +++ b/MobileGL/MG_Test/Wire/SocketTransportTest.cpp @@ -51,6 +51,8 @@ #if !defined(_WIN32) #include +#include +#include #include #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(&address), sizeof(address)), 0); + socklen_t addressSize = sizeof(address); + ASSERT_EQ(::getsockname(reservation, reinterpret_cast(&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 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) { diff --git a/MobileGL/MG_Test/Wire/StreamLinkTest.cpp b/MobileGL/MG_Test/Wire/StreamLinkTest.cpp new file mode 100644 index 00000000..47800bc5 --- /dev/null +++ b/MobileGL/MG_Test/Wire/StreamLinkTest.cpp @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: LGPL-3.0-only +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 + 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 bytes{1, 2, 3, 4, 5, 6, 7, 8}; + std::memcpy(static_cast(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 diff --git a/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp b/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp index fbe0e75b..68005585 100644 --- a/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp +++ b/MobileGL/MG_Test/Wire/SurfaceControlFrameTest.cpp @@ -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) { diff --git a/MobileGL/MG_Util/Debug/Log.cpp b/MobileGL/MG_Util/Debug/Log.cpp index dfe1bccc..fff43470 100644 --- a/MobileGL/MG_Util/Debug/Log.cpp +++ b/MobileGL/MG_Util/Debug/Log.cpp @@ -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 lock(LogMutex()); + s_forwarder = forwarder; + s_forwarderUser = user; + } + + void WithLogBarrier(void (*action)(void*), void* user) { + std::lock_guard lock(LogMutex()); + action(user); + } + + void WritePeerLog(const char* message) { + std::lock_guard 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); } diff --git a/MobileGL/MG_Util/Debug/Log.h b/MobileGL/MG_Util/Debug/Log.h index b89a9325..d02ca02c 100644 --- a/MobileGL/MG_Util/Debug/Log.h +++ b/MobileGL/MG_Util/Debug/Log.h @@ -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 diff --git a/android-plugin/app/src/trace/AndroidManifest.xml b/android-plugin/app/src/trace/AndroidManifest.xml index a160bab1..bfb628a2 100644 --- a/android-plugin/app/src/trace/AndroidManifest.xml +++ b/android-plugin/app/src/trace/AndroidManifest.xml @@ -2,7 +2,17 @@ + + + + + + diff --git a/android-plugin/app/src/trace/java/top/mobilegl/plugin/MobileGLServerService.java b/android-plugin/app/src/trace/java/top/mobilegl/plugin/MobileGLServerService.java new file mode 100644 index 00000000..7285c7e6 --- /dev/null +++ b/android-plugin/app/src/trace/java/top/mobilegl/plugin/MobileGLServerService.java @@ -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 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 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(); + } +} diff --git a/scripts/ci/junit_tally.py b/scripts/ci/junit_tally.py index c2df08eb..2f727f00 100755 --- a/scripts/ci/junit_tally.py +++ b/scripts/ci/junit_tally.py @@ -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 [" + " | ".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 diff --git a/scripts/ci/p65_link_metrics.py b/scripts/ci/p65_link_metrics.py new file mode 100644 index 00000000..b006b919 --- /dev/null +++ b/scripts/ci/p65_link_metrics.py @@ -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()) diff --git a/scripts/ci/spawn_lane_parity.py b/scripts/ci/spawn_lane_parity.py index 887a4662..958739a9 100644 --- a/scripts/ci/spawn_lane_parity.py +++ b/scripts/ci/spawn_lane_parity.py @@ -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__": diff --git a/scripts/ci/tcp_lane_tools_test.py b/scripts/ci/tcp_lane_tools_test.py new file mode 100644 index 00000000..625c3d3a --- /dev/null +++ b/scripts/ci/tcp_lane_tools_test.py @@ -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'') + 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() diff --git a/scripts/ci/tcp_log_forwarding_control.py b/scripts/ci/tcp_log_forwarding_control.py new file mode 100644 index 00000000..906d5ba0 --- /dev/null +++ b/scripts/ci/tcp_log_forwarding_control.py @@ -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()) diff --git a/scripts/ci/tcp_server_fixture.py b/scripts/ci/tcp_server_fixture.py new file mode 100644 index 00000000..0b30a3a8 --- /dev/null +++ b/scripts/ci/tcp_server_fixture.py @@ -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() diff --git a/scripts/ci/tcp_supervisor_smoke.py b/scripts/ci/tcp_supervisor_smoke.py new file mode 100644 index 00000000..1b527f7a --- /dev/null +++ b/scripts/ci/tcp_supervisor_smoke.py @@ -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() diff --git a/scripts/gen_pipe.py b/scripts/gen_pipe.py index 4db7c34e..562ca3bc 100644 --- a/scripts/gen_pipe.py +++ b/scripts/gen_pipe.py @@ -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(MGPWireOp::%s), static_cast(%s), static_cast(%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) diff --git a/tools/trace_replay/CMakeLists.txt b/tools/trace_replay/CMakeLists.txt index 12c3a7b1..17388b81 100644 --- a/tools/trace_replay/CMakeLists.txt +++ b/tools/trace_replay/CMakeLists.txt @@ -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() diff --git a/tools/trace_replay/run_trace_case.cmake b/tools/trace_replay/run_trace_case.cmake index 6ce7bd7f..71ebe954 100644 --- a/tools/trace_replay/run_trace_case.cmake +++ b/tools/trace_replay/run_trace_case.cmake @@ -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 " diff --git a/tools/trace_replay/tcp_device_server.py b/tools/trace_replay/tcp_device_server.py new file mode 100644 index 00000000..4afccad3 --- /dev/null +++ b/tools/trace_replay/tcp_device_server.py @@ -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://:40613') + + +if __name__ == '__main__': + main()