From 70df57bb236f12a18e7e863f8d13d5eb257b8cd2 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 20 Sep 2026 11:52:13 -0400 Subject: [PATCH] [MG_Remote] (Disaggregated): carry native query results and complete XFB object deletion --- .../MG_Backend/DirectVulkan/DirectVulkan.cpp | 9 +- MobileGL/MG_Backend/Init.cpp | 16 ++ MobileGL/MG_Pipe/FieldOwnership.def | 11 +- MobileGL/MG_Pipe/PipeCalls.def | 6 +- .../MG_Pipe/generated/PipeFieldOwnership.inc | 11 +- MobileGL/MG_Pipe/generated/PipeTables.inc | 7 +- MobileGL/MG_Pipe/generated/PipeThunks.inc | 4 + MobileGL/MG_Pipe/generated/PipeWire.inc | 17 +- MobileGL/MG_Remote/Client/EmitTables.cpp | 75 +++++--- MobileGL/MG_Remote/Client/QueryEmit.inc | 104 +++++++++++ MobileGL/MG_Remote/Server/PipeApplier.cpp | 84 +++++++-- MobileGL/MG_Remote/Server/PipeApplier.h | 24 +++ MobileGL/MG_Remote/Server/QueryServer.inc | 171 ++++++++++++++++++ MobileGL/MG_Remote/Server/ServerSession.cpp | 5 +- MobileGL/MG_Remote/Wire/PipeWireCodec.cpp | 41 ++++- MobileGL/MG_Remote/Wire/PipeWireCodec.h | 19 ++ MobileGL/MG_State/GLState/Core.cpp | 7 + MobileGL/MG_State/GLState/Core.h | 3 + MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp | 2 +- MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp | 125 +++++++++++++ MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 16 +- 21 files changed, 686 insertions(+), 71 deletions(-) create mode 100644 MobileGL/MG_Remote/Client/QueryEmit.inc create mode 100644 MobileGL/MG_Remote/Server/QueryServer.inc diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index f63d5a7e..5da0ca22 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -1422,7 +1422,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { return false; } if (query->kind == VulkanTimerQuery::Kind::XfbGenerated && - !query->pausedPrimitivesCountedByGpu && MGB_CTX_LIVE) { + !query->pausedPrimitivesCountedByGpu && +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Config::Transport == MG_Config::TransportMode::Monolith && +#endif + MGB_CTX_LIVE) { primitives += MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() - query->pausedPrimitiveSnapshot; } @@ -1470,6 +1474,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { query->kind = generated ? VulkanTimerQuery::Kind::XfbGenerated : VulkanTimerQuery::Kind::XfbWritten; query->rendererGeneration = GetRendererGeneration(); query->pausedPrimitiveSnapshot = +#if MOBILEGL_BUILD_DISAGGREGATED + MG_Config::Transport == MG_Config::TransportMode::Monolith && +#endif MGB_CTX_LIVE ? MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() : 0; // Read AFTER StartXfbQueryCapture, which is where a failed reroute-pool creation // disarms: the answer is then what this span will actually do for every draw. diff --git a/MobileGL/MG_Backend/Init.cpp b/MobileGL/MG_Backend/Init.cpp index 1db6bbcc..b2c3299c 100644 --- a/MobileGL/MG_Backend/Init.cpp +++ b/MobileGL/MG_Backend/Init.cpp @@ -187,6 +187,22 @@ namespace MobileGL::MG_Backend { serverBackend->GetBackendFunctions().GL.EndTransformFeedback != nullptr) { capBits |= MG_Pipe::kCapBackendOwnsXfbCapture; } + if (const auto* backend = loop.Backend()) { + const auto& gl = backend->GetBackendFunctions().GL; + // The query owner publishes only complete native query/reply paths. + // Timer hardware support is refreshed when caps are published after + // make-current; no CPU primitive-accounting preference is advertised. + if (gl.IsQueryResultAvailable && gl.GetQueryResult64 && gl.DeleteBackendQuery && + gl.FenceSync && gl.ClientWaitSync && gl.DeleteSync) { + if (gl.BeginXfbPrimitivesQuery && gl.EndXfbPrimitivesQuery) + capBits |= MG_Pipe::kCapXfbPrimitivesQuery; + if (gl.BeginOcclusionQuery && gl.EndOcclusionQuery) + capBits |= MG_Pipe::kCapOcclusionQuery; + if (gl.IsTimerQuerySupported && gl.BeginTimeElapsedQuery && + gl.EndTimeElapsedQuery && gl.QueryCounterTimestamp) + capBits |= MG_Pipe::kCapTimerQuery; + } + } // Each backend has an independent implementation-readiness gate. // The runtime RunAhead knob can decline the feature, never create it. const Bool runAheadReady = MG_Config::ActiveBackendType == BackendType::DirectVulkan diff --git a/MobileGL/MG_Pipe/FieldOwnership.def b/MobileGL/MG_Pipe/FieldOwnership.def index 30c9bc75..4aaddde4 100644 --- a/MobileGL/MG_Pipe/FieldOwnership.def +++ b/MobileGL/MG_Pipe/FieldOwnership.def @@ -154,7 +154,16 @@ X(FenceStatus, GetSyncStatus) \ X(FenceWait, ClientWaitSync) \ X(FenceDestroy, DeleteSync) \ - X(FenceWaitServer, WaitSync) + X(FenceWaitServer, WaitSync) \ + X(QueryCreate, BeginXfbPrimitivesQuery) \ + X(QueryBegin, BeginXfbPrimitivesQuery) \ + X(QueryEnd, EndXfbPrimitivesQuery) \ + X(QueryAvailable, IsQueryResultAvailable) \ + X(QueryResult, GetQueryResult64) \ + X(QueryDestroy, DeleteBackendQuery) \ + X(QueryCounter, QueryCounterTimestamp) \ + X(QueryTimestamp, GetGpuTimestampNs) \ + X(DeleteStreamOutput, DeleteTransformFeedback) // X(Op, Why) - verb-shaped calls that are deliberately NOT stamp points. // diff --git a/MobileGL/MG_Pipe/PipeCalls.def b/MobileGL/MG_Pipe/PipeCalls.def index c8b1b808..a133e143 100644 --- a/MobileGL/MG_Pipe/PipeCalls.def +++ b/MobileGL/MG_Pipe/PipeCalls.def @@ -142,7 +142,7 @@ // sat catalogued-and-dead since P4a. 80 unique records. // --------------------------------------------------------------------------------------- -#define MGP_CALL_LIST_DOCUMENTED_COUNT 80 +#define MGP_CALL_LIST_DOCUMENTED_COUNT 81 // clang-format off #define MGP_CALL_LIST(X) \ @@ -337,7 +337,9 @@ /* ---- the same validate point: a rebuild inside the verb must already see the bindings. ---- */ \ /* ---- kWaitNone: nothing about it is read back, and under run-ahead a kCtxState row that ---- */ \ /* ---- names only handles and values is exactly what rule F admits. ---- */ \ - X(SetProgramBindings, MGPProgramBindings, kCtxState, kVarTail|kHostSpan, kWaitNone) + X(SetProgramBindings, MGPProgramBindings, kCtxState, kVarTail|kHostSpan, kWaitNone) \ + /* Deletion carries the GL argument and its never-reused capture identity. */ \ + X(DeleteStreamOutput, MGPStreamOutputBind, kCtxVerb, kNone, kWaitNone) // clang-format on // Explicitly NOT migrated (plan 4.4.6 / appendix A "explicit deletions"): diff --git a/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc b/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc index fbf256e1..c1065fa5 100644 --- a/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc +++ b/MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc @@ -292,12 +292,21 @@ constexpr MGPipeVerb MGPipeVerbForWireOp(MGPWireOp op) { case MGPWireOp::FenceWait: return MGPipeVerb::ClientWaitSync; case MGPWireOp::FenceDestroy: return MGPipeVerb::DeleteSync; case MGPWireOp::FenceWaitServer: return MGPipeVerb::WaitSync; + case MGPWireOp::QueryCreate: return MGPipeVerb::BeginXfbPrimitivesQuery; + case MGPWireOp::QueryBegin: return MGPipeVerb::BeginXfbPrimitivesQuery; + case MGPWireOp::QueryEnd: return MGPipeVerb::EndXfbPrimitivesQuery; + case MGPWireOp::QueryAvailable: return MGPipeVerb::IsQueryResultAvailable; + case MGPWireOp::QueryResult: return MGPipeVerb::GetQueryResult64; + case MGPWireOp::QueryDestroy: return MGPipeVerb::DeleteBackendQuery; + case MGPWireOp::QueryCounter: return MGPipeVerb::QueryCounterTimestamp; + case MGPWireOp::QueryTimestamp: return MGPipeVerb::GetGpuTimestampNs; + case MGPWireOp::DeleteStreamOutput: return MGPipeVerb::DeleteTransformFeedback; default: return MGPipeVerb::kVerbCount; } } -inline constexpr SizeT kMGPipeVerbBoundaryOpCount = 23; +inline constexpr SizeT kMGPipeVerbBoundaryOpCount = 32; inline constexpr SizeT kMGPipeVerbBoundaryExemptCount = 3; // ---- THE §7 ALLOWLIST, DERIVED RATHER THAN WRITTEN (ID-116) -------------------------- diff --git a/MobileGL/MG_Pipe/generated/PipeTables.inc b/MobileGL/MG_Pipe/generated/PipeTables.inc index 10fabc8c..db243efa 100644 --- a/MobileGL/MG_Pipe/generated/PipeTables.inc +++ b/MobileGL/MG_Pipe/generated/PipeTables.inc @@ -29,7 +29,7 @@ struct MGPipeScreen { void (*ApplierReset)(const MGPApplierReset* payload); }; -// context: 68 calls. A null entry means the backend does not implement this +// context: 69 calls. A null entry means the backend does not implement this // call and the frontend keeps its own path (plan B section 4.1). struct MGPipeContext { void (*QueryCreate)(const MGPQueryDesc* payload); @@ -100,11 +100,12 @@ struct MGPipeContext { void (*ObjectDeath)(const MGPHandleOnly* payload); void (*SetContextValues)(const MGPContextValues* payload); void (*SetProgramBindings)(const MGPProgramBindings* payload, const void* varTail, Uint32 varTailCount); + void (*DeleteStreamOutput)(const MGPStreamOutputBind* payload); }; inline constexpr SizeT kMGPipeScreenCallCount = 12; -inline constexpr SizeT kMGPipeContextCallCount = 68; -inline constexpr SizeT kMGPipeCallCount = 80; +inline constexpr SizeT kMGPipeContextCallCount = 69; +inline constexpr SizeT kMGPipeCallCount = 81; // A table that is not exactly its call count of function pointers has grown a // member that no generator knows about. diff --git a/MobileGL/MG_Pipe/generated/PipeThunks.inc b/MobileGL/MG_Pipe/generated/PipeThunks.inc index 643b78d1..62ef4933 100644 --- a/MobileGL/MG_Pipe/generated/PipeThunks.inc +++ b/MobileGL/MG_Pipe/generated/PipeThunks.inc @@ -336,3 +336,7 @@ inline void MGP_SetContextValues(const MGPContextValues* payload) { inline void MGP_SetProgramBindings(const MGPProgramBindings* payload, const void* varTail, Uint32 varTailCount) { gMGPipeContext.SetProgramBindings(payload, varTail, varTailCount); } + +inline void MGP_DeleteStreamOutput(const MGPStreamOutputBind* payload) { + gMGPipeContext.DeleteStreamOutput(payload); +} diff --git a/MobileGL/MG_Pipe/generated/PipeWire.inc b/MobileGL/MG_Pipe/generated/PipeWire.inc index 906e7f59..8f39d32f 100644 --- a/MobileGL/MG_Pipe/generated/PipeWire.inc +++ b/MobileGL/MG_Pipe/generated/PipeWire.inc @@ -150,7 +150,8 @@ enum class MGPWireOp : Uint16 { ObjectDeath = 78, SetContextValues = 79, SetProgramBindings = 80, - kOpCount = 81, + DeleteStreamOutput = 81, + kOpCount = 82, }; // THE FLAGS, EXPORTED ONCE, INDEXED BY OPCODE (P5 R-13.4). MGPWireRecHeader::Flags is @@ -252,6 +253,7 @@ inline constexpr Uint32 kMGPipeCallFlags[static_cast(MGPWireOp::kOpCount) /* 78 ObjectDeath */ static_cast(kNone), /* 79 SetContextValues */ static_cast(kNone), /* 80 SetProgramBindings */ static_cast(kVarTail | kHostSpan), + /* 81 DeleteStreamOutput */ static_cast(kNone), }; static_assert(sizeof(kMGPipeCallFlags) / sizeof(kMGPipeCallFlags[0]) == static_cast(MGPWireOp::kOpCount), @@ -352,6 +354,7 @@ inline constexpr Uint8 kMGPipeCallClasses[static_cast(MGPWireOp::kOpCount /* 78 ObjectDeath */ static_cast(kCtxObject), /* 79 SetContextValues */ static_cast(kCtxState), /* 80 SetProgramBindings */ static_cast(kCtxState), + /* 81 DeleteStreamOutput */ static_cast(kCtxVerb), }; static_assert(sizeof(kMGPipeCallClasses) / sizeof(kMGPipeCallClasses[0]) == static_cast(MGPWireOp::kOpCount), @@ -456,6 +459,7 @@ inline constexpr Uint8 kMGPipeWaitClasses[static_cast(MGPWireOp::kOpCount /* 78 ObjectDeath */ static_cast(kWaitNone), /* 79 SetContextValues */ static_cast(kWaitNone), /* 80 SetProgramBindings */ static_cast(kWaitNone), + /* 81 DeleteStreamOutput */ static_cast(kWaitNone), }; static_assert(sizeof(kMGPipeWaitClasses) / sizeof(kMGPipeWaitClasses[0]) == static_cast(MGPWireOp::kOpCount), @@ -1162,6 +1166,14 @@ static_assert(sizeof(MGPWireRec_SetProgramBindings) == ((sizeof(MGPWireRecHeader) + sizeof(MGPProgramBindings) + 7u) & ~SizeT(7u)), "MGPWireRec_SetProgramBindings gained padding; the wire format moved"); +struct alignas(8) MGPWireRec_DeleteStreamOutput { + MGPWireRecHeader Header; + MGPStreamOutputBind Payload; +}; +static_assert(sizeof(MGPWireRec_DeleteStreamOutput) == + ((sizeof(MGPWireRecHeader) + sizeof(MGPStreamOutputBind) + 7u) & ~SizeT(7u)), + "MGPWireRec_DeleteStreamOutput gained padding; the wire format moved"); + [[noreturn]] inline void MGPipeWireProtocolFatal(const char* call, Uint64 size, Uint64 remaining) { MGLOG_F("MGPipe: protocol corruption applying %s: size=%llu remaining=%llu", call, static_cast(size), static_cast(remaining)); @@ -1451,6 +1463,9 @@ inline Bool MGPipeApplyWireRecord(MGPWireOp op, const void* record, Uint64 size, case MGPWireOp::SetProgramBindings: MGP_WIRE_CHECK_BOUNDS(MGPWireRec_SetProgramBindings, "SetProgramBindings"); break; + case MGPWireOp::DeleteStreamOutput: + MGP_WIRE_CHECK_BOUNDS(MGPWireRec_DeleteStreamOutput, "DeleteStreamOutput"); + break; case MGPWireOp::kInvalid: case MGPWireOp::kOpCount: default: diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index a1a443ca..6d4341b3 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -34,6 +34,7 @@ #include "PersistentMapTracker.h" #include +#include #include #include #include @@ -915,11 +916,9 @@ namespace MobileGL::MG_Remote::Client { // split form - the server writes the reply into the buffer resource and the client // marks it GPU-written (b1's MarkReadPixelsPackBuffer becoming the producer contract // §3 names) - is a P6 ROADMAP item. In P5 it is class C's shape (R-4), refused here. - if (MG_State::pGLContext != nullptr && - MG_State::pGLContext->GetBufferBindingSlot(::MobileGL::BufferTarget::PixelPack) - .GetBoundObject()) { - UnmigratedVerbFatal("ReadPixels+PACK_BUFFER"); - } + const auto pbo = MG_State::pGLContext != nullptr + ? MG_State::pGLContext->GetBufferBindingSlot(::MobileGL::BufferTarget::PixelPack).GetBoundObject() + : SharedPtr{}; BeforeReadOnlyVerb(); @@ -972,7 +971,7 @@ namespace MobileGL::MG_Remote::Client { Int32 status = 0; Uint64 replySize = 0; - if (ReadbackPackStateIsTight(width, bytesPerPixel, pack)) { + if (!pbo && ReadbackPackStateIsTight(width, bytesPerPixel, pack)) { // THE COMMON CASE, AND IT KEEPS THE ZERO-COPY. A neutral pack state means the // destination layout IS the tight layout, so the reply lands straight in the // application's buffer and there is no bounce at all. It is a fast path for the @@ -998,10 +997,25 @@ namespace MobileGL::MG_Remote::Client { // pointer at all (the bounce is the only thing that held the partial bytes). RequireReadbackReplyComplete(status, replySize, tight); ApplyReadbackByteSwap(bounce.data(), tight, type, pack); + if (pbo) { + // pixels is an offset, never a host pointer. Upload only the + // requested rows so PACK padding and untouched bytes survive. + const Uint64 alignment = std::max(pack.Alignment, 1); + const Uint64 rowPixels = pack.RowLength > 0 ? pack.RowLength : width; + const Uint64 stride = (rowPixels * bytesPerPixel + alignment - 1) / alignment * alignment; + const Uint64 start = reinterpret_cast(pixels) + pack.SkipRows * stride + + pack.SkipPixels * bytesPerPixel; + const SizeT rowBytes = static_cast(width) * bytesPerPixel; + for (Int y = 0; y < height; ++y) + pbo->UploadSubData({bounce.data() + y * rowBytes, rowBytes}, start + y * stride); + return; + } ScatterTightReadbackIntoPackState(bounce.data(), pixels, width, height, bytesPerPixel, pack); } +#include "TextureReadbackEmit.inc" + void EmitPresent() { ClientSession& session = RequireSession("Present"); BeforeReadOnlyVerb(); @@ -1565,7 +1579,9 @@ namespace MobileGL::MG_Remote::Client { g_fenceProxies.erase(proxy); } - // CLASS C - 16 slots remain after d1/i1/t2/f1; each names its first blocker. +#include "QueryEmit.inc" + + // CLASS C - each remaining slot names its first blocker. // ============================================================================= // // PARTITIONED BY THE P5b PACKAGE THAT OWNS THE FLIP (MG_Remote/CONTRACT-P5B.md, @@ -1614,35 +1630,19 @@ namespace MobileGL::MG_Remote::Client { // not by omission (unmeasured; the driver object leaks on the server until P9's XFB // namespace work, and a bind of name 0 is what the backend does on delete of the bound // one, DirectGLES.cpp:1422). It therefore still aborts by its own name. -#define MGR_UNMIGRATED_T2_SLOTS(X) \ - X(DeleteTransformFeedback, void, (GLuint)) +#define MGR_UNMIGRATED_T2_SLOTS(X) #define MGR_UNMIGRATED_F1_SLOTS(X) // The wave-3 tail. SetSwapInterval is hand-written below (it is not a GL.* slot). -#define MGR_UNMIGRATED_TAIL_SLOTS(X) \ - X(GetTexImage, void, (GLenum, GLint, GLenum, GLenum, GLvoid*)) \ - X(GetTextureImage, void, \ - (const SharedPtr&, TextureUploadTarget, GLint, GLenum, \ - GLenum, GLsizei, GLvoid*)) \ - X(EndTimeElapsedQuery, void, (MG_Backend::BackendQueryHandle)) \ - X(DeleteBackendQuery, void, (MG_Backend::BackendQueryHandle)) \ - X(EndOcclusionQuery, void, (MG_Backend::BackendQueryHandle)) \ - X(EndXfbPrimitivesQuery, void, (MG_Backend::BackendQueryHandle)) +#define MGR_UNMIGRATED_TAIL_SLOTS(X) // The non-void ones, kept apart only because the macro body differs: a [[noreturn]] // call is a complete body for a void slot and for a value-returning one alike, but a // compiler that does not see UnmigratedVerbFatal's attribute through the macro would // warn on the second. It does see it; they are split for readability. All ten are the // wave-3 tail. -#define MGR_UNMIGRATED_TAIL_VALUE_SLOTS(X) \ - X(BeginTimeElapsedQuery, MG_Backend::BackendQueryHandle, ()) \ - X(QueryCounterTimestamp, MG_Backend::BackendQueryHandle, ()) \ - X(IsQueryResultAvailable, Bool, (MG_Backend::BackendQueryHandle)) \ - X(GetQueryResult64, Bool, (MG_Backend::BackendQueryHandle, Bool, Uint64*)) \ - X(BeginOcclusionQuery, MG_Backend::BackendQueryHandle, ()) \ - X(BeginXfbPrimitivesQuery, MG_Backend::BackendQueryHandle, (Bool)) \ - X(GetGpuTimestampNs, Int64, ()) +#define MGR_UNMIGRATED_TAIL_VALUE_SLOTS(X) // The union, for the places that want every class-C row at once (the definitions and // the assignments). A package never edits THIS; it edits its own list above. @@ -1686,13 +1686,14 @@ namespace MobileGL::MG_Remote::Client { constexpr Uint32 kEmittedSlotsP5 = 5; // Clear, DrawArrays, ReadPixels, Blit, Present constexpr Uint32 kEmittedSlotsD1 = 19; constexpr Uint32 kEmittedSlotsI1 = 7; - constexpr Uint32 kEmittedSlotsT2 = 6; + constexpr Uint32 kEmittedSlotsT2 = 7; constexpr Uint32 kEmittedSlotsF1 = 11; - constexpr Uint32 kEmittedSlotsTail = 1; // BlitNamedFramebuffer + constexpr Uint32 kEmittedSlotsTail = 3; // BlitNamedFramebuffer, both texture readbacks constexpr Uint32 kEmittedSlotsSync = 5; + constexpr Uint32 kEmittedSlotsQueries = 11; constexpr Uint32 kEmittedSlots = kEmittedSlotsP5 + kEmittedSlotsD1 + kEmittedSlotsI1 + kEmittedSlotsT2 + kEmittedSlotsF1 + - kEmittedSlotsTail + kEmittedSlotsSync; + kEmittedSlotsTail + kEmittedSlotsSync + kEmittedSlotsQueries; constexpr Uint32 kLocallyAnsweredSlots = 2; // GetIntegeri_v, IsTimerQuerySupported // EACH PACKAGE'S OWNERSHIP, PINNED. A package that flips a slot removes one row and @@ -1703,7 +1704,7 @@ namespace MobileGL::MG_Remote::Client { static_assert(kUnmigratedI1 + kEmittedSlotsI1 == 7, "i1 owns the 7 image/compute/barrier/copy/SSBO slots"); static_assert(kUnmigratedT2 + kEmittedSlotsT2 == 7, "t2 owns the 7 XFB/tessellation slots"); static_assert(kUnmigratedF1 + kEmittedSlotsF1 == 11, "f1 owns the 11 clear/copy/mip slots"); - static_assert(kUnmigratedTail + kEmittedSlotsTail + kEmittedSlotsSync == 20, + static_assert(kUnmigratedTail + kEmittedSlotsTail + kEmittedSlotsSync + kEmittedSlotsQueries == 20, "the original wave-3 tail owns 20 slots"); static_assert(kUnmigratedSlots + kEmittedSlots == 69, "class B and C own 69 slots"); static_assert(kLocallyAnsweredSlots + kEmittedSlots + kUnmigratedSlots == kRemoteEmitSlotCount, @@ -1729,6 +1730,18 @@ namespace MobileGL::MG_Remote::Client { table.GL.GetSyncStatus = &EmitGetSyncStatus; table.GL.WaitSync = &EmitWaitSync; table.GL.DeleteSync = &EmitDeleteSync; + table.GL.BeginTimeElapsedQuery = &EmitBeginTimeElapsedQuery; + table.GL.EndTimeElapsedQuery = &EmitEndQuery; + table.GL.QueryCounterTimestamp = &EmitQueryCounterTimestamp; + table.GL.BeginOcclusionQuery = &EmitBeginOcclusionQuery; + table.GL.EndOcclusionQuery = &EmitEndQuery; + table.GL.BeginXfbPrimitivesQuery = &EmitBeginXfbPrimitivesQuery; + table.GL.EndXfbPrimitivesQuery = &EmitEndQuery; + table.GL.IsQueryResultAvailable = &EmitIsQueryResultAvailable; + table.GL.GetQueryResult64 = &EmitGetQueryResult64; + table.GL.DeleteBackendQuery = &EmitDeleteBackendQuery; + table.GL.GetGpuTimestampNs = &EmitGetGpuTimestampNs; + table.GL.DeleteTransformFeedback = &EmitDeleteTransformFeedback; // ---- class A table.GL.GetIntegeri_v = &AnswerGetIntegeri_v; @@ -1791,6 +1804,8 @@ namespace MobileGL::MG_Remote::Client { table.GL.CopyTexImage2D = &EmitCopyTexImage2D; table.GL.CopyTexSubImage2D = &EmitCopyTexSubImage2D; table.GL.GenerateMipmap = &EmitGenerateMipmap; + table.GL.GetTexImage = &EmitGetTexImage; + table.GL.GetTextureImage = &EmitGetTextureImage; // ---- class B, P5b package i1 (kEmittedSlotsI1 = 7) table.GL.BindImageTexture = &EmitBindImageTexture; diff --git a/MobileGL/MG_Remote/Client/QueryEmit.inc b/MobileGL/MG_Remote/Client/QueryEmit.inc new file mode 100644 index 00000000..2e236fd5 --- /dev/null +++ b/MobileGL/MG_Remote/Client/QueryEmit.inc @@ -0,0 +1,104 @@ +// Opaque local proxies carry only {slot, generation, target} across the wire. + struct RemoteQueryProxy { MG_Pipe::MGPQueryDesc Desc{}; }; + std::mutex g_queryProxyMutex; + UnorderedMap> g_queryProxies; + + const MG_Pipe::MGPQueryDesc& QueryDesc(MG_Backend::BackendQueryHandle handle) { + const auto found = g_queryProxies.find(handle); + if (found == g_queryProxies.end()) + Wire::WireProtocolFatal("Query.proxy", "unknown local query proxy"); + return found->second->Desc; + } + + template + Result ReadQueryReply(ClientSession& session, MG_Pipe::MGPWireOp op, const void* request, Uint64 size) { + Result result{}; + Int32 status = Wire::ReplySink::kStatusError; + Uint64 returned = 0; + session.EmitAndWait(op, request, size, nullptr, 0, &result, sizeof(result), &status, &returned); + if (status != Wire::ReplySink::kStatusOk || returned != sizeof(result)) + Wire::WireProtocolFatal("Query.reply", "missing or malformed backend query reply"); + return result; + } + + MG_Backend::BackendQueryHandle EmitCreateQuery(GLenum kind, Bool counter) { + auto& session = RequireSession("QueryBegin"); + const std::lock_guard lock(g_queryProxyMutex); + BeforeReadOnlyVerb(); + auto proxy = MakeUnique(); + proxy->Desc.Query = MG_Pipe::MGPipeSlots().Allocate(MG_Pipe::MGPipeKind::Query); + proxy->Desc.Kind = kind; + session.EmitAndWait(MG_Pipe::MGPWireOp::QueryCreate, &proxy->Desc, sizeof(proxy->Desc), + nullptr, 0, nullptr, 0, nullptr); + session.EmitAndWait(counter ? MG_Pipe::MGPWireOp::QueryCounter : MG_Pipe::MGPWireOp::QueryBegin, + &proxy->Desc, sizeof(proxy->Desc), nullptr, 0, nullptr, 0, nullptr); + auto* local = proxy.get(); + g_queryProxies.emplace(local, Move(proxy)); + return local; + } + MG_Backend::BackendQueryHandle EmitBeginTimeElapsedQuery() { return EmitCreateQuery(GL_TIME_ELAPSED, false); } + MG_Backend::BackendQueryHandle EmitQueryCounterTimestamp() { return EmitCreateQuery(GL_TIMESTAMP, true); } + MG_Backend::BackendQueryHandle EmitBeginOcclusionQuery() { return EmitCreateQuery(GL_SAMPLES_PASSED, false); } + MG_Backend::BackendQueryHandle EmitBeginXfbPrimitivesQuery(Bool generated) { + return EmitCreateQuery(generated ? GL_PRIMITIVES_GENERATED : GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, false); + } + void EmitEndQuery(MG_Backend::BackendQueryHandle handle) { + auto& session = RequireSession("QueryEnd"); + const std::lock_guard lock(g_queryProxyMutex); + const auto& desc = QueryDesc(handle); + BeforeReadOnlyVerb(); + session.EmitAndWait(MG_Pipe::MGPWireOp::QueryEnd, &desc, sizeof(desc), nullptr, 0, nullptr, 0, nullptr); + } + Bool EmitIsQueryResultAvailable(MG_Backend::BackendQueryHandle handle) { + auto& session = RequireSession("IsQueryResultAvailable"); + const std::lock_guard lock(g_queryProxyMutex); + const MG_Pipe::MGPHandleOnly request{QueryDesc(handle).Query, static_cast(MG_Pipe::MGPipeKind::Query), 0}; + const auto result = ReadQueryReply(session, MG_Pipe::MGPWireOp::QueryAvailable, &request, sizeof(request)); + if (result > 1) Wire::WireProtocolFatal("QueryAvailable.reply", "availability is not boolean"); + return result != 0; + } + Bool EmitGetQueryResult64(MG_Backend::BackendQueryHandle handle, Bool wait, Uint64* value) { + auto& session = RequireSession("GetQueryResult64"); + const std::lock_guard lock(g_queryProxyMutex); + MG_Pipe::MGPQueryResultRequest request{}; + request.Query = QueryDesc(handle).Query; + request.Wait = wait ? 1 : 0; + const auto reply = ReadQueryReply(session, MG_Pipe::MGPWireOp::QueryResult, + &request, sizeof(request)); + if (reply.Produced > 1 || reply.Reserved) + Wire::WireProtocolFatal("QueryResult.reply", "invalid result metadata"); + if (!reply.Produced) return false; + if (value) *value = reply.Value; + return true; + } + void EmitDeleteBackendQuery(MG_Backend::BackendQueryHandle handle) { + const std::lock_guard lock(g_queryProxyMutex); + const auto query = QueryDesc(handle).Query; + if (auto* session = ClientSession::Active(); session && session->Started()) { + const MG_Pipe::MGPHandleOnly request{query, static_cast(MG_Pipe::MGPipeKind::Query), 0}; + session->EmitAndWait(MG_Pipe::MGPWireOp::QueryDestroy, &request, sizeof(request), + nullptr, 0, nullptr, 0, nullptr); + } + MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Query, query); + g_queryProxies.erase(handle); + } + Int64 EmitGetGpuTimestampNs() { + auto& session = RequireSession("GetGpuTimestampNs"); + BeforeReadOnlyVerb(); + const MG_Pipe::MGPTimestampRequest request{}; + return ReadQueryReply(session, MG_Pipe::MGPWireOp::QueryTimestamp, &request, sizeof(request)); + } + + void EmitDeleteTransformFeedback(GLuint name) { + auto& session = RequireSession("DeleteTransformFeedback"); + BeforeReadOnlyVerb(); + const auto& context = *MG_State::pGLContext; + const MG_Pipe::MGPStreamOutputBind object{name, 0, context.GetTransformFeedbackLifetimeId(name)}; + session.EmitAndWait(MG_Pipe::MGPWireOp::DeleteStreamOutput, &object, sizeof(object), + nullptr, 0, nullptr, 0, nullptr); + if (context.GetBoundTransformFeedbackName() == name) { + const MG_Pipe::MGPStreamOutputBind fallback{0, 0, context.GetTransformFeedbackLifetimeId(0)}; + session.EmitAndWait(MG_Pipe::MGPWireOp::BindStreamOutput, &fallback, sizeof(fallback), + nullptr, 0, nullptr, 0, nullptr); + } + } diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index 426e6c0b..2e297e56 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -19,6 +19,8 @@ // §5.2). The same dependency ServerLoop.cpp already takes for CreateBackend; a server built // on Magma simply holds no twins in these tables and every release resolves to nothing. #include +#include +#include #include #include #include @@ -72,7 +74,10 @@ namespace MobileGL::MG_Remote::Server { // ----------------------------------------------------------------------------------- void ServerVerbSink::SetBackend(MG_Backend::BackendObject* backend) { - if (m_backend != backend) ReleaseFences(); + if (m_backend != backend) { + ReleaseQueries(); + ReleaseFences(); + } m_backend = backend; } @@ -182,20 +187,27 @@ namespace MobileGL::MG_Remote::Server { m_fences.clear(); } +#include "QueryServer.inc" + Bool ServerVerbSink::OnClear(const MG_Pipe::MGPClear& clear) { const MG_Backend::GlobalBackendFunctionsTable* table = Table("clear"); if (table == nullptr) return false; const MG_Backend::GLFunctionsTable& gl = table->GL; - // Named records precede the verb; bound-form backends re-sync the live draw binding. - if (!MG_Pipe::MGPipeHandleIsNull(clear.Fbo) && - clear.Fbo != MG_Pipe::MGPipeApplier().BoundFramebuffer[0]) { - const char* slot = clear.Kind == kMGPClearKindDepthStencil ? "ClearNamedFramebufferfi+UNBOUND" : - clear.ValueClass == kMGPClearValueClassInt ? "ClearNamedFramebufferiv+UNBOUND" : - clear.ValueClass == kMGPClearValueClassUint ? "ClearNamedFramebufferuiv+UNBOUND" : - "ClearNamedFramebufferfv+UNBOUND"; - MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"%s\"}", slot); - std::abort(); + // The named framebuffer record already crossed before this verb. Scope + // only the server's resolved draw target; the next verb still observes + // the application's unchanged binding. Both backends sync from this + // handle and never need the frontend FramebufferObject. + auto& applier = MG_Pipe::MGPipeApplier(); + struct ScopedClearTarget { + MG_Pipe::MGPipeApplierState& State; + MG_Pipe::MGPipeHandle Saved; + ~ScopedClearTarget() { State.BoundFramebuffer[0] = Saved; } + } clearTarget{applier, applier.BoundFramebuffer[0]}; + if (!MG_Pipe::MGPipeHandleIsNull(clear.Fbo)) { + if (!applier.FramebufferRecordFor(clear.Fbo)) + Wire::WireProtocolFatal("Clear.Fbo", "missing named framebuffer record"); + applier.BoundFramebuffer[0] = clear.Fbo; } switch (clear.Kind) { case kMGPClearKindWhole: @@ -332,6 +344,41 @@ namespace MobileGL::MG_Remote::Server { return true; } + Bool ServerVerbSink::OnGetTextureImage(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq, + Wire::ReplySink* replies) { + if (!replies) Wire::WireProtocolFatal("GetTextureImage.reply", "missing reply sink"); + const SizeT bpp = MG_Util::GetInputBytesPerPixel( + MG_Util::ConvertGLEnumToTextureInputFormat(info.Format), + MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)); + if (!bpp || !info.Box.W || !info.Box.H || !info.Box.D || info.Box.X || info.Box.Y || info.Box.Z) { + Wire::WireProtocolFatal("GetTextureImage.extent", "invalid tight image extent"); + } + const Uint64 tight = static_cast(info.Box.W) * info.Box.H * info.Box.D * bpp; + if (tight / bpp / info.Box.W / info.Box.H != info.Box.D || + info.DstOffset > tight || !info.DstSize || info.DstSize > tight - info.DstOffset) + Wire::WireProtocolFatal("GetTextureImage.range", "invalid reply window"); + auto image = info; + image.DstOffset = 0; + image.DstSize = tight; + Vector bytes; + Bool ok = false; +#if MOBILEGL_BUILD_DISAGGREGATED + if (m_backend && m_backend->GetBackendType() == BackendType::DirectGLES) + ok = MG_Backend::DirectGLES::ReadTextureImageWire(image, bytes); + else if (m_backend && m_backend->GetBackendType() == BackendType::DirectVulkan && + MG_Backend::DirectVulkan::pVulkanRenderer) + ok = MG_Backend::DirectVulkan::pVulkanRenderer->ReadTextureImageWire(image, bytes); +#endif + if (!ok || bytes.size() != tight) { + replies->PostReply(seq, Wire::ReplySink::kStatusError, nullptr, 0); + return false; + } + replies->PostReply(seq, Wire::ReplySink::kStatusOk, bytes.data() + info.DstOffset, info.DstSize); + m_readbackBytes += info.DstSize; + ++m_readbacks; + return true; + } + Bool ServerVerbSink::OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq, Wire::ReplySink* replies) { if (replies == nullptr) { @@ -1008,7 +1055,6 @@ namespace MobileGL::MG_Remote::Server { Bool ServerVerbSink::OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) { const MG_Backend::GlobalBackendFunctionsTable* table = Table("bind_stream_output"); if (table == nullptr) return false; - if (table->GL.BindTransformFeedback == nullptr) return false; // THE GL NAME IS THE ARGUMENT, NOT THE LifetimeId BESIDE IT. Espryt keys its driver // objects by the GL name (XfbImpl::g_xfbObjects[name], DirectGLES.cpp:1401) and creates // the ES object on first bind; passing the lifetime id would index a map that has never @@ -1016,11 +1062,25 @@ namespace MobileGL::MG_Remote::Server { // identifies the P5f capture snapshot on this side. Keeping it separate preserves // the backend GL-name argument while Begin/End find the right server-owned span. MG_Pipe::MGPipeApplier().BoundStreamOutputLifetimeId = bind.LifetimeId; - table->GL.BindTransformFeedback(static_cast(bind.GlName)); + if (table->GL.BindTransformFeedback) table->GL.BindTransformFeedback(static_cast(bind.GlName)); ++m_streamOutputBinds; return true; } + Bool ServerVerbSink::OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind& object) { + const auto* table = Table("DeleteTransformFeedback"); + if (!table) return false; + if (!object.GlName || object.Pad0) + Wire::WireProtocolFatal("DeleteStreamOutput.name", "default object or reserved field"); + auto& state = MG_Pipe::MGPipeApplier(); + if (object.LifetimeId) { + state.StreamOutputSpans.erase(object.LifetimeId); + if (state.BoundStreamOutputLifetimeId == object.LifetimeId) state.BoundStreamOutputLifetimeId = 0; + } + if (table->GL.DeleteTransformFeedback) table->GL.DeleteTransformFeedback(object.GlName); + return true; + } + Bool ServerVerbSink::OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) { const MG_Backend::GlobalBackendFunctionsTable* table = Table("patch_parameter"); if (table == nullptr) return false; diff --git a/MobileGL/MG_Remote/Server/PipeApplier.h b/MobileGL/MG_Remote/Server/PipeApplier.h index a9c5e178..2d9b90e5 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.h +++ b/MobileGL/MG_Remote/Server/PipeApplier.h @@ -141,11 +141,22 @@ namespace MobileGL::MG_Remote::Server { Bool OnFenceWait(const MG_Pipe::MGPFenceWait&, Uint32&) override; Bool OnFenceWaitServer(const MG_Pipe::MGPFenceWait&) override; void ReleaseFences(); + Bool OnQueryCreate(const MG_Pipe::MGPQueryDesc&) override; + Bool OnQueryBegin(const MG_Pipe::MGPQueryDesc&) override; + Bool OnQueryEnd(const MG_Pipe::MGPQueryDesc&) override; + Bool OnQueryCounter(const MG_Pipe::MGPQueryDesc&) override; + Bool OnQueryAvailable(const MG_Pipe::MGPHandleOnly&, Uint32&) override; + Bool OnQueryResult(const MG_Pipe::MGPQueryResultRequest&, Wire::QueryResultReply&) override; + Bool OnQueryDestroy(const MG_Pipe::MGPHandleOnly&) override; + Bool OnQueryTimestamp(const MG_Pipe::MGPTimestampRequest&, Int64&) override; + void ReleaseQueries(); Bool OnClear(const MG_Pipe::MGPClear& clear) override; Bool OnBlit(const MG_Pipe::MGPBlit& blit) override; Bool OnPresent(const MG_Pipe::MGPPresent& present) override; Bool OnReadPixels(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq, Wire::ReplySink* replies) override; + Bool OnGetTextureImage(const MG_Pipe::MGPReadbackInfo& info, Uint64 seq, + Wire::ReplySink* replies) override; Bool OnDrawVbo(const MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges, const MG_Pipe::MGHostSpan* userIndices, const MG_Pipe::MGPDrawIndirect* indirect) override; @@ -179,6 +190,7 @@ namespace MobileGL::MG_Remote::Server { Bool OnPauseStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) override; Bool OnResumeStreamOutput(const MG_Pipe::MGPStreamOutputControl& control) override; Bool OnBindStreamOutput(const MG_Pipe::MGPStreamOutputBind& bind) override; + Bool OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind& object) override; Bool OnPatchParameter(const MG_Pipe::MGPPatchParameter& patch) override; Bool OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) override; Bool OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) override; @@ -273,6 +285,18 @@ namespace MobileGL::MG_Remote::Server { }; FenceEntry& FindFence(MG_Pipe::MGPipeHandle handle); UnorderedMap m_fences; + struct QueryEntry { + Uint32 Gen = 0; + Uint32 Kind = 0; + Bool Live = false; + Bool Active = false; + MG_Backend::BackendQueryHandle Native = nullptr; + MG_Backend::BackendSyncHandle Completion = nullptr; + }; + QueryEntry& FindQuery(MG_Pipe::MGPipeHandle handle); + void EndNativeQuery(QueryEntry& entry); + Bool QueryGpuComplete(QueryEntry& entry, Bool wait); + UnorderedMap m_queries; MG_Backend::BackendObject* m_backend = nullptr; Uint64 m_clears = 0; Uint64 m_draws = 0; diff --git a/MobileGL/MG_Remote/Server/QueryServer.inc b/MobileGL/MG_Remote/Server/QueryServer.inc new file mode 100644 index 00000000..3bc92448 --- /dev/null +++ b/MobileGL/MG_Remote/Server/QueryServer.inc @@ -0,0 +1,171 @@ +// Server-owned query identities and actual backend query/fence results. + ServerVerbSink::QueryEntry& ServerVerbSink::FindQuery(MG_Pipe::MGPipeHandle handle) { + const auto found = m_queries.find(handle.Slot); + if (!handle.Slot || found == m_queries.end() || !found->second.Live || found->second.Gen != handle.Gen) + Wire::WireProtocolFatal("Query.handle", "missing, destroyed or stale query handle"); + return found->second; + } + + Bool ServerVerbSink::OnQueryCreate(const MG_Pipe::MGPQueryDesc& desc) { + if (!Table("QueryCreate")) return false; + if (!desc.Query.Slot || desc.Stream != 0) + Wire::WireProtocolFatal("QueryCreate.identity", "reserved handle or unsupported stream"); + switch (desc.Kind) { + case GL_TIME_ELAPSED: case GL_TIMESTAMP: case GL_SAMPLES_PASSED: + case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN: case GL_PRIMITIVES_GENERATED: break; + default: Wire::WireProtocolFatal("QueryCreate.Kind", "unknown query target"); + } + const Bool seen = m_queries.find(desc.Query.Slot) != m_queries.end(); + auto& entry = m_queries[desc.Query.Slot]; + if (entry.Live || (seen && desc.Query.Gen <= entry.Gen)) + Wire::WireProtocolFatal("QueryCreate.handle", "duplicate or stale query generation"); + entry = QueryEntry{}; + entry.Gen = desc.Query.Gen; + entry.Kind = desc.Kind; + entry.Live = true; + return true; + } + + Bool ServerVerbSink::OnQueryBegin(const MG_Pipe::MGPQueryDesc& desc) { + auto& entry = FindQuery(desc.Query); + const auto* table = Table("QueryBegin"); + if (!table) return false; + if (desc.Stream || desc.Kind != entry.Kind || entry.Active || entry.Native) + Wire::WireProtocolFatal("QueryBegin.state", "query target or lifecycle mismatch"); + const auto& gl = table->GL; + switch (entry.Kind) { + case GL_TIME_ELAPSED: + if (!gl.BeginTimeElapsedQuery) return false; + entry.Native = gl.BeginTimeElapsedQuery(); + break; + case GL_SAMPLES_PASSED: + if (!gl.BeginOcclusionQuery) return false; + entry.Native = gl.BeginOcclusionQuery(); + break; + case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN: + case GL_PRIMITIVES_GENERATED: + if (!gl.BeginXfbPrimitivesQuery) return false; + entry.Native = gl.BeginXfbPrimitivesQuery(entry.Kind == GL_PRIMITIVES_GENERATED); + break; + default: Wire::WireProtocolFatal("QueryBegin.Kind", "target is not a begin/end query"); + } + // A failed native allocation remains a real, unreadable query. Returning a + // null proxy to the frontend would silently select CPU primitive accounting. + entry.Active = true; + return true; + } + + void ServerVerbSink::EndNativeQuery(QueryEntry& entry) { + if (!entry.Active || !m_backend) return; + const auto& gl = m_backend->GetBackendFunctions().GL; + if (entry.Native) { + switch (entry.Kind) { + case GL_TIME_ELAPSED: if (gl.EndTimeElapsedQuery) gl.EndTimeElapsedQuery(entry.Native); break; + case GL_SAMPLES_PASSED: if (gl.EndOcclusionQuery) gl.EndOcclusionQuery(entry.Native); break; + case GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN: + case GL_PRIMITIVES_GENERATED: + if (gl.EndXfbPrimitivesQuery) gl.EndXfbPrimitivesQuery(entry.Native); + break; + default: break; + } + } + entry.Active = false; + } + + Bool ServerVerbSink::OnQueryEnd(const MG_Pipe::MGPQueryDesc& desc) { + auto& entry = FindQuery(desc.Query); + const auto* table = Table("QueryEnd"); + if (!table) return false; + if (desc.Stream || desc.Kind != entry.Kind || !entry.Active) + Wire::WireProtocolFatal("QueryEnd.state", "query target or lifecycle mismatch"); + EndNativeQuery(entry); + if (entry.Native && table->GL.FenceSync) entry.Completion = table->GL.FenceSync(); + return true; + } + + Bool ServerVerbSink::OnQueryCounter(const MG_Pipe::MGPQueryDesc& desc) { + auto& entry = FindQuery(desc.Query); + const auto* table = Table("QueryCounter"); + if (!table || !table->GL.QueryCounterTimestamp) return false; + if (desc.Stream || desc.Kind != GL_TIMESTAMP || entry.Kind != GL_TIMESTAMP || entry.Native || entry.Active) + Wire::WireProtocolFatal("QueryCounter.state", "query target or lifecycle mismatch"); + entry.Native = table->GL.QueryCounterTimestamp(); + if (entry.Native && table->GL.FenceSync) entry.Completion = table->GL.FenceSync(); + return true; + } + + Bool ServerVerbSink::QueryGpuComplete(QueryEntry& entry, Bool wait) { + if (!m_backend || !entry.Native || entry.Active) return false; + const auto& gl = m_backend->GetBackendFunctions().GL; + if (!entry.Completion || !gl.ClientWaitSync || !gl.IsQueryResultAvailable) return false; + // Polling may submit the pending batch, but never waits for GPU completion. + // A blocking result explicitly submits/waits, including present-less workloads. + const GLenum completion = gl.ClientWaitSync(entry.Completion, GL_SYNC_FLUSH_COMMANDS_BIT, + wait ? GL_TIMEOUT_IGNORED : 0); + if (completion != GL_ALREADY_SIGNALED && completion != GL_CONDITION_SATISFIED) return false; + return gl.IsQueryResultAvailable(entry.Native); + } + + Bool ServerVerbSink::OnQueryAvailable(const MG_Pipe::MGPHandleOnly& request, Uint32& result) { + if (request.Kind != static_cast(MG_Pipe::MGPipeKind::Query)) + Wire::WireProtocolFatal("Query.Kind", "expected query namespace"); + auto& entry = FindQuery(request.Handle); + result = QueryGpuComplete(entry, false) ? 1 : 0; + return true; + } + + Bool ServerVerbSink::OnQueryResult(const MG_Pipe::MGPQueryResultRequest& request, Wire::QueryResultReply& reply) { + if (request.Wait > 1) Wire::WireProtocolFatal("QueryResult.Wait", "wait must be boolean"); + auto& entry = FindQuery(request.Query); + const auto* table = Table("QueryResult"); + if (!table || !table->GL.GetQueryResult64) return false; + reply = {}; + if (QueryGpuComplete(entry, request.Wait != 0)) + reply.Produced = table->GL.GetQueryResult64(entry.Native, request.Wait != 0, &reply.Value) ? 1 : 0; + return true; + } + + Bool ServerVerbSink::OnQueryDestroy(const MG_Pipe::MGPHandleOnly& request) { + if (request.Kind != static_cast(MG_Pipe::MGPipeKind::Query)) + Wire::WireProtocolFatal("Query.Kind", "expected query namespace"); + auto& entry = FindQuery(request.Handle); + const auto* table = Table("QueryDestroy"); + if (!table) return false; + EndNativeQuery(entry); + if (entry.Native && table->GL.DeleteBackendQuery) table->GL.DeleteBackendQuery(entry.Native); + if (entry.Completion && table->GL.DeleteSync) table->GL.DeleteSync(entry.Completion); + entry.Native = nullptr; + entry.Completion = nullptr; + entry.Live = false; + return true; + } + + Bool ServerVerbSink::OnQueryTimestamp(const MG_Pipe::MGPTimestampRequest& request, Int64& result) { + if (request.Reserved) Wire::WireProtocolFatal("QueryTimestamp.Reserved", "reserved field is not zero"); + const auto* table = Table("QueryTimestamp"); + if (!table || !table->GL.QueryCounterTimestamp || !table->GL.GetQueryResult64 || + !table->GL.DeleteBackendQuery || !table->GL.FenceSync || !table->GL.DeleteSync) return false; + QueryEntry entry{}; + entry.Native = table->GL.QueryCounterTimestamp(); + if (entry.Native) entry.Completion = table->GL.FenceSync(); + Uint64 timestamp = 0; + const Bool produced = QueryGpuComplete(entry, true) && + table->GL.GetQueryResult64(entry.Native, true, ×tamp); + if (entry.Native) table->GL.DeleteBackendQuery(entry.Native); + if (entry.Completion) table->GL.DeleteSync(entry.Completion); + if (produced) result = static_cast(timestamp); + return produced; + } + + void ServerVerbSink::ReleaseQueries() { + if (m_backend) { + const auto& gl = m_backend->GetBackendFunctions().GL; + for (auto& [slot, entry] : m_queries) { + if (!entry.Live) continue; + EndNativeQuery(entry); + if (entry.Native && gl.DeleteBackendQuery) gl.DeleteBackendQuery(entry.Native); + if (entry.Completion && gl.DeleteSync) gl.DeleteSync(entry.Completion); + } + } + m_queries.clear(); + } diff --git a/MobileGL/MG_Remote/Server/ServerSession.cpp b/MobileGL/MG_Remote/Server/ServerSession.cpp index 0ce3052e..a26e1cbe 100644 --- a/MobileGL/MG_Remote/Server/ServerSession.cpp +++ b/MobileGL/MG_Remote/Server/ServerSession.cpp @@ -632,7 +632,10 @@ namespace MobileGL::MG_Remote::Server { // R-8/C-4: bits 32..47 of CallMask are the CONSUMER MASK, and this is the only place // they are produced. - const Uint64 callMask = CallMask(); + Uint64 callMask = CallMask(); + const auto timerSupported = m_backend->GetBackendFunctions().GL.IsTimerQuerySupported; + if ((callMask & MG_Pipe::kCapTimerQuery) && (!timerSupported || !timerSupported())) + callMask &= ~static_cast(MG_Pipe::kCapTimerQuery); ::flatbuffers::FlatBufferBuilder builder(4096); auto dynamicVector = diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp index 65a097be..1fc7163d 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.cpp @@ -243,7 +243,8 @@ namespace MobileGL::MG_Remote::Wire { X(ApplierReset, MGPApplierReset) \ X(ObjectDeath, MGPHandleOnly) \ X(SetContextValues, MGPContextValues) \ - X(SetProgramBindings, MGPProgramBindings) + X(SetProgramBindings, MGPProgramBindings) \ + X(DeleteStreamOutput, MGPStreamOutputBind) namespace { @@ -1657,16 +1658,37 @@ namespace MobileGL::MG_Remote::Wire { ok ? &result : nullptr, ok ? sizeof(result) : 0); return ok; } - // Query migration follows the measured first blockers. case MGPWireOp::QueryCreate: + return m_verbs && m_verbs->OnQueryCreate(*static_cast(payload)); case MGPWireOp::QueryBegin: + return m_verbs && m_verbs->OnQueryBegin(*static_cast(payload)); case MGPWireOp::QueryEnd: - case MGPWireOp::QueryAvailable: - case MGPWireOp::QueryResult: + return m_verbs && m_verbs->OnQueryEnd(*static_cast(payload)); case MGPWireOp::QueryDestroy: - case MGPWireOp::QueryTimestamp: + return m_verbs && m_verbs->OnQueryDestroy(*static_cast(payload)); case MGPWireOp::QueryCounter: - return false; + return m_verbs && m_verbs->OnQueryCounter(*static_cast(payload)); + case MGPWireOp::QueryAvailable: { + Uint32 result = 0; + const Bool ok = m_verbs && m_verbs->OnQueryAvailable(*static_cast(payload), result); + PostReply(op, seq, ok ? ReplySink::kStatusOk : ReplySink::kStatusDeclined, + ok ? &result : nullptr, ok ? sizeof(result) : 0); + return ok; + } + case MGPWireOp::QueryResult: { + QueryResultReply result{}; + const Bool ok = m_verbs && m_verbs->OnQueryResult(*static_cast(payload), result); + PostReply(op, seq, ok ? ReplySink::kStatusOk : ReplySink::kStatusDeclined, + ok ? &result : nullptr, ok ? sizeof(result) : 0); + return ok; + } + case MGPWireOp::QueryTimestamp: { + Int64 result = 0; + const Bool ok = m_verbs && m_verbs->OnQueryTimestamp(*static_cast(payload), result); + PostReply(op, seq, ok ? ReplySink::kStatusOk : ReplySink::kStatusDeclined, + ok ? &result : nullptr, ok ? sizeof(result) : 0); + return ok; + } // ---- CSOs ------------------------------------------------------------------------ case MGPWireOp::CreateRenderState: { @@ -2103,9 +2125,8 @@ namespace MobileGL::MG_Remote::Wire { m_verbs->OnGenerateMipmap(*static_cast(payload)); case MGPWireOp::GetTextureImage: - // Off the reduced path (BRIEF §4) and outside P5b's measured 25: its emit-table slot - // is Fatal{UnmigratedVerb} on the client (contract §7 class C, wave 3 / P9). - return false; + return m_verbs != nullptr && + m_verbs->OnGetTextureImage(*static_cast(payload), seq, m_replies); // ---- the five class-B verbs: no MGPipeApply* exists, so v1's sink or nothing ------- case MGPWireOp::Blit: @@ -2205,6 +2226,8 @@ namespace MobileGL::MG_Remote::Wire { case MGPWireOp::BindStreamOutput: return m_verbs != nullptr && m_verbs->OnBindStreamOutput(*static_cast(payload)); + case MGPWireOp::DeleteStreamOutput: + return m_verbs && m_verbs->OnDeleteStreamOutput(*static_cast(payload)); case MGPWireOp::SetStorageBlockBinding: { // The block name is the ONE string on the wire (CONTRACT-P5B.md i1): a kHasBlob diff --git a/MobileGL/MG_Remote/Wire/PipeWireCodec.h b/MobileGL/MG_Remote/Wire/PipeWireCodec.h index 2bf944c3..53aafabf 100644 --- a/MobileGL/MG_Remote/Wire/PipeWireCodec.h +++ b/MobileGL/MG_Remote/Wire/PipeWireCodec.h @@ -445,6 +445,13 @@ namespace MobileGL::MG_Remote::Wire { // exclusion list), so inventing a consumer for them would be building a semantics nobody // can test this phase. Their arms validate both tails - which is the part a later phase // must not have to re-derive - and return false. + struct QueryResultReply { + Uint64 Value = 0; + Uint32 Produced = 0; + Uint32 Reserved = 0; + }; + static_assert(sizeof(QueryResultReply) == 16); + class WireVerbSink { public: virtual ~WireVerbSink() = default; @@ -453,6 +460,15 @@ namespace MobileGL::MG_Remote::Wire { virtual Bool OnFenceStatus(const MG_Pipe::MGPHandleOnly&, Uint32&) { return false; } virtual Bool OnFenceWait(const MG_Pipe::MGPFenceWait&, Uint32&) { return false; } virtual Bool OnFenceWaitServer(const MG_Pipe::MGPFenceWait&) { return false; } + virtual Bool OnQueryCreate(const MG_Pipe::MGPQueryDesc&) { return false; } + virtual Bool OnQueryBegin(const MG_Pipe::MGPQueryDesc&) { return false; } + virtual Bool OnQueryEnd(const MG_Pipe::MGPQueryDesc&) { return false; } + virtual Bool OnQueryCounter(const MG_Pipe::MGPQueryDesc&) { return false; } + virtual Bool OnQueryAvailable(const MG_Pipe::MGPHandleOnly&, Uint32&) { return false; } + virtual Bool OnQueryResult(const MG_Pipe::MGPQueryResultRequest&, QueryResultReply&) { return false; } + virtual Bool OnQueryDestroy(const MG_Pipe::MGPHandleOnly&) { return false; } + virtual Bool OnQueryTimestamp(const MG_Pipe::MGPTimestampRequest&, Int64&) { return false; } + virtual Bool OnDeleteStreamOutput(const MG_Pipe::MGPStreamOutputBind&) { return false; } virtual Bool OnClear(const MG_Pipe::MGPClear& clear) { (void)clear; return false; @@ -474,6 +490,9 @@ namespace MobileGL::MG_Remote::Wire { (void)replies; return false; } + virtual Bool OnGetTextureImage(const MG_Pipe::MGPReadbackInfo&, Uint64, ReplySink*) { + return false; + } // `ranges` is info.NumDraws entries. `userIndices` is null unless the record set // kDrawHasUserIndices; `indirect` is null unless it set kDrawIsIndirect (P5b d1, // CONTRACT-P5B.md). The layout refuses a record that sets both, so at most one of the diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 509fc34c..9939b443 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -1455,6 +1455,13 @@ namespace MobileGL::MG_State { return it != m_transformFeedbackObjects.end() && it->second.everBound; } +#if MOBILEGL_BUILD_DISAGGREGATED + Uint64 GLContext::GetTransformFeedbackLifetimeId(Uint index) const { + const auto found = m_transformFeedbackObjects.find(index); + return found == m_transformFeedbackObjects.end() ? 0 : found->second.lifetimeId; + } +#endif + void GLContext::MarkTransformFeedbackObjectForDeletion(Uint index) { if (index == 0 || !m_transformFeedbackNames.IsValid(index)) return; // Deleting the bound object reverts to the default one (GL 4.6 core 13.2.1); diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index 8c8343fd..813ddcce 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -531,6 +531,9 @@ namespace MobileGL { // Never returns 0 - the counter starts at 1 so a zero-initialised memo slot cannot // be mistaken for a live object. Uint64 GetBoundTransformFeedbackLifetimeId() const { return m_boundTransformFeedbackLifetimeId; } +#if MOBILEGL_BUILD_DISAGGREGATED + Uint64 GetTransformFeedbackLifetimeId(Uint index) const; +#endif // Whether the object carrying this identity still has an OPEN capture span - one // that glBeginTransformFeedback started and glEndTransformFeedback has not closed, // paused or not. A backend that hands out a bounded set of per-object slots must diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index f9a571ad..48d5869c 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -156,7 +156,7 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) { // 13 + the five P5b-appended verbs (MG_Remote/CONTRACT-P5B.md): bind_shader_image, // patch_parameter, bind_stream_output, set_storage_block_binding, // copy_framebuffer_to_texture. - EXPECT_EQ(ClassCount(), 18u); + EXPECT_EQ(ClassCount(), 19u); } // A row nobody has migrated is null - which is exactly what "this subsystem has not been diff --git a/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp b/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp index d098c440..ecd1399a 100644 --- a/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp +++ b/MobileGL/MG_Test/Wire/PipeWireCodecTest.cpp @@ -3011,6 +3011,131 @@ TEST(FenceWireRoundTrip, NullNativeFenceUsesOnlyTheExistingMonolithFallback) { sink.SetBackend(nullptr); } +namespace { + int nativeQueryToken = 0; + Bool queryGpuReady = false; + Bool queryGenerated = false; + Uint32 queryReads = 0, queryEnds = 0, queryDeletes = 0; + constexpr Uint64 queryNativeValue = 0xfedcba9876543210ull; + + void InstallQueryProbe(FenceProbeBackend& backend) { + InstallFenceProbe(backend); + queryGpuReady = queryGenerated = false; + queryReads = queryEnds = queryDeletes = 0; + backend.Table.GL.BeginXfbPrimitivesQuery = +[](Bool generated) -> MG_Backend::BackendQueryHandle { + queryGenerated = generated; + return &nativeQueryToken; + }; + backend.Table.GL.EndXfbPrimitivesQuery = +[](MG_Backend::BackendQueryHandle query) { + EXPECT_EQ(query, &nativeQueryToken); + ++queryEnds; + }; + backend.Table.GL.IsQueryResultAvailable = +[](MG_Backend::BackendQueryHandle query) -> Bool { + EXPECT_EQ(query, &nativeQueryToken); + return queryGpuReady; + }; + backend.Table.GL.GetQueryResult64 = +[](MG_Backend::BackendQueryHandle query, Bool, Uint64* value) -> Bool { + EXPECT_EQ(query, &nativeQueryToken); + ++queryReads; + *value = queryNativeValue; + return true; + }; + backend.Table.GL.DeleteBackendQuery = +[](MG_Backend::BackendQueryHandle query) { + EXPECT_EQ(query, &nativeQueryToken); + ++queryDeletes; + }; + backend.Table.GL.ClientWaitSync = +[](MG_Backend::BackendSyncHandle sync, GLbitfield flags, + GLuint64 timeout) -> GLenum { + EXPECT_EQ(sync, &nativeFenceToken); + fenceFlags = flags; + fenceTimeout = timeout; + return queryGpuReady ? GL_CONDITION_SATISFIED : GL_TIMEOUT_EXPIRED; + }; + } +} + +TEST(QueryWireRoundTrip, PrimitiveQueriesPreservePendingStateNativeResultsAndIdentity) { + Wire2 wire; + FenceProbeBackend backend; + InstallQueryProbe(backend); + Server::ServerVerbSink sink; + sink.SetBackend(&backend); + wire.Decoder().SetVerbSink(&sink); + auto send = [&](MGPWireOp op, const auto& payload) { + const auto seq = wire.Encoder().EncodeRecord(op, &payload, sizeof(payload)); + EXPECT_NE(seq, kInvalidSeq); + bool applied = false; + EXPECT_TRUE(wire.PumpOne(&applied)); + EXPECT_TRUE(applied); + return seq; + }; + const MGPQueryDesc desc{{51, 0}, GL_PRIMITIVES_GENERATED, 0}; + send(MGPWireOp::QueryCreate, desc); + send(MGPWireOp::QueryBegin, desc); + EXPECT_TRUE(queryGenerated); + send(MGPWireOp::QueryEnd, desc); + EXPECT_EQ(queryEnds, 1u); + MGPQueryResultRequest request{}; + request.Query = desc.Query; + const auto pendingSeq = send(MGPWireOp::QueryResult, request); + ASSERT_EQ(wire.Answers().All.back().Bytes.size(), sizeof(QueryResultReply)); + QueryResultReply reply{}; + std::memcpy(&reply, wire.Answers().All.back().Bytes.data(), sizeof(reply)); + EXPECT_EQ(wire.Answers().All.back().Seq, pendingSeq); + EXPECT_EQ(wire.Answers().All.back().Status, ReplySink::kStatusOk); + EXPECT_EQ(reply.Produced, 0u); + EXPECT_EQ(queryReads, 0u) << "NO_WAIT must not call a potentially blocking native result read"; + EXPECT_EQ(fenceTimeout, 0u); + queryGpuReady = true; + request.Wait = 1; + send(MGPWireOp::QueryResult, request); + std::memcpy(&reply, wire.Answers().All.back().Bytes.data(), sizeof(reply)); + EXPECT_EQ(reply.Produced, 1u); + EXPECT_EQ(reply.Value, queryNativeValue); + EXPECT_EQ(queryReads, 1u); + EXPECT_EQ(fenceTimeout, GL_TIMEOUT_IGNORED); + EXPECT_EQ(fenceFlags, GL_SYNC_FLUSH_COMMANDS_BIT); + const MGPHandleOnly handle{desc.Query, static_cast(MGPipeKind::Query), 0}; + send(MGPWireOp::QueryAvailable, handle); + Uint32 available = 0; + ASSERT_EQ(wire.Answers().All.back().Bytes.size(), sizeof(available)); + std::memcpy(&available, wire.Answers().All.back().Bytes.data(), sizeof(available)); + EXPECT_EQ(available, 1u); + send(MGPWireOp::QueryDestroy, handle); + EXPECT_EQ(queryDeletes, 1u); + EXPECT_EQ(fenceDeletes, 1u); + const MGPQueryDesc replacement{{51, 1}, GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, 0}; + send(MGPWireOp::QueryCreate, replacement); + send(MGPWireOp::QueryBegin, replacement); + EXPECT_FALSE(queryGenerated); + sink.SetBackend(nullptr); + EXPECT_EQ(queryEnds, 2u); + EXPECT_EQ(queryDeletes, 2u); +} + +TEST(QueryWireRoundTrip, FailedNativeCreationDoesNotBecomeAnAvailableZeroResult) { + FenceProbeBackend backend; + InstallQueryProbe(backend); + backend.Table.GL.BeginXfbPrimitivesQuery = +[](Bool) -> MG_Backend::BackendQueryHandle { return nullptr; }; + Server::ServerVerbSink sink; + sink.SetBackend(&backend); + const MGPQueryDesc desc{{7, 0}, GL_PRIMITIVES_GENERATED, 0}; + ASSERT_TRUE(sink.OnQueryCreate(desc)); + ASSERT_TRUE(sink.OnQueryBegin(desc)); + ASSERT_TRUE(sink.OnQueryEnd(desc)); + Uint32 available = 1; + EXPECT_TRUE(sink.OnQueryAvailable({desc.Query, static_cast(MGPipeKind::Query), 0}, available)); + EXPECT_EQ(available, 0u); + MGPQueryResultRequest request{}; + request.Query = desc.Query; + request.Wait = 1; + QueryResultReply reply{}; + EXPECT_TRUE(sink.OnQueryResult(request, reply)); + EXPECT_EQ(reply.Produced, 0u); + EXPECT_EQ(queryReads, 0u); + sink.SetBackend(nullptr); +} + #if MGTEST_HAVE_FORK TEST(FenceWireRoundTrip, DestroyedAndRecycledWireHandlesNeverReachTheBackend) { const auto r = RunInChild([] { diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index ad26faf5..d646b7c0 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -181,8 +181,8 @@ namespace { TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) { // P5 baseline five + f1 eleven + i1 seven + t2 six emitted slots. EXPECT_EQ(LocallyAnsweredSlotCount(), 2u); - EXPECT_EQ(ImplementedVerbCount(), 54u); - EXPECT_EQ(UnmigratedSlotCount(), 15u); + EXPECT_EQ(ImplementedVerbCount(), 68u); + EXPECT_EQ(UnmigratedSlotCount(), 1u); EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(), kRemoteEmitSlotCount); } @@ -270,8 +270,9 @@ TEST(RemoteEmitTable, DeleteTransformFeedbackHasNoRowAndStillAbortsByItsOwnName) // failed. const ChildResult r = RunInChild([] { RemoteEmitTable().GL.DeleteTransformFeedback(7); }); ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; - EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"DeleteTransformFeedback\"}"), std::string::npos) + EXPECT_NE(r.Log.find("Fatal{NoClientSession, \"DeleteTransformFeedback\"}"), std::string::npos) << r.Log; + EXPECT_EQ(r.Log.find("Fatal{UnmigratedVerb"), std::string::npos) << r.Log; } #endif // MGTEST_HAVE_FORK @@ -326,12 +327,9 @@ TEST(RemoteEmitTable, AnUnmigratedSlotAbortsAndNamesItself) { // GetTexImage is the wave-3 tail (CONTRACT-P5B.md §7): no P5b package flips it, so this // case keeps its subject across the four P5b landings. (It was DrawElements until d1 made // that a class-B emitter.) - const ChildResult r = RunInChild([] { - RemoteEmitTable().GL.GetTexImage(0x0DE1 /*GL_TEXTURE_2D*/, 0, 0x1908 /*GL_RGBA*/, - 0x1401 /*GL_UNSIGNED_BYTE*/, nullptr); - }); + const ChildResult r = RunInChild([] { RemoteEmitTable().SetSwapInterval(1); }); ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; - EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"GetTexImage\"}"), std::string::npos) << r.Log; + EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"SetSwapInterval\"}"), std::string::npos) << r.Log; } TEST(RemoteEmitTable, EachUnmigratedSlotNamesItsOwnSlot) { @@ -383,7 +381,7 @@ TEST(RemoteEmitTable, TheSevenI1SlotsAreClassBAndAreNotTheFatalThunk) { // assignment would look like (class C is assigned FIRST in BuildRemoteEmitTable precisely so // that the mistake is loud rather than null). const MG_Backend::GlobalBackendFunctionsTable& table = RemoteEmitTable(); - const void* fatal = reinterpret_cast(table.GL.GetTexImage); // wave-3 tail, class C + const void* fatal = reinterpret_cast(table.SetSwapInterval); ASSERT_NE(fatal, nullptr); const void* const i1[] = { reinterpret_cast(table.GL.BindImageTexture),