[Feat] (MG_Remote): lower named framebuffer blits through scoped wire bindings

This commit is contained in:
2026-09-16 13:44:35 -04:00
parent 12905a7eb8
commit 56c6e49d1d
6 changed files with 181 additions and 12 deletions
@@ -2159,3 +2159,23 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
) )
endforeach() endforeach()
endif() endif()
# P5b measured named-blit blockers, exercising both backend bound-form lowerings.
if (MOBILEGL_BUILD_DISAGGREGATED)
foreach(namedBlitBackend DirectGLES DirectVulkan)
mgl_itest_join_environment(MGL_ITEST_NAMED_BLIT_ENVIRONMENT
"MOBILEGL_BACKEND_TYPE=${namedBlitBackend}" "MOBILEGL_TRANSPORT=inproc"
"MOBILEGL_ITEST_REQUIRE_GPU=1" "MGITEST_SPLIT_LANE=1"
${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_COMMON_ENV})
foreach(namedBlitCase NamedBlitPreservesBindingsAndRestoresNextVerbsPixels NamedBlitDefaultEndpointPixels)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "${namedBlitBackend}.Split.NamedBlit."
TEST_FILTER "F1WireScenario.${namedBlitCase}"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-split"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${MGL_ITEST_NAMED_BLIT_ENVIRONMENT}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p5b-${namedBlitBackend}-${namedBlitCase}.log")
endforeach()
endforeach()
endif()
@@ -219,4 +219,85 @@ TEST_F(F1WireScenario, GenerateMipmapPixels) {
const int expected[4] = {64, 128, 191, 255}; 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"; for (int i = 0; i < 4; ++i) EXPECT_NEAR(pixel[i], expected[i], 1) << "F1.GenerateMipmap.pixels";
} }
TEST_F(F1WireScenario, NamedBlitPreservesBindingsAndRestoresNextVerbsPixels) {
if (!Ready()) return;
Attach(GL_RGBA8);
glClearColor(1, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
GLuint fbos[3]{}, textures[3]{};
glGenFramebuffers(3, fbos);
glGenTextures(3, textures);
for (int i = 0; i < 3; ++i) {
glBindFramebuffer(GL_FRAMEBUFFER, fbos[i]);
glBindTexture(GL_TEXTURE_2D, textures[i]);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 8, 8);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textures[i], 0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
glClearColor(0, i == 1 ? 1 : 0, 1, 1);
glClear(GL_COLOR_BUFFER_BIT);
}
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbos[1]);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbos[2]);
const auto before = PeekSplitRuntime().emitSeq;
glBlitNamedFramebuffer(fbo, fbos[0], 0, 0, 8, 8, 0, 0, 8, 8, GL_COLOR_BUFFER_BIT, GL_NEAREST);
EXPECT_GT(PeekSplitRuntime().emitSeq, before);
GLint read = 0, draw = 0;
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &read);
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &draw);
EXPECT_EQ(read, GLint(fbos[1]));
EXPECT_EQ(draw, GLint(fbos[2]));
// No intervening bind: this must clear the restored draw FBO, not the DSA destination.
glClearColor(1, 0, 1, 1);
glClear(GL_COLOR_BUFFER_BIT);
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbos[2]);
std::array<GLubyte, 4> pixel{};
glReadPixels(2, 3, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data());
EXPECT_EQ(pixel, (std::array<GLubyte, 4>{255, 0, 255, 255})) << "named blit restored draw before clear";
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbos[0]);
glReadPixels(2, 3, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data());
EXPECT_EQ(pixel, (std::array<GLubyte, 4>{255, 0, 0, 255})) << "unbound named blit copied source";
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbos[1]);
glBlitFramebuffer(0, 0, 8, 8, 0, 0, 8, 8, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbos[2]);
glReadPixels(2, 3, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data());
EXPECT_EQ(pixel, (std::array<GLubyte, 4>{0, 255, 255, 255})) << "ordinary blit follows restored bindings";
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glDeleteFramebuffers(3, fbos);
glDeleteTextures(3, textures);
}
TEST_F(F1WireScenario, NamedBlitDefaultEndpointPixels) {
if (!Ready()) return;
Attach(GL_RGBA8);
glClearColor(1, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClearColor(0, 0, 1, 1);
glClear(GL_COLOR_BUFFER_BIT);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glBlitNamedFramebuffer(fbo, 0, 0, 0, 8, 8, 0, 0, 8, 8, GL_COLOR_BUFFER_BIT, GL_NEAREST);
GLint read = 0, draw = 0;
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &read);
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &draw);
EXPECT_EQ(read, GLint(fbo));
EXPECT_EQ(draw, GLint(fbo));
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
std::array<GLubyte, 4> pixel{};
glReadPixels(2, 3, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data());
EXPECT_EQ(pixel, (std::array<GLubyte, 4>{255, 0, 0, 255})) << "default draw endpoint";
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glClearColor(0, 1, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
glBlitNamedFramebuffer(0, fbo, 0, 0, 8, 8, 0, 0, 8, 8, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &read);
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &draw);
EXPECT_EQ(read, GLint(fbo));
EXPECT_EQ(draw, GLint(fbo));
glReadPixels(2, 3, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data());
EXPECT_EQ(pixel, (std::array<GLubyte, 4>{255, 0, 0, 255})) << "default read endpoint";
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
}
} // namespace MGITest } // namespace MGITest
+15
View File
@@ -404,6 +404,21 @@ the user-index span.
--- ---
13. **Measured named blits use the existing `blit` row with a scoped client binding override.**
P5b's next trace census reaches `BlitNamedFramebuffer` in iris-BSL and
improved-transparency on both backends. The client saves its read/draw binding objects,
binds the named arguments in the client shadow only, validates `BlitNamedFramebuffer`,
and emits the original rectangles/mask/filter plus both original `MGPBlit` handles.
The barrier holds those bindings until the server's bound `BlitFramebuffer` finishes;
then RAII restores both client bindings. Their version changes make the next ordinary
verb republish the restored state. The existing classified BARRIER-PULLED accessors
remain the sole frontend read path; no pointer is added to the record or to a side channel,
and no driver call occurs on the client. Name 0 carries `kMGPipeDefaultFramebuffer`.
This adds no opcode and supersedes only named-blit's wave-3 deferral. *Overturned by:*
removing the lockstep barrier or adding process separation; then both framebuffer
handles need server-only resolution before dispatch. Pixel/binding gates cover unbound
endpoints, default endpoints and ordinary clear/blit immediately after restoration.
## §7 The 71 slots after P5b's contract commit, and after the four packages ## §7 The 71 slots after P5b's contract commit, and after the four packages
On this head the partition is CONTRACT-P5 §7's, unchanged: **A = 2, B = 5, C = 64** On this head the partition is CONTRACT-P5 §7's, unchanged: **A = 2, B = 5, C = 64**
+59 -8
View File
@@ -10,7 +10,7 @@
// //
// THE PARTITION IS CONTRACT-P5.md §7's AND IS NOT RE-DERIVED HERE (R-15, ID-12): // THE PARTITION IS CONTRACT-P5.md §7's AND IS NOT RE-DERIVED HERE (R-15, ID-12):
// class A 2 slots answered locally from the caps mirror, never emitted, never Fatal // class A 2 slots answered locally from the caps mirror, never emitted, never Fatal
// class B 48 slots emitted; class C 21 slots name their unmigrated verb. // class B 49 slots emitted; class C 20 slots name their unmigrated verb.
// The three counts are static_asserted to sum to kRemoteEmitSlotCount below, so a slot that // The three counts are static_asserted to sum to kRemoteEmitSlotCount below, so a slot that
// changes class without changing the arithmetic is a build break rather than a behaviour // changes class without changing the arithmetic is a build break rather than a behaviour
// change nobody reviewed. // change nobody reviewed.
@@ -60,6 +60,7 @@
#include "WireTables.h" #include "WireTables.h"
#include <MG_Impl/Pipe/FramebufferEmit.h> #include <MG_Impl/Pipe/FramebufferEmit.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include <MG_Impl/Pipe/TextureEmit.h> #include <MG_Impl/Pipe/TextureEmit.h>
namespace MobileGL::MG_Remote::Client { namespace MobileGL::MG_Remote::Client {
@@ -716,7 +717,7 @@ namespace MobileGL::MG_Remote::Client {
MG_Pipe::MGPBlit record{}; MG_Pipe::MGPBlit record{};
// Same reasoning as Clear's Fbo: the read and draw bindings are gPipeInputs', set // Same reasoning as Clear's Fbo: the read and draw bindings are gPipeInputs', set
// by MGP_FILL(BlitFramebuffer) at GL_Framebuffer.cpp:660 and held still by the // by MGP_FILL(BlitFramebuffer) at GL_Framebuffer.cpp:660 and held still by the
// barrier. glBlitNamedFramebuffer, which DOES name two framebuffers, is class C. // barrier. The named form below carries both handles after a scoped binding override.
record.ReadFbo = MG_Pipe::kMGPipeNullHandle; record.ReadFbo = MG_Pipe::kMGPipeNullHandle;
record.DrawFbo = MG_Pipe::kMGPipeNullHandle; record.DrawFbo = MG_Pipe::kMGPipeNullHandle;
record.SrcX0 = srcX0; record.SrcX0 = srcX0;
@@ -733,6 +734,57 @@ namespace MobileGL::MG_Remote::Client {
nullptr, 0, nullptr); nullptr, 0, nullptr);
} }
// DSA blits use the existing bound-form backend while the verb barrier holds.
// Only client shadow bindings change here; no driver call or frontend pointer crosses
// the wire. Bind() bumps their versions on entry and restore, so the next ordinary
// verb republishes the application's original bindings even if no GL bind intervenes.
class ScopedBlitBindings {
public:
using Fbo = MG_State::GLState::FramebufferObject;
ScopedBlitBindings(const SharedPtr<Fbo>& read, const SharedPtr<Fbo>& draw)
: m_read(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read)),
m_draw(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw)),
m_savedRead(m_read.GetBoundObject()), m_savedDraw(m_draw.GetBoundObject()) {
m_read.Bind(read);
m_draw.Bind(draw);
}
~ScopedBlitBindings() {
m_read.Bind(m_savedRead);
m_draw.Bind(m_savedDraw);
}
ScopedBlitBindings(const ScopedBlitBindings&) = delete;
ScopedBlitBindings& operator=(const ScopedBlitBindings&) = delete;
private:
BindingSlot<Fbo>& m_read;
BindingSlot<Fbo>& m_draw;
SharedPtr<Fbo> m_savedRead;
SharedPtr<Fbo> m_savedDraw;
};
void EmitBlitNamedFramebuffer(
const SharedPtr<MG_State::GLState::FramebufferObject>& read,
const SharedPtr<MG_State::GLState::FramebufferObject>& draw,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0,
GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) {
ClientSession& session = RequireSession("BlitNamedFramebuffer");
const ScopedBlitBindings bindings(read, draw);
// The frontend's first validate preceded this temporary lowering. Refresh both
// emitted framebuffer state and the existing BARRIER-PULLED binding fields now.
MG_Pipe::MGPipeValidateForVerb(MG_Pipe::MGPipeVerb::BlitNamedFramebuffer);
BeforeReadOnlyVerb();
MG_Pipe::MGPBlit record{};
record.ReadFbo = MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*read);
record.DrawFbo = MG_Pipe::MGPipeFramebufferEmitter::HandleFor(*draw);
record.SrcX0 = srcX0; record.SrcY0 = srcY0;
record.SrcX1 = srcX1; record.SrcY1 = srcY1;
record.DstX0 = dstX0; record.DstY0 = dstY0;
record.DstX1 = dstX1; record.DstY1 = dstY1;
record.Mask = static_cast<Uint32>(mask);
record.Filter = static_cast<Uint32>(filter);
session.EmitAndWait(MG_Pipe::MGPWireOp::Blit, &record, sizeof(record), nullptr, 0,
nullptr, 0, nullptr);
}
// How many bytes glReadPixels will pack for this rectangle, from the PACK half of the // How many bytes glReadPixels will pack for this rectangle, from the PACK half of the
// pixel-store state. // pixel-store state.
// //
@@ -1445,10 +1497,6 @@ namespace MobileGL::MG_Remote::Client {
// The wave-3 tail. SetSwapInterval is hand-written below (it is not a GL.* slot). // The wave-3 tail. SetSwapInterval is hand-written below (it is not a GL.* slot).
#define MGR_UNMIGRATED_TAIL_SLOTS(X) \ #define MGR_UNMIGRATED_TAIL_SLOTS(X) \
X(BlitNamedFramebuffer, void, \
(const SharedPtr<MG_State::GLState::FramebufferObject>&, \
const SharedPtr<MG_State::GLState::FramebufferObject>&, GLint, GLint, GLint, GLint, GLint, \
GLint, GLint, GLint, GLbitfield, GLenum)) \
X(GetTexImage, void, (GLenum, GLint, GLenum, GLenum, GLvoid*)) \ X(GetTexImage, void, (GLenum, GLint, GLenum, GLenum, GLvoid*)) \
X(GetTextureImage, void, \ X(GetTextureImage, void, \
(const SharedPtr<MG_State::GLState::ITextureObject>&, TextureUploadTarget, GLint, GLenum, \ (const SharedPtr<MG_State::GLState::ITextureObject>&, TextureUploadTarget, GLint, GLenum, \
@@ -1521,8 +1569,10 @@ namespace MobileGL::MG_Remote::Client {
constexpr Uint32 kEmittedSlotsI1 = 7; constexpr Uint32 kEmittedSlotsI1 = 7;
constexpr Uint32 kEmittedSlotsT2 = 6; constexpr Uint32 kEmittedSlotsT2 = 6;
constexpr Uint32 kEmittedSlotsF1 = 11; constexpr Uint32 kEmittedSlotsF1 = 11;
constexpr Uint32 kEmittedSlotsTail = 1; // BlitNamedFramebuffer
constexpr Uint32 kEmittedSlots = constexpr Uint32 kEmittedSlots =
kEmittedSlotsP5 + kEmittedSlotsD1 + kEmittedSlotsI1 + kEmittedSlotsT2 + kEmittedSlotsF1; kEmittedSlotsP5 + kEmittedSlotsD1 + kEmittedSlotsI1 + kEmittedSlotsT2 + kEmittedSlotsF1 +
kEmittedSlotsTail;
constexpr Uint32 kLocallyAnsweredSlots = 2; // GetIntegeri_v, IsTimerQuerySupported constexpr Uint32 kLocallyAnsweredSlots = 2; // GetIntegeri_v, IsTimerQuerySupported
// EACH PACKAGE'S OWNERSHIP, PINNED. A package that flips a slot removes one row and // EACH PACKAGE'S OWNERSHIP, PINNED. A package that flips a slot removes one row and
@@ -1533,7 +1583,7 @@ namespace MobileGL::MG_Remote::Client {
static_assert(kUnmigratedI1 + kEmittedSlotsI1 == 7, "i1 owns the 7 image/compute/barrier/copy/SSBO slots"); 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(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(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(kUnmigratedTail + kEmittedSlotsTail == 20, "the original wave-3 tail owns 20 slots");
static_assert(kUnmigratedSlots + kEmittedSlots == 69, "class B and C own 69 slots"); static_assert(kUnmigratedSlots + kEmittedSlots == 69, "class B and C own 69 slots");
static_assert(kLocallyAnsweredSlots + kEmittedSlots + kUnmigratedSlots == kRemoteEmitSlotCount, static_assert(kLocallyAnsweredSlots + kEmittedSlots + kUnmigratedSlots == kRemoteEmitSlotCount,
"the three classes no longer partition the 71 slots"); "the three classes no longer partition the 71 slots");
@@ -1589,6 +1639,7 @@ namespace MobileGL::MG_Remote::Client {
table.GL.MultiDrawElementsIndirectCount = &EmitMultiDrawElementsIndirectCount; table.GL.MultiDrawElementsIndirectCount = &EmitMultiDrawElementsIndirectCount;
table.GL.ReadPixels = &EmitReadPixels; table.GL.ReadPixels = &EmitReadPixels;
table.GL.BlitFramebuffer = &EmitBlitFramebuffer; table.GL.BlitFramebuffer = &EmitBlitFramebuffer;
table.GL.BlitNamedFramebuffer = &EmitBlitNamedFramebuffer;
table.Present = &EmitPresent; table.Present = &EmitPresent;
// ---- class B, P5b t2. Assigned AFTER the class-C block above, which is what makes // ---- class B, P5b t2. Assigned AFTER the class-C block above, which is what makes
// the flip a single-line change per slot: the Fatal thunk is overwritten, and a slot // the flip a single-line change per slot: the Fatal thunk is overwritten, and a slot
+4 -2
View File
@@ -138,8 +138,10 @@ namespace MobileGL::MG_Remote::Server {
if (table->GL.BlitFramebuffer == nullptr) return false; if (table->GL.BlitFramebuffer == nullptr) return false;
// Same ruling as OnClear's: the read and draw framebuffers are already bound by the // Same ruling as OnClear's: the read and draw framebuffers are already bound by the
// set_framebuffer_state records that preceded this one, so the unnamed entry point is // set_framebuffer_state records that preceded this one, so the unnamed entry point is
// the one that matches what the server's state actually is. BlitNamedFramebuffer needs // the one that matches what the server's state actually is. For the named form the
// two frontend SharedPtrs, which table 2 lists as uncarried. // client temporarily binds the two named objects and validates before emitting; its
// existing BARRIER-PULLED fields stay fixed until this call returns. ReadFbo/DrawFbo
// retain the original identities for the later handle-only endpoint (CONTRACT-P5B §6).
table->GL.BlitFramebuffer(blit.SrcX0, blit.SrcY0, blit.SrcX1, blit.SrcY1, blit.DstX0, table->GL.BlitFramebuffer(blit.SrcX0, blit.SrcY0, blit.SrcX1, blit.SrcY1, blit.DstX0,
blit.DstY0, blit.DstX1, blit.DstY1, blit.DstY0, blit.DstX1, blit.DstY1,
static_cast<GLbitfield>(blit.Mask), static_cast<GLbitfield>(blit.Mask),
+2 -2
View File
@@ -177,8 +177,8 @@ namespace {
TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) { TEST(RemoteEmitTable, TheThreeClassesPartitionAllSeventyOneSlots) {
// P5 baseline five + f1 eleven + i1 seven + t2 six emitted slots. // P5 baseline five + f1 eleven + i1 seven + t2 six emitted slots.
EXPECT_EQ(LocallyAnsweredSlotCount(), 2u); EXPECT_EQ(LocallyAnsweredSlotCount(), 2u);
EXPECT_EQ(ImplementedVerbCount(), 48u); EXPECT_EQ(ImplementedVerbCount(), 49u);
EXPECT_EQ(UnmigratedSlotCount(), 21u); EXPECT_EQ(UnmigratedSlotCount(), 20u);
EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(), EXPECT_EQ(LocallyAnsweredSlotCount() + ImplementedVerbCount() + UnmigratedSlotCount(),
kRemoteEmitSlotCount); kRemoteEmitSlotCount);
} }