From 7ffa4998ab2be3d9ecb2604635dedff8c5705bc9 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 16 Sep 2026 11:49:41 -0400 Subject: [PATCH] [Feat] (Remote): migrate f1 clear copy and mipmap verbs under inproc --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 16 + .../Scenarios/F1WireScenario.cpp | 222 +++++++++++++ MobileGL/MG_Remote/Client/EmitTables.cpp | 153 +++++++-- MobileGL/MG_Remote/Server/PipeApplier.cpp | 37 ++- MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 302 +++++++++++++++++- 5 files changed, 697 insertions(+), 33 deletions(-) create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index af59f21a..4438e24d 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -2010,3 +2010,19 @@ if (MOBILEGL_BUILD_DISAGGREGATED) check "${CMAKE_CTEST_COMMAND}" "${CMAKE_BINARY_DIR}") set_tests_properties(SplitLogPaths.PrivateAndDistinct PROPERTIES LABELS "integration-split") endif() + +# f1: split-only source keeps the monolith registry unchanged. +if (MOBILEGL_BUILD_DISAGGREGATED) + target_sources(MobileGLIntegrationTest PRIVATE Scenarios/F1WireScenario.cpp) + foreach(f1Slot ClearBufferfi ClearBufferfv ClearBufferiv ClearBufferuiv ClearNamedFramebufferfi ClearNamedFramebufferfv ClearNamedFramebufferiv ClearNamedFramebufferuiv CopyTexImage2D CopyTexSubImage2D GenerateMipmap) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.Split.F1." + TEST_FILTER "F1WireScenario.${f1Slot}Pixels" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS "integration-split" + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p5b-f1-${f1Slot}.log" + ) + endforeach() +endif() diff --git a/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp new file mode 100644 index 00000000..40cc64d3 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp @@ -0,0 +1,222 @@ +// f1 split-only pixel controls: every result depends on the migrated verb. +#include "../Harness/ScenarioFixture.h" +#include "../Harness/SplitRuntimePeek.h" +#include +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { +namespace { +class F1WireScenario : public ScenarioTest { +protected: + GLuint fbo = 0, texture = 0; + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + const auto why = SplitRuntimeSkipReason(); + if (!why.empty()) GTEST_SKIP() << why; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glDisable(GL_SCISSOR_TEST); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glDepthMask(GL_TRUE); + glStencilMask(~0u); + } + void TearDown() override { + if (!Ready()) return; + glBindFramebuffer(GL_FRAMEBUFFER, 0); + if (fbo) glDeleteFramebuffers(1, &fbo); + if (texture) glDeleteTextures(1, &texture); + ScenarioTest::TearDown(); + } + void Attach(GLenum format, GLenum attachment = GL_COLOR_ATTACHMENT0, int levels = 1) { + glTexStorage2D(GL_TEXTURE_2D, levels, format, 8, 8); + glFramebufferTexture2D(GL_FRAMEBUFFER, attachment, GL_TEXTURE_2D, texture, 0); + if (attachment == GL_DEPTH_STENCIL_ATTACHMENT) { glDrawBuffer(GL_NONE); glReadBuffer(GL_NONE); } + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE)) << "F1.setup.framebuffer"; + } +}; +} + +TEST_F(F1WireScenario, ClearBufferfvPixels) { + // Red once (executed, reverted): zero the clear record values; F1.ClearBufferfv.pixels fails. + if (!Ready()) return; + Attach(GL_RGBA8); + const GLfloat value[4] = {0.25f, 0.5f, 0.75f, 1.0f}; + const auto before = PeekSplitRuntime().emitSeq; + glClearBufferfv(GL_COLOR, 0, value); + ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearBufferfv.wire"; + GLubyte pixel[4]{}; + glReadPixels(2, 3, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearBufferfv.error"; + const int expected[4] = {64, 128, 191, 255}; + for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1) << "F1.ClearBufferfv.pixels"; +} + +TEST_F(F1WireScenario, ClearNamedFramebufferfvPixels) { + // Red once (executed, reverted): zero the clear record values; F1.ClearNamedFramebufferfv.pixels fails. + if (!Ready()) return; + Attach(GL_RGBA8); + const GLfloat value[4] = {0.25f, 0.5f, 0.75f, 1.0f}; + const auto before = PeekSplitRuntime().emitSeq; + glClearNamedFramebufferfv(fbo, GL_COLOR, 0, value); + ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearNamedFramebufferfv.wire"; + GLubyte pixel[4]{}; + glReadPixels(2, 3, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearNamedFramebufferfv.error"; + const int expected[4] = {64, 128, 191, 255}; + for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1) << "F1.ClearNamedFramebufferfv.pixels"; +} + +TEST_F(F1WireScenario, ClearBufferivPixels) { + // Red once (executed, reverted): zero the clear record values; F1.ClearBufferiv.pixels fails. + if (!Ready()) return; + Attach(GL_RGBA32I); + const GLint value[4] = {-37, 19, -11, 5}; + const auto before = PeekSplitRuntime().emitSeq; + glClearBufferiv(GL_COLOR, 0, value); + ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearBufferiv.wire"; + GLint pixel[4]{}; + glReadPixels(2, 3, 1, 1, GL_RGBA_INTEGER, GL_INT, pixel); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearBufferiv.error"; + for (int i = 0; i < 4; ++i) EXPECT_EQ(pixel[i], value[i]) << "F1.ClearBufferiv.pixels"; +} + +TEST_F(F1WireScenario, ClearNamedFramebufferivPixels) { + // Red once (executed, reverted): zero the clear record values; F1.ClearNamedFramebufferiv.pixels fails. + if (!Ready()) return; + Attach(GL_RGBA32I); + const GLint value[4] = {-37, 19, -11, 5}; + const auto before = PeekSplitRuntime().emitSeq; + glClearNamedFramebufferiv(fbo, GL_COLOR, 0, value); + ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearNamedFramebufferiv.wire"; + GLint pixel[4]{}; + glReadPixels(2, 3, 1, 1, GL_RGBA_INTEGER, GL_INT, pixel); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearNamedFramebufferiv.error"; + for (int i = 0; i < 4; ++i) EXPECT_EQ(pixel[i], value[i]) << "F1.ClearNamedFramebufferiv.pixels"; +} + +TEST_F(F1WireScenario, ClearBufferuivPixels) { + // Red once (executed, reverted): zero the clear record values; F1.ClearBufferuiv.pixels fails. + if (!Ready()) return; + Attach(GL_RGBA32UI); + const GLuint value[4] = {37, 19, 11, 5}; + const auto before = PeekSplitRuntime().emitSeq; + glClearBufferuiv(GL_COLOR, 0, value); + ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearBufferuiv.wire"; + GLuint pixel[4]{}; + glReadPixels(2, 3, 1, 1, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixel); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearBufferuiv.error"; + for (int i = 0; i < 4; ++i) EXPECT_EQ(pixel[i], value[i]) << "F1.ClearBufferuiv.pixels"; +} + +TEST_F(F1WireScenario, ClearNamedFramebufferuivPixels) { + // Red once (executed, reverted): zero the clear record values; F1.ClearNamedFramebufferuiv.pixels fails. + if (!Ready()) return; + Attach(GL_RGBA32UI); + const GLuint value[4] = {37, 19, 11, 5}; + const auto before = PeekSplitRuntime().emitSeq; + glClearNamedFramebufferuiv(fbo, GL_COLOR, 0, value); + ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearNamedFramebufferuiv.wire"; + GLuint pixel[4]{}; + glReadPixels(2, 3, 1, 1, GL_RGBA_INTEGER, GL_UNSIGNED_INT, pixel); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearNamedFramebufferuiv.error"; + for (int i = 0; i < 4; ++i) EXPECT_EQ(pixel[i], value[i]) << "F1.ClearNamedFramebufferuiv.pixels"; +} + +TEST_F(F1WireScenario, ClearBufferfiPixels) { + // Red once (executed, reverted): zero the clear record values; F1.ClearBufferfi.pixels fails. + if (!Ready()) return; + Attach(GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL_ATTACHMENT); + const auto before = PeekSplitRuntime().emitSeq; + glClearBufferfi(GL_DEPTH_STENCIL, 0, 0.375f, 91); + ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearBufferfi.wire"; + GLuint pixel = 0; + glReadPixels(2, 3, 1, 1, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, &pixel); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearBufferfi.error"; + EXPECT_EQ(pixel & 255u, 91u) << "F1.ClearBufferfi.pixels"; + EXPECT_NEAR(double(pixel >> 8) / 16777215.0, 0.375, 0.00001) << "F1.ClearBufferfi.pixels"; +} + +TEST_F(F1WireScenario, ClearNamedFramebufferfiPixels) { + // Red once (executed, reverted): zero the clear record values; F1.ClearNamedFramebufferfi.pixels fails. + if (!Ready()) return; + Attach(GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL_ATTACHMENT); + const auto before = PeekSplitRuntime().emitSeq; + glClearNamedFramebufferfi(fbo, GL_DEPTH_STENCIL, 0, 0.375f, 91); + ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ClearNamedFramebufferfi.wire"; + GLuint pixel = 0; + glReadPixels(2, 3, 1, 1, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, &pixel); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ClearNamedFramebufferfi.error"; + EXPECT_EQ(pixel & 255u, 91u) << "F1.ClearNamedFramebufferfi.pixels"; + EXPECT_NEAR(double(pixel >> 8) / 16777215.0, 0.375, 0.00001) << "F1.ClearNamedFramebufferfi.pixels"; +} + +TEST_F(F1WireScenario, CopyTexImage2DPixels) { + // Red once (executed, reverted): omit the copy sink call; F1.CopyTexImage2D.pixels fails. + if (!Ready()) return; + Attach(GL_RGBA8); + glClearColor(0.25f, 0.5f, 0.75f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + GLuint destination = 0; + glGenTextures(1, &destination); + glBindTexture(GL_TEXTURE_2D, destination); + + const auto before = PeekSplitRuntime().emitSeq; + glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 3, 4, 4, 0); + ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.CopyTexImage2D.wire"; + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, destination, 0); + GLubyte pixel[4]{}; + glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.CopyTexImage2D.error"; + const int expected[4] = {64, 128, 191, 255}; + for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1) << "F1.CopyTexImage2D.pixels"; + glDeleteTextures(1, &destination); +} + +TEST_F(F1WireScenario, CopyTexSubImage2DPixels) { + // Red once (executed, reverted): omit the copy sink call; F1.CopyTexSubImage2D.pixels fails. + if (!Ready()) return; + Attach(GL_RGBA8); + glClearColor(0.25f, 0.5f, 0.75f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + GLuint destination = 0; + glGenTextures(1, &destination); + glBindTexture(GL_TEXTURE_2D, destination); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 4, 4); + const auto before = PeekSplitRuntime().emitSeq; + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 1, 1, 2, 3, 2, 2); + ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.CopyTexSubImage2D.wire"; + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, destination, 0); + GLubyte pixel[4]{}; + glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.CopyTexSubImage2D.error"; + const int expected[4] = {64, 128, 191, 255}; + for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1) << "F1.CopyTexSubImage2D.pixels"; + glDeleteTextures(1, &destination); +} + +TEST_F(F1WireScenario, GenerateMipmapPixels) { + // Red once (executed, reverted): omit the mipmap sink call; F1.GenerateMipmap.pixels fails. + if (!Ready()) return; + Attach(GL_RGBA8, GL_COLOR_ATTACHMENT0, 4); + glClearColor(0.25f, 0.5f, 0.75f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + const auto before = PeekSplitRuntime().emitSeq; + glGenerateMipmap(GL_TEXTURE_2D); + ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.GenerateMipmap.wire"; + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 2); + GLubyte pixel[4]{}; + glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.GenerateMipmap.error"; + const int expected[4] = {64, 128, 191, 255}; + for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1) << "F1.GenerateMipmap.pixels"; +} +} // namespace MGITest diff --git a/MobileGL/MG_Remote/Client/EmitTables.cpp b/MobileGL/MG_Remote/Client/EmitTables.cpp index 6af52e8e..edae5dc2 100644 --- a/MobileGL/MG_Remote/Client/EmitTables.cpp +++ b/MobileGL/MG_Remote/Client/EmitTables.cpp @@ -39,6 +39,8 @@ #include #include "WireTables.h" +#include +#include namespace MobileGL::MG_Remote::Client { @@ -174,6 +176,123 @@ namespace MobileGL::MG_Remote::Client { nullptr, 0, nullptr); } + + // ---- f1: verbatim clear/copy/mipmap records (CONTRACT-P5B §2) ---- + void EmitF1Clear(const char* slot, MG_Pipe::MGPipeHandle fbo, GLenum buffer, + GLint drawbuffer, Uint8 valueClass, const void* value, + GLfloat depth = 0, GLint stencil = 0) { + auto& session = RequireSession(slot); + BeforeReadOnlyVerb(); + MG_Pipe::MGPClear record{}; + record.Fbo = fbo; + record.DrawBufferIndex = drawbuffer; + record.ValueClass = valueClass; + switch (buffer) { + case GL_COLOR: + record.Kind = MG_Pipe::kMGPipeClearKindColor; + std::memcpy(record.ColorValue, value, sizeof(record.ColorValue)); + break; + case GL_DEPTH: + record.Kind = MG_Pipe::kMGPipeClearKindDepth; + std::memcpy(&record.DepthValue, value, sizeof(record.DepthValue)); + break; + case GL_STENCIL: + record.Kind = MG_Pipe::kMGPipeClearKindStencil; + std::memcpy(&record.StencilValue, value, sizeof(record.StencilValue)); + break; + case GL_DEPTH_STENCIL: + record.Kind = MG_Pipe::kMGPipeClearKindDepthStencil; + record.DepthValue = depth; + record.StencilValue = stencil; + break; + default: UnmigratedVerbFatal(slot); + } + session.EmitAndWait(MG_Pipe::MGPWireOp::Clear, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + + void EmitClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) { + EmitF1Clear("ClearBufferfv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, + MG_Pipe::kMGPipeClearValueClassFloat, value); + } + void EmitClearNamedFramebufferfv(const SharedPtr& fbo, + GLenum buffer, GLint drawbuffer, const GLfloat* value) { + EmitF1Clear("ClearNamedFramebufferfv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), + buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassFloat, value); + } + + void EmitClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) { + EmitF1Clear("ClearBufferiv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, + MG_Pipe::kMGPipeClearValueClassInt, value); + } + void EmitClearNamedFramebufferiv(const SharedPtr& fbo, + GLenum buffer, GLint drawbuffer, const GLint* value) { + EmitF1Clear("ClearNamedFramebufferiv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), + buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassInt, value); + } + + void EmitClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) { + EmitF1Clear("ClearBufferuiv", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, + MG_Pipe::kMGPipeClearValueClassUint, value); + } + void EmitClearNamedFramebufferuiv(const SharedPtr& fbo, + GLenum buffer, GLint drawbuffer, const GLuint* value) { + EmitF1Clear("ClearNamedFramebufferuiv", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), + buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassUint, value); + } + + void EmitClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { + EmitF1Clear("ClearBufferfi", MG_Pipe::kMGPipeNullHandle, buffer, drawbuffer, + MG_Pipe::kMGPipeClearValueClassFloat, nullptr, depth, stencil); + } + void EmitClearNamedFramebufferfi(const SharedPtr& fbo, + GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) { + EmitF1Clear("ClearNamedFramebufferfi", MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*fbo), + buffer, drawbuffer, MG_Pipe::kMGPipeClearValueClassFloat, nullptr, depth, stencil); + } + const SharedPtr& F1BoundTexture(GLenum target) { + auto& ctx = *MG_State::pGLContext; + return ctx.GetTextureUnitObject(ctx.GetActiveTextureUnit()) + .GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target)).GetBoundObject(); + } + void EmitF1Copy(GLenum target, GLint level, GLenum format, GLint x, GLint y, + GLsizei width, GLsizei height, GLint xoffset, GLint yoffset, Bool subImage) { + auto& session = RequireSession(subImage ? "CopyTexSubImage2D" : "CopyTexImage2D"); + BeforeReadOnlyVerb(); + MG_Pipe::MGPCopyFromFramebuffer record{}; + record.Dst = MG_Pipe::MGPipeTextureEmitterInstance().FindTexture(*F1BoundTexture(target)); + record.Target = static_cast(target); + record.Level = level; + record.InternalFormat = format; + record.X = x; record.Y = y; + record.Width = width; record.Height = height; + record.XOffset = xoffset; record.YOffset = yoffset; + record.SubImage = subImage; + session.EmitAndWait(MG_Pipe::MGPWireOp::CopyFramebufferToTexture, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + void EmitCopyTexImage2D(GLenum target, GLint level, GLenum format, GLint x, GLint y, + GLsizei width, GLsizei height, GLint) { + EmitF1Copy(target, level, format, x, y, width, height, 0, 0, false); + } + void EmitCopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, + GLint x, GLint y, GLsizei width, GLsizei height) { + EmitF1Copy(target, level, 0, x, y, width, height, xoffset, yoffset, true); + } + void EmitGenerateMipmap(GLenum target) { + auto& session = RequireSession("GenerateMipmap"); + BeforeReadOnlyVerb(); + const auto& texture = F1BoundTexture(target); + MG_Pipe::MGPMipPlan record{}; + record.Res = MG_Pipe::MGPipeTextureEmitterInstance().FindTexture(*texture); + record.Target = static_cast(target); + record.BaseLevel = texture->GetLevelRange().x(); + const auto* mipmap = dynamic_cast(texture.get()); + record.LevelCount = mipmap ? mipmap->GetMipmapLevelCount() : 0; + session.EmitAndWait(MG_Pipe::MGPWireOp::GenerateMipmap, &record, sizeof(record), + nullptr, 0, nullptr, 0, nullptr); + } + void EmitDrawArrays(GLenum mode, GLint first, GLsizei count) { ClientSession& session = RequireSession("DrawArrays"); BeforeDrawVerb(); @@ -539,22 +658,7 @@ namespace MobileGL::MG_Remote::Client { X(BindTransformFeedback, void, (GLuint)) \ X(DeleteTransformFeedback, void, (GLuint)) -#define MGR_UNMIGRATED_F1_SLOTS(X) \ - X(ClearBufferfi, void, (GLenum, GLint, GLfloat, GLint)) \ - X(ClearBufferfv, void, (GLenum, GLint, const GLfloat*)) \ - X(ClearBufferuiv, void, (GLenum, GLint, const GLuint*)) \ - X(ClearBufferiv, void, (GLenum, GLint, const GLint*)) \ - X(ClearNamedFramebufferfv, void, \ - (const SharedPtr&, GLenum, GLint, const GLfloat*)) \ - X(ClearNamedFramebufferfi, void, \ - (const SharedPtr&, GLenum, GLint, GLfloat, GLint)) \ - X(ClearNamedFramebufferiv, void, \ - (const SharedPtr&, GLenum, GLint, const GLint*)) \ - X(ClearNamedFramebufferuiv, void, \ - (const SharedPtr&, GLenum, GLint, const GLuint*)) \ - X(CopyTexImage2D, void, (GLenum, GLint, GLenum, GLint, GLint, GLsizei, GLsizei, GLint)) \ - X(CopyTexSubImage2D, void, (GLenum, GLint, GLint, GLint, GLint, GLint, GLsizei, GLsizei)) \ - X(GenerateMipmap, void, (GLenum)) +#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) \ @@ -646,7 +750,7 @@ namespace MobileGL::MG_Remote::Client { constexpr Uint32 kEmittedSlotsD1 = 0; constexpr Uint32 kEmittedSlotsI1 = 0; constexpr Uint32 kEmittedSlotsT2 = 0; - constexpr Uint32 kEmittedSlotsF1 = 0; + constexpr Uint32 kEmittedSlotsF1 = 11; constexpr Uint32 kEmittedSlots = kEmittedSlotsP5 + kEmittedSlotsD1 + kEmittedSlotsI1 + kEmittedSlotsT2 + kEmittedSlotsF1; constexpr Uint32 kLocallyAnsweredSlots = 2; // GetIntegeri_v, IsTimerQuerySupported @@ -660,7 +764,7 @@ namespace MobileGL::MG_Remote::Client { 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 == 20, "the wave-3 tail is 20 slots and no P5b package owns one"); - static_assert(kUnmigratedSlots == 64, "CONTRACT-P5.md §7 class C is 64 slots at the P5b contract commit"); + static_assert(kUnmigratedSlots + kEmittedSlots == 69, "class B and C own 69 slots"); static_assert(kLocallyAnsweredSlots + kEmittedSlots + kUnmigratedSlots == kRemoteEmitSlotCount, "the three classes no longer partition the 71 slots"); @@ -698,6 +802,19 @@ namespace MobileGL::MG_Remote::Client { table.GL.BlitFramebuffer = &EmitBlitFramebuffer; table.Present = &EmitPresent; + // ---- f1 ---- + table.GL.ClearBufferfi = &EmitClearBufferfi; + table.GL.ClearBufferfv = &EmitClearBufferfv; + table.GL.ClearBufferiv = &EmitClearBufferiv; + table.GL.ClearBufferuiv = &EmitClearBufferuiv; + table.GL.ClearNamedFramebufferfi = &EmitClearNamedFramebufferfi; + table.GL.ClearNamedFramebufferfv = &EmitClearNamedFramebufferfv; + table.GL.ClearNamedFramebufferiv = &EmitClearNamedFramebufferiv; + table.GL.ClearNamedFramebufferuiv = &EmitClearNamedFramebufferuiv; + table.GL.CopyTexImage2D = &EmitCopyTexImage2D; + table.GL.CopyTexSubImage2D = &EmitCopyTexSubImage2D; + table.GL.GenerateMipmap = &EmitGenerateMipmap; + return table; } diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index cae264b6..e49cb2db 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -73,14 +73,16 @@ namespace MobileGL::MG_Remote::Server { if (table == nullptr) return false; const MG_Backend::GLFunctionsTable& gl = table->GL; - // THE FBO HANDLE IS NOT RESOLVED HERE, AND THAT IS THE RULING RATHER THAN AN OMISSION. - // MGPClear::Fbo names the framebuffer the clear belongs to, but the BINDING is already - // server state: set_framebuffer_state (op 33) arrives ahead of the clear and the - // applier has bound it. Re-resolving the handle to a frontend FramebufferObject here - // would need the SharedPtr the four ClearNamedFramebuffer* entries take - a frontend - // heap reference that table 2 lists as one of the six fields with no wire carrier. So - // P5 clears THE BOUND FRAMEBUFFER, which for the reduced path (default FBO) is exactly - // right, and the named form is P7's along with the handle it needs. + // 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(); + } switch (clear.Kind) { case kMGPClearKindWhole: if (gl.Clear == nullptr) return false; @@ -415,12 +417,25 @@ namespace MobileGL::MG_Remote::Server { // ---- f1 ---- Bool ServerVerbSink::OnGenerateMipmap(const MG_Pipe::MGPMipPlan& plan) { - (void)plan; - ServerUnmigratedVerbFatal("GenerateMipmap"); + const auto* table = Table("GenerateMipmap"); + if (table == nullptr || table->GL.GenerateMipmap == nullptr) return false; + table->GL.GenerateMipmap(plan.Target); + return true; } Bool ServerVerbSink::OnCopyFramebufferToTexture(const MG_Pipe::MGPCopyFromFramebuffer& copy) { - ServerUnmigratedVerbFatal(copy.SubImage ? "CopyTexSubImage2D" : "CopyTexImage2D"); + const auto* table = Table(copy.SubImage ? "CopyTexSubImage2D" : "CopyTexImage2D"); + if (table == nullptr) return false; + if (copy.SubImage) { + if (table->GL.CopyTexSubImage2D == nullptr) return false; + table->GL.CopyTexSubImage2D(copy.Target, copy.Level, copy.XOffset, copy.YOffset, + copy.X, copy.Y, copy.Width, copy.Height); + } else { + if (table->GL.CopyTexImage2D == nullptr) return false; + table->GL.CopyTexImage2D(copy.Target, copy.Level, copy.InternalFormat, + copy.X, copy.Y, copy.Width, copy.Height, 0); + } + return true; } // ----------------------------------------------------------------------------------- diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index acef2348..14f4e533 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -175,8 +175,8 @@ TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) { // table itself reports with - which is also what t1's arming condition reads - rather than // recomputed here, so a table that lost an emitter cannot look like one that never had it. EXPECT_EQ(LocallyAnsweredSlotCount(), 2u); - EXPECT_EQ(ImplementedVerbCount(), 5u); - EXPECT_EQ(UnmigratedSlotCount(), 64u); + EXPECT_EQ(ImplementedVerbCount(), 16u); + EXPECT_EQ(UnmigratedSlotCount(), 53u); EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(), kRemoteEmitSlotCount); } @@ -240,9 +240,9 @@ TEST(RemoteEmitTable, AnUnmigratedSlotAbortsAndNamesItself) { TEST(RemoteEmitTable, EachUnmigratedSlotNamesItsOwnSlot) { // The half the case above cannot state on its own: that the name in the message is the // slot's and not a constant. Two different slots, two different names. - const ChildResult r = RunInChild([] { RemoteEmitTable().GL.GenerateMipmap(0x0DE1); }); + const ChildResult r = RunInChild([] { RemoteEmitTable().GL.GetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); }); ASSERT_TRUE(DiedOfAbort(r)) << DescribeStatus(r) << "\n" << r.Log; - EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"GenerateMipmap\"}"), std::string::npos) << r.Log; + EXPECT_NE(r.Log.find("Fatal{UnmigratedVerb, \"GetTexImage\"}"), std::string::npos) << r.Log; EXPECT_EQ(r.Log.find("DrawElements"), std::string::npos) << "the Fatal message names a slot other than the one that was called:\n" << r.Log; @@ -847,6 +847,300 @@ TEST(RemoteReadback, AReplyIsScatteredOnlyWhenItIsOkAndExactlyTheReadsExtent) { } #include "RemoteClientControls.inc" +#include +#include +#include + +// f1: the installed emitters are decoded by a peer on the apply thread. +#if MGTEST_HAVE_FORK +namespace { +struct F1Peer : Codec::WireVerbSink { + MGPClear clear{}; + MGPCopyFromFramebuffer copy{}; + MGPMipPlan mip{}; + unsigned calls = 0; + Bool OnClear(const MGPClear& v) override { clear = v; ++calls; return true; } + Bool OnCopyFramebufferToTexture(const MGPCopyFromFramebuffer& v) override { copy = v; ++calls; return true; } + Bool OnGenerateMipmap(const MGPMipPlan& v) override { mip = v; ++calls; return true; } + void Install() { + if (Srv::ServerLoopInstance().RunOnApplyThread([](void* self) { + auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{}); + decoder.SetVerbSink(static_cast(self)); + return MOBILEGL_OK; + }, this) != MOBILEGL_OK) ::_exit(82); + } +}; +} + +TEST(RemoteF1, ClearBufferfvFieldsCross) { + // Red once (executed, reverted): zero the emitted clear values; F1.ClearBufferfv.fields fails. + const auto child = RunInChild([] { + StartControlSession(); + F1Peer peer; peer.Install(); + const GLfloat value[4] = {1, 7, 13, 23}; + + RemoteEmitTable().GL.ClearBufferfv(GL_COLOR, 3, value); + const auto& r = peer.clear; + if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassFloat || + r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 || + !MGPipeHandleIsNull(r.Fbo)) ::_exit(101); + ClientSessionInstance().Stop(); + }); + EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0) + << "F1.ClearBufferfv.fields: " << DescribeStatus(child) << child.Log; +} + +TEST(RemoteF1, ClearNamedFramebufferfvFieldsCross) { + // Red once (executed, reverted): zero the emitted clear values; F1.ClearNamedFramebufferfv.fields fails. + const auto child = RunInChild([] { + StartControlSession(); + F1Peer peer; peer.Install(); + const GLfloat value[4] = {1, 7, 13, 23}; + const auto fbo = MakeShared(73); + RemoteEmitTable().GL.ClearNamedFramebufferfv(fbo, GL_COLOR, 3, value); + const auto& r = peer.clear; + if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassFloat || + r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 || + r.Fbo != MGPipeFramebufferEmitter::HandleFor(*fbo)) ::_exit(101); + ClientSessionInstance().Stop(); + }); + EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0) + << "F1.ClearNamedFramebufferfv.fields: " << DescribeStatus(child) << child.Log; +} + +TEST(RemoteF1, ClearBufferivFieldsCross) { + // Red once (executed, reverted): zero the emitted clear values; F1.ClearBufferiv.fields fails. + const auto child = RunInChild([] { + StartControlSession(); + F1Peer peer; peer.Install(); + const GLint value[4] = {1, 7, 13, 23}; + + RemoteEmitTable().GL.ClearBufferiv(GL_COLOR, 3, value); + const auto& r = peer.clear; + if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassInt || + r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 || + !MGPipeHandleIsNull(r.Fbo)) ::_exit(101); + ClientSessionInstance().Stop(); + }); + EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0) + << "F1.ClearBufferiv.fields: " << DescribeStatus(child) << child.Log; +} + +TEST(RemoteF1, ClearNamedFramebufferivFieldsCross) { + // Red once (executed, reverted): zero the emitted clear values; F1.ClearNamedFramebufferiv.fields fails. + const auto child = RunInChild([] { + StartControlSession(); + F1Peer peer; peer.Install(); + const GLint value[4] = {1, 7, 13, 23}; + const auto fbo = MakeShared(73); + RemoteEmitTable().GL.ClearNamedFramebufferiv(fbo, GL_COLOR, 3, value); + const auto& r = peer.clear; + if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassInt || + r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 || + r.Fbo != MGPipeFramebufferEmitter::HandleFor(*fbo)) ::_exit(101); + ClientSessionInstance().Stop(); + }); + EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0) + << "F1.ClearNamedFramebufferiv.fields: " << DescribeStatus(child) << child.Log; +} + +TEST(RemoteF1, ClearBufferuivFieldsCross) { + // Red once (executed, reverted): zero the emitted clear values; F1.ClearBufferuiv.fields fails. + const auto child = RunInChild([] { + StartControlSession(); + F1Peer peer; peer.Install(); + const GLuint value[4] = {1, 7, 13, 23}; + + RemoteEmitTable().GL.ClearBufferuiv(GL_COLOR, 3, value); + const auto& r = peer.clear; + if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassUint || + r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 || + !MGPipeHandleIsNull(r.Fbo)) ::_exit(101); + ClientSessionInstance().Stop(); + }); + EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0) + << "F1.ClearBufferuiv.fields: " << DescribeStatus(child) << child.Log; +} + +TEST(RemoteF1, ClearNamedFramebufferuivFieldsCross) { + // Red once (executed, reverted): zero the emitted clear values; F1.ClearNamedFramebufferuiv.fields fails. + const auto child = RunInChild([] { + StartControlSession(); + F1Peer peer; peer.Install(); + const GLuint value[4] = {1, 7, 13, 23}; + const auto fbo = MakeShared(73); + RemoteEmitTable().GL.ClearNamedFramebufferuiv(fbo, GL_COLOR, 3, value); + const auto& r = peer.clear; + if (peer.calls != 1 || r.Kind != kMGPipeClearKindColor || r.ValueClass != kMGPipeClearValueClassUint || + r.DrawBufferIndex != 3 || std::memcmp(r.ColorValue, value, sizeof(value)) != 0 || + r.Fbo != MGPipeFramebufferEmitter::HandleFor(*fbo)) ::_exit(101); + ClientSessionInstance().Stop(); + }); + EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0) + << "F1.ClearNamedFramebufferuiv.fields: " << DescribeStatus(child) << child.Log; +} + +TEST(RemoteF1, ClearBufferfiFieldsCross) { + // Red once (executed, reverted): zero the emitted clear values; F1.ClearBufferfi.fields fails. + const auto child = RunInChild([] { + StartControlSession(); F1Peer peer; peer.Install(); + + RemoteEmitTable().GL.ClearBufferfi(GL_DEPTH_STENCIL, 0, 0.375f, 91); + const auto& r = peer.clear; + if (peer.calls != 1 || r.Kind != kMGPipeClearKindDepthStencil || r.DrawBufferIndex != 0 || + r.DepthValue != 0.375f || r.StencilValue != 91 || + !MGPipeHandleIsNull(r.Fbo)) ::_exit(101); + ClientSessionInstance().Stop(); + }); + EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0) + << "F1.ClearBufferfi.fields: " << DescribeStatus(child) << child.Log; +} + +TEST(RemoteF1, ClearNamedFramebufferfiFieldsCross) { + // Red once (executed, reverted): zero the emitted clear values; F1.ClearNamedFramebufferfi.fields fails. + const auto child = RunInChild([] { + StartControlSession(); F1Peer peer; peer.Install(); + const auto fbo = MakeShared(73); + RemoteEmitTable().GL.ClearNamedFramebufferfi(fbo, GL_DEPTH_STENCIL, 0, 0.375f, 91); + const auto& r = peer.clear; + if (peer.calls != 1 || r.Kind != kMGPipeClearKindDepthStencil || r.DrawBufferIndex != 0 || + r.DepthValue != 0.375f || r.StencilValue != 91 || + r.Fbo != MGPipeFramebufferEmitter::HandleFor(*fbo)) ::_exit(101); + ClientSessionInstance().Stop(); + }); + EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0) + << "F1.ClearNamedFramebufferfi.fields: " << DescribeStatus(child) << child.Log; +} + +namespace { +SharedPtr F1Texture() { + MG_State::pGLContext = MakeUnique(); + auto tex = MakeShared(91); + tex->SetInternalFormat(TextureInternalFormat::RGBA8); + for (Uint level = 0; level != 3; ++level) { + const Int size = 8 >> level; + tex->AllocateStorage(TextureUploadTarget::Texture2D, level, + MG_State::GLState::MipmapInput{IntVec3{size, size, 1}, static_cast(size * size * 4)}); + } + tex->SetBaseLevel(1); + MGPipeTextureEmitterInstance().AcquireTexture(tex->GetLifetimeId(), tex.get()); + MG_State::pGLContext->GetTextureUnitObject(0).GetBindingSlot(TextureTarget::Texture2D).Bind(tex); + return tex; +} +} + +TEST(RemoteF1, CopyTexImage2DFieldsCross) { + // Red once (executed, reverted): increment the emitted copy level; F1.CopyTexImage2D.fields fails. + const auto child = RunInChild([] { + StartControlSession(); F1Peer peer; peer.Install(); const auto tex = F1Texture(); + RemoteEmitTable().GL.CopyTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, -3, 4, 11, 13, 0); + const auto& r = peer.copy; + if (peer.calls != 1 || r.Dst != MGPipeTextureEmitterInstance().FindTexture(*tex) || + MGPipeHandleIsNull(r.Dst) || r.Target != GL_TEXTURE_2D || r.Level != 2 || + r.InternalFormat != GL_RGBA8 || r.X != -3 || r.Y != 4 || r.Width != 11 || r.Height != 13 || + r.XOffset != 0 || r.YOffset != 0 || r.SubImage != 0) ::_exit(101); + ClientSessionInstance().Stop(); + }); + EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0) + << "F1.CopyTexImage2D.fields: " << DescribeStatus(child) << child.Log; +} + +TEST(RemoteF1, CopyTexSubImage2DFieldsCross) { + // Red once (executed, reverted): increment the emitted copy level; F1.CopyTexSubImage2D.fields fails. + const auto child = RunInChild([] { + StartControlSession(); F1Peer peer; peer.Install(); const auto tex = F1Texture(); + RemoteEmitTable().GL.CopyTexSubImage2D(GL_TEXTURE_2D, 2, 5, 7, -3, 4, 11, 13); + const auto& r = peer.copy; + if (peer.calls != 1 || r.Dst != MGPipeTextureEmitterInstance().FindTexture(*tex) || + MGPipeHandleIsNull(r.Dst) || r.Target != GL_TEXTURE_2D || r.Level != 2 || + r.InternalFormat != 0 || r.X != -3 || r.Y != 4 || r.Width != 11 || r.Height != 13 || + r.XOffset != 5 || r.YOffset != 7 || r.SubImage != 1) ::_exit(101); + ClientSessionInstance().Stop(); + }); + EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0) + << "F1.CopyTexSubImage2D.fields: " << DescribeStatus(child) << child.Log; +} + +TEST(RemoteF1, GenerateMipmapFieldsCross) { + // Red once (executed, reverted): increment the emitted base level; F1.GenerateMipmap.fields fails. + const auto child = RunInChild([] { + StartControlSession(); F1Peer peer; peer.Install(); const auto tex = F1Texture(); + RemoteEmitTable().GL.GenerateMipmap(GL_TEXTURE_2D); + const auto& r = peer.mip; + if (peer.calls != 1 || r.Res != MGPipeTextureEmitterInstance().FindTexture(*tex) || + MGPipeHandleIsNull(r.Res) || r.Target != GL_TEXTURE_2D || r.BaseLevel != 1 || r.LevelCount != 3) + ::_exit(101); + ClientSessionInstance().Stop(); + }); + EXPECT_TRUE(WIFEXITED(child.Status) && WEXITSTATUS(child.Status) == 0) + << "F1.GenerateMipmap.fields: " << DescribeStatus(child) << child.Log; +} +#endif + + + +#if MGTEST_HAVE_FORK + +TEST(RemoteF1, UnboundNamedfvRefusesByName) { + // Red once (executed, reverted): disable the named-FBO refusal; its exact Fatal disappears. + const auto child = RunInChild([] { + CapsPeer backend; + Srv::ServerVerbSink sink; + sink.SetBackend(&backend); + MGPClear r{}; + r.Fbo = {701, 1}; + r.Kind = kMGPipeClearKindColor; + r.ValueClass = kMGPipeClearValueClassFloat; + sink.OnClear(r); + }); + ExpectNamedAbort(child, "Fatal{UnmigratedVerb, \"ClearNamedFramebufferfv+UNBOUND\"}"); +} + +TEST(RemoteF1, UnboundNamedivRefusesByName) { + // Red once (executed, reverted): disable the named-FBO refusal; its exact Fatal disappears. + const auto child = RunInChild([] { + CapsPeer backend; + Srv::ServerVerbSink sink; + sink.SetBackend(&backend); + MGPClear r{}; + r.Fbo = {701, 1}; + r.Kind = kMGPipeClearKindColor; + r.ValueClass = kMGPipeClearValueClassInt; + sink.OnClear(r); + }); + ExpectNamedAbort(child, "Fatal{UnmigratedVerb, \"ClearNamedFramebufferiv+UNBOUND\"}"); +} + +TEST(RemoteF1, UnboundNameduivRefusesByName) { + // Red once (executed, reverted): disable the named-FBO refusal; its exact Fatal disappears. + const auto child = RunInChild([] { + CapsPeer backend; + Srv::ServerVerbSink sink; + sink.SetBackend(&backend); + MGPClear r{}; + r.Fbo = {701, 1}; + r.Kind = kMGPipeClearKindColor; + r.ValueClass = kMGPipeClearValueClassUint; + sink.OnClear(r); + }); + ExpectNamedAbort(child, "Fatal{UnmigratedVerb, \"ClearNamedFramebufferuiv+UNBOUND\"}"); +} + +TEST(RemoteF1, UnboundNamedfiRefusesByName) { + // Red once (executed, reverted): disable the named-FBO refusal; its exact Fatal disappears. + const auto child = RunInChild([] { + CapsPeer backend; + Srv::ServerVerbSink sink; + sink.SetBackend(&backend); + MGPClear r{}; + r.Fbo = {701, 1}; + r.Kind = kMGPipeClearKindDepthStencil; + r.ValueClass = kMGPipeClearValueClassFloat; + sink.OnClear(r); + }); + ExpectNamedAbort(child, "Fatal{UnmigratedVerb, \"ClearNamedFramebufferfi+UNBOUND\"}"); +} +#endif int main(int argc, char** argv) { namespace fs = std::filesystem;