mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-14 21:43:24 +09:00
Breaks the one process = one server = one session = one context equation
that the whole stack's correctness used to rest on. Design and per-slice
shape: docs/Disaggregated/design/11-state-ownership.md.
S1 wire: bind_context ring record (opcode 84) announces the current
context at the make-current edge; CreateContext/DestroyContext join the
SurfaceOp control frames; control revision 5 -> 6; the server keeps a
per-session token table and refuses unknown tokens by name.
S2 server: SessionRuntime per session with thread-scoped resolution
(singletons become fallbacks), per-thread gPipeInputs, per-session latch
domains, per-thread segment resolvers; the in-process unix supervisor
serves up to MOBILEGL_IPC_INPROC_MAX_SESSIONS (16) concurrent sessions;
surface mode is declared per session and the display lease refuses a
second on-screen session by name.
S3 frontend: ShareGroupState owns the shared object stores and name
generators (buffer/texture/renderbuffer/sampler/program), GLContext keeps
per-context bindings/errors/container objects; pGLContext becomes
thread-local with a process default context preserving the single-context
world verbatim; eglCreateContext builds a real GLContext and joins the
sharing context's group.
S4 backend: native EGL tuples keyed by (session, context token);
InitDisplayAndContext's unconditional teardown is gone - creating a
second context or surface no longer destroys the first; eglCreateContext
sharing is wired to the native driver; bind_context is the native switch
point.
S5 applier: MGPipeApplierState splits into MGPipeObjectRecords (per
share group) and MGPipeWorkingState (per context), resolved through a
{session, token} registry with a thread-local cached fast path.
Tests: MultiSessionTest (incl. two sessions x two contexts interleaved
with real clear/readback witnesses), ApplierOwnershipTest (with a
null-resolver negative control), ContextStateOwnershipTest,
ServerLoopEglTest native-context liveness; 99/99 of the acceptance set
green on the host build.
Known debts (documented in the design doc): twin registries still keyed
by handle alone, no per-record ContextSlot, process-global backend
generations, MakeCurrent dedup per current binding.
426 lines
20 KiB
C++
426 lines
20 KiB
C++
// MobileGL - MobileGL/MG_Test/Wire/P12ServerRig.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
|
|
|
|
// P12 (on-screen server window): the two-process rig InProcessServerTest and ServerOwnedSurfaceTest
|
|
// share - a server (the exec'd `--serve` supervisor, or a forked process running the IN-PROCESS
|
|
// server entry on a thread), forked peers that talk to it over loopback TCP, and the healthy
|
|
// session PeerLatchTest's second session runs (a record applied and, with headless EGL, a green clear
|
|
// read back). The shapes are PeerLatchTest's, trimmed to what these two suites need; see there for
|
|
// the reasons behind each (why peers are forks, why a failing case keeps its logs).
|
|
//
|
|
// Header-only and everything in an anonymous namespace: each suite is one translation unit.
|
|
|
|
#pragma once
|
|
|
|
#include <MG_Remote/Client/ClientSession.h>
|
|
#include <MG_Remote/FatalFunnel.h>
|
|
#include <MG_Remote/Protocol/generated/protocol_generated.h>
|
|
#include <MG_Remote/Server/ServerLoop.h>
|
|
#include <MG_Remote/Server/ServerSpawn.h>
|
|
#include <MG_Remote/Transport/ILink.h>
|
|
#include <MG_Remote/Wire/PipeWireCodec.h>
|
|
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
|
#include <MG_Pipe/PipeApply.h>
|
|
#include <MG_Util/Debug/Log.h>
|
|
#include <Config.h>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <chrono>
|
|
#include <csignal>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include <arpa/inet.h>
|
|
#include <netinet/in.h>
|
|
#include <poll.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
namespace {
|
|
|
|
namespace P12 {
|
|
namespace P = MobileGL::MG_Pipe;
|
|
namespace Remote = MobileGL::MG_Remote;
|
|
namespace Debug = MobileGL::MG_Util::Debug;
|
|
using MobileGL::Int32;
|
|
using MobileGL::Uint32;
|
|
using MobileGL::Uint64;
|
|
using MobileGL::Uint8;
|
|
|
|
inline std::string ServerImage() {
|
|
if (const char* explicitPath = std::getenv("MOBILEGL_TEST_SERVER_PATH")) return explicitPath;
|
|
return "libMobileGLServer.so";
|
|
}
|
|
|
|
// The server (and a server process forked from this one) inherits this; its make-current
|
|
// needs a headless EGL the way ServerLoopEglTest's does.
|
|
inline void PinHeadlessEgl() {
|
|
if (std::getenv("EGL_PLATFORM") == nullptr) ::setenv("EGL_PLATFORM", "surfaceless", 1);
|
|
if (std::getenv("__EGL_VENDOR_LIBRARY_FILENAMES") == nullptr) {
|
|
const char* mesa = "/usr/share/glvnd/egl_vendor.d/50_mesa.json";
|
|
std::error_code ec;
|
|
if (std::filesystem::exists(mesa, ec)) ::setenv("__EGL_VENDOR_LIBRARY_FILENAMES", mesa, 1);
|
|
}
|
|
::unsetenv("DISPLAY");
|
|
::unsetenv("WAYLAND_DISPLAY");
|
|
}
|
|
|
|
inline std::string ReadFile(const std::string& path) {
|
|
std::ifstream in(path, std::ios::binary);
|
|
std::stringstream all;
|
|
all << in.rdbuf();
|
|
return all.str();
|
|
}
|
|
|
|
inline int FreeLoopbackPort() {
|
|
const int fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
|
if (fd < 0) return -1;
|
|
sockaddr_in addr{};
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
addr.sin_port = 0;
|
|
socklen_t len = sizeof(addr);
|
|
int port = -1;
|
|
if (::bind(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == 0 &&
|
|
::getsockname(fd, reinterpret_cast<sockaddr*>(&addr), &len) == 0) {
|
|
port = ntohs(addr.sin_port);
|
|
}
|
|
::close(fd);
|
|
return port;
|
|
}
|
|
|
|
inline bool WaitFor(const std::function<bool()>& predicate, int timeoutMs) {
|
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
|
|
while (std::chrono::steady_clock::now() < deadline) {
|
|
if (predicate()) return true;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
|
}
|
|
return predicate();
|
|
}
|
|
|
|
inline Uint32 Count(const std::string& haystack, const std::string& needle) {
|
|
Uint32 n = 0;
|
|
for (auto at = haystack.find(needle); at != std::string::npos; at = haystack.find(needle, at + 1)) ++n;
|
|
return n;
|
|
}
|
|
|
|
// A server process this test owns: its pid, its tcp:// endpoint, and the log base its roles
|
|
// write under. Stopped with SIGTERM (then SIGKILL) and reaped; the logs are removed unless the
|
|
// case failed - then they are the evidence the messages quote.
|
|
struct ServerProcess {
|
|
pid_t pid = -1;
|
|
std::string endpoint;
|
|
std::string logBase;
|
|
int exitStatus = -1; // waitpid status once reaped
|
|
|
|
std::string Log() const { return ReadFile(Debug::RoleLogPath(logBase.c_str(), Debug::LogRole::Server)); }
|
|
|
|
// Waits for the process to end on its own, up to `timeoutMs`.
|
|
bool WaitExit(int timeoutMs) {
|
|
if (pid <= 0) return true;
|
|
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
|
|
for (;;) {
|
|
int status = 0;
|
|
const pid_t got = ::waitpid(pid, &status, WNOHANG);
|
|
if (got == pid) {
|
|
exitStatus = status;
|
|
pid = -1;
|
|
return true;
|
|
}
|
|
if (got < 0) {
|
|
pid = -1;
|
|
return true;
|
|
}
|
|
if (std::chrono::steady_clock::now() >= deadline) return false;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
}
|
|
}
|
|
void Stop() {
|
|
if (pid <= 0) return;
|
|
::kill(pid, SIGTERM);
|
|
if (!WaitExit(10000)) {
|
|
::kill(pid, SIGKILL);
|
|
(void)WaitExit(5000);
|
|
}
|
|
}
|
|
~ServerProcess() {
|
|
Stop();
|
|
if (logBase.empty() || ::testing::Test::HasFailure()) return;
|
|
std::error_code ec;
|
|
std::filesystem::remove(Debug::RoleLogPath(logBase.c_str(), Debug::LogRole::Server), ec);
|
|
std::filesystem::remove(Debug::RoleLogPath(logBase.c_str(), Debug::LogRole::Client), ec);
|
|
std::filesystem::remove(logBase, ec);
|
|
}
|
|
};
|
|
|
|
// The exec'd `--serve` supervisor (the fork-per-session shape), on a fresh loopback port.
|
|
inline bool LaunchSupervisor(const std::string& label, ServerProcess* server) {
|
|
server->logBase = "/tmp/mgl-p12-" + label + "-" + std::to_string(::getpid()) + ".log";
|
|
::setenv("MOBILEGL_LOG_FILE_PATH", server->logBase.c_str(), 1);
|
|
PinHeadlessEgl();
|
|
for (int attempt = 0; attempt < 5; ++attempt) {
|
|
const int port = FreeLoopbackPort();
|
|
if (port <= 0) return false;
|
|
server->endpoint = "tcp://127.0.0.1:" + std::to_string(port);
|
|
Debug::TruncateRoleLogs(server->logBase.c_str());
|
|
Remote::Server::LaunchedServer launched;
|
|
if (Remote::Server::LaunchServerWithArgs(ServerImage(), server->endpoint, {"--serve"}, &launched) !=
|
|
MOBILEGL_OK)
|
|
return false;
|
|
server->pid = launched.pid;
|
|
bool listening = false;
|
|
(void)WaitFor(
|
|
[&] {
|
|
if (server->Log().find("listening on") != std::string::npos) return listening = true;
|
|
return server->WaitExit(0) && server->pid <= 0;
|
|
},
|
|
10000);
|
|
if (listening) return true;
|
|
if (server->pid > 0) return false; // alive and silent: not a port race
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------------------
|
|
// Forked peers (PeerLatchTest's RunPeer, trimmed)
|
|
// ------------------------------------------------------------------------------------
|
|
|
|
struct PeerReport {
|
|
Int32 started = 0; // handshake answered Welcome
|
|
Uint32 serverPid = 0; // Welcome::serverPid
|
|
Int32 forged = 0; // the peer's bad bytes went out
|
|
Int32 applied = 0; // a record was applied
|
|
Int32 egl = 0; // headless EGL came up in the session
|
|
Int32 status = -99; // the read-back's reply status
|
|
Uint32 pixel = 0; // the read-back texel
|
|
char note[256] = {};
|
|
};
|
|
|
|
inline void Note(PeerReport& r, const char* text) { std::snprintf(r.note, sizeof(r.note), "%s", text); }
|
|
|
|
using PeerBody = std::function<void(Remote::Client::ClientSession&, PeerReport&)>;
|
|
|
|
// Runs `body` in a forked peer connected over TCP to `endpoint`. `holdMs` > 0: the peer reports
|
|
// once `body` returns and then holds its connections open and silent that long; the caller
|
|
// gets its pid in `heldPeer` and owes it ReleaseHeldPeer. `before` runs in the peer first (a
|
|
// config it must differ in).
|
|
inline PeerReport RunPeer(const std::string& endpoint, const PeerBody& body, bool stopCleanly,
|
|
int timeoutMs = 30000, int holdMs = 0, pid_t* heldPeer = nullptr,
|
|
const std::function<void()>& before = {}) {
|
|
PeerReport report{};
|
|
int fds[2] = {-1, -1};
|
|
if (::pipe(fds) != 0) {
|
|
Note(report, "pipe failed");
|
|
return report;
|
|
}
|
|
std::fflush(nullptr);
|
|
const pid_t pid = ::fork();
|
|
if (pid == 0) {
|
|
::close(fds[0]);
|
|
if (before) before();
|
|
PeerReport r{};
|
|
auto& client = Remote::Client::ClientSessionInstance();
|
|
MobileGL::MG_Config::Ipc.Control = endpoint.c_str();
|
|
MobileGL::MG_Config::Ipc.Data = "auto";
|
|
const MobileGLResult started = client.StartSpawned();
|
|
r.started = started == MOBILEGL_OK ? 1 : 0;
|
|
r.serverPid = client.PeerServerPid();
|
|
if (r.started) body(client, r);
|
|
else std::snprintf(r.note, sizeof(r.note), "start rc=%d", static_cast<int>(started));
|
|
if (stopCleanly && r.started) client.Stop();
|
|
const ssize_t wrote = ::write(fds[1], &r, sizeof(r));
|
|
(void)wrote;
|
|
std::fflush(nullptr);
|
|
if (holdMs > 0) std::this_thread::sleep_for(std::chrono::milliseconds(holdMs));
|
|
::_exit(0);
|
|
}
|
|
::close(fds[1]);
|
|
if (pid < 0) {
|
|
::close(fds[0]);
|
|
Note(report, "fork failed");
|
|
return report;
|
|
}
|
|
pollfd pfd{fds[0], POLLIN, 0};
|
|
const int ready = ::poll(&pfd, 1, timeoutMs);
|
|
if (ready > 0) {
|
|
const ssize_t got = ::read(fds[0], &report, sizeof(report));
|
|
if (got != static_cast<ssize_t>(sizeof(report))) {
|
|
report = PeerReport{};
|
|
Note(report, "peer died before reporting");
|
|
}
|
|
} else {
|
|
Note(report, "peer timed out");
|
|
::kill(pid, SIGKILL);
|
|
}
|
|
::close(fds[0]);
|
|
if (holdMs > 0 && heldPeer != nullptr && ready > 0) {
|
|
*heldPeer = pid;
|
|
return report;
|
|
}
|
|
int status = 0;
|
|
::waitpid(pid, &status, 0);
|
|
return report;
|
|
}
|
|
|
|
inline void ReleaseHeldPeer(pid_t peer) {
|
|
if (peer <= 0) return;
|
|
::kill(peer, SIGKILL);
|
|
int status = 0;
|
|
::waitpid(peer, &status, 0);
|
|
}
|
|
|
|
// A control frame straight onto the control connection: the SurfaceOp latch rows' carrier.
|
|
inline bool SendSurfaceOp(Remote::Client::ClientSession& client, PeerReport& r, Uint8 kind, Uint8 windowKind,
|
|
Uint64 token) {
|
|
flatbuffers::FlatBufferBuilder builder(256);
|
|
const auto op = ::MobileGL::Wire::CreateSurfaceOp(
|
|
builder, /*seq=*/4242, static_cast<::MobileGL::Wire::SurfaceOpKind>(kind), /*display=*/1,
|
|
/*surface=*/1, static_cast<::MobileGL::Wire::WindowKind>(windowKind), token, 16, 16, 0, 1, 1);
|
|
const auto envelope =
|
|
::MobileGL::Wire::CreateCtrlEnvelope(builder, ::MobileGL::Wire::CtrlMsg::SurfaceOp, op.Union());
|
|
::MobileGL::Wire::FinishCtrlEnvelopeBuffer(builder, envelope);
|
|
Remote::Transport::ITransport* control = client.Control_Plane();
|
|
if (control == nullptr ||
|
|
control->SendFrame(MobileGLByteSpan{builder.GetBufferPointer(), builder.GetSize()}) != MOBILEGL_OK) {
|
|
Note(r, "the control frame could not be sent");
|
|
return false;
|
|
}
|
|
r.forged = 1;
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
|
return true;
|
|
}
|
|
|
|
inline bool Emit(Remote::Client::ClientSession& client, P::MGPWireOp op, const void* payload, Uint64 bytes,
|
|
const void* tail = nullptr, Uint64 tailBytes = 0) {
|
|
Int32 status = Remote::Wire::ReplySink::kStatusError;
|
|
const Uint64 seq = client.EmitAndWait(op, payload, bytes, tail, tailBytes, nullptr, 0, &status);
|
|
return seq != Remote::Wire::kInvalidSeq &&
|
|
client.WaitForApplied(seq, 5000) == Remote::Transport::SessionWait::Reached;
|
|
}
|
|
|
|
// The client's virtual EGL handles (EGLImpl mints small integers; ServerLoopEglTest's shape).
|
|
inline EGLDisplay Dpy() { return reinterpret_cast<EGLDisplay>(static_cast<std::uintptr_t>(0x1)); }
|
|
inline EGLSurface Surf() { return reinterpret_cast<EGLSurface>(static_cast<std::uintptr_t>(0x1)); }
|
|
inline EGLContext Ctx() { return reinterpret_cast<EGLContext>(static_cast<std::uintptr_t>(0x1)); }
|
|
|
|
inline bool BringUpEgl(PeerReport& r) {
|
|
EGLint major = -1;
|
|
EGLint minor = -1;
|
|
if (!Remote::Server::ServerInitializeEGLDisplay(Dpy(), &major, &minor)) return Note(r, "no EGL display"), false;
|
|
if (!Remote::Server::ServerCreateEGLPbufferSurface(Surf(), 8, 8)) return Note(r, "no pbuffer"), false;
|
|
if (!Remote::Server::ServerMakeEGLCurrent(Dpy(), Surf(), Surf(), Ctx())) return Note(r, "no make-current"), false;
|
|
r.egl = 1;
|
|
return true;
|
|
}
|
|
|
|
// A HEALTHY SESSION: one record applied and, with headless EGL, a clear read back -
|
|
// PeerLatchTest's session B, verbatim in what it sends. P14 S2: the clear's colour is
|
|
// HealthySessionWithColor's parameter so a suite that runs several sessions can tell whose
|
|
// read-back it is reading; HealthySession keeps the two-argument shape every existing
|
|
// caller takes its address of.
|
|
//
|
|
// THE PACKING IS THE READ-BACK'S OWN, A<<24|B<<16|G<<8|R - a GL_RGBA/GL_UNSIGNED_BYTE
|
|
// texel little-endian in a Uint32, which is how PeerReport::pixel comes back and how every
|
|
// expectation on it is written (green (0,1,0,1) is 0xFF00FF00). Decoding it in the other
|
|
// order would clear the complement of the colour the caller asked for.
|
|
inline void HealthySessionWithColor(Remote::Client::ClientSession& client, PeerReport& r,
|
|
Uint32 rgba) {
|
|
P::MGPMemoryBarrier barrier{};
|
|
barrier.Bits = 0x2000u;
|
|
r.applied = Emit(client, P::MGPWireOp::MemoryBarrier, &barrier, sizeof(barrier)) ? 1 : 0;
|
|
if (!BringUpEgl(r)) return;
|
|
P::MGPFramebufferState framebuffer{};
|
|
framebuffer.Fbo = P::kMGPipeDefaultFramebuffer;
|
|
framebuffer.Target = static_cast<Uint8>(P::MGPipeFramebufferTarget::Both);
|
|
framebuffer.IsDefault = 1;
|
|
framebuffer.Complete = 1;
|
|
framebuffer.Width = framebuffer.Height = 8;
|
|
framebuffer.Layers = framebuffer.Samples = 1;
|
|
framebuffer.FixedSampleLocations = 1;
|
|
for (auto& drawBuffer : framebuffer.DrawBuffers) drawBuffer = -1;
|
|
framebuffer.DrawBuffers[0] = 0;
|
|
framebuffer.ContentHash = 1;
|
|
P::MGPPixelPackState pack{};
|
|
pack.Pack.Alignment = 4;
|
|
P::MGPSamplerViews views{};
|
|
views.Count = 1;
|
|
const P::MGPBoundView unboundView{};
|
|
P::MGPSamplerStates samplers{};
|
|
samplers.Count = 1;
|
|
const P::MGPipeHandle unboundSampler = P::kMGPipeNullHandle;
|
|
const P::MGPContextValues context{};
|
|
MobileGL::RenderStateParameters params{};
|
|
for (auto& mask : params.ColorMasks) mask = MobileGL::BoolVec4(true, true, true, true);
|
|
std::vector<Uint8> pipelineBytes(P::kMGPipePipelineChunkBytes, 0);
|
|
P::MGPipeGatherPipelineBytes(params, pipelineBytes.data());
|
|
P::MGPRenderStateDesc cso{};
|
|
cso.Cso = P::MGPipeHandle{P::kMGPipeFirstAllocatableSlot, 1u};
|
|
cso.BaseCso = P::kMGPipeNullHandle;
|
|
cso.ChunkMask = P::MGPipeRenderStateChunkDetail::kAllPipelineHalfBits;
|
|
cso.Blob = client.Encoder().StageBytes(pipelineBytes.data(), pipelineBytes.size());
|
|
P::MGPBindRenderState bindCso{};
|
|
bindCso.Cso = cso.Cso;
|
|
bindCso.Version = 1;
|
|
bindCso.PipelineVersion = 1;
|
|
if (!Emit(client, P::MGPWireOp::CreateRenderState, &cso, sizeof(cso)) ||
|
|
!Emit(client, P::MGPWireOp::BindRenderState, &bindCso, sizeof(bindCso)))
|
|
return Note(r, "the render state was not applied");
|
|
if (!Emit(client, P::MGPWireOp::SetFramebufferState, &framebuffer, sizeof(framebuffer)) ||
|
|
!Emit(client, P::MGPWireOp::SetPixelPackState, &pack, sizeof(pack)) ||
|
|
!Emit(client, P::MGPWireOp::SetSamplerViews, &views, sizeof(views), &unboundView, sizeof(unboundView)) ||
|
|
!Emit(client, P::MGPWireOp::BindSamplerStates, &samplers, sizeof(samplers), &unboundSampler,
|
|
sizeof(unboundSampler)) ||
|
|
!Emit(client, P::MGPWireOp::SetContextValues, &context, sizeof(context)))
|
|
return Note(r, "the default framebuffer's inputs were not applied");
|
|
P::MGPClear clear{};
|
|
clear.Fbo = P::kMGPipeNullHandle;
|
|
clear.Kind = P::kMGPipeClearKindColor;
|
|
clear.DrawBufferIndex = 0;
|
|
clear.BufferMask = GL_COLOR_BUFFER_BIT;
|
|
clear.ValueClass = P::kMGPipeClearValueClassFloat;
|
|
const float color[4] = {static_cast<float>(rgba & 0xFFu) / 255.0f,
|
|
static_cast<float>((rgba >> 8) & 0xFFu) / 255.0f,
|
|
static_cast<float>((rgba >> 16) & 0xFFu) / 255.0f,
|
|
static_cast<float>((rgba >> 24) & 0xFFu) / 255.0f};
|
|
std::memcpy(clear.ColorValue, color, sizeof(color));
|
|
if (!Emit(client, P::MGPWireOp::Clear, &clear, sizeof(clear))) return Note(r, "clear not applied");
|
|
P::MGPReadbackInfo read{};
|
|
read.Res = P::kMGPipeNullHandle;
|
|
read.Box = P::MGPBox{0, 0, 0, 1, 1, 1};
|
|
read.Format = GL_RGBA;
|
|
read.Type = GL_UNSIGNED_BYTE;
|
|
read.DstSize = 4;
|
|
Uint8 texel[4] = {};
|
|
Int32 status = Remote::Wire::ReplySink::kStatusError;
|
|
Uint64 replySize = 0;
|
|
const Uint64 seq = client.EmitAndWait(P::MGPWireOp::ReadPixels, &read, sizeof(read), nullptr, 0, texel,
|
|
sizeof(texel), &status, &replySize);
|
|
r.status = status;
|
|
if (seq == Remote::Wire::kInvalidSeq || replySize != sizeof(texel)) return Note(r, "no read-back");
|
|
std::memcpy(&r.pixel, texel, sizeof(r.pixel));
|
|
}
|
|
|
|
// The green every existing caller asked for, with the two-argument signature a
|
|
// `std::function<void(ClientSession&, PeerReport&)>` can be built from.
|
|
inline void HealthySession(Remote::Client::ClientSession& client, PeerReport& r) {
|
|
HealthySessionWithColor(client, r, 0xFF00FF00u);
|
|
}
|
|
} // namespace P12
|
|
|
|
} // namespace
|