[MG_Backend, MG_IntegrationTest] (P7): a scaled multisample colour blit resolves into a scratch and blits that, and a multisample destination declines

CONTRACT-P7 §3.2's `multisample-blit-shape@P7`. The name covered two halves that
are not the same question.

The half with an arm: a multisample colour blit whose destination rectangle is a
different size from its source. vkCmdResolveImage carries one extent and one
offset per side, so it can express neither the scale nor the flip - which is why
this arm already resolved into a single-sample scratch at raw offsets and blitted
THAT for the flip. The two `width != |dx1-dx0|` clauses were the only thing
keeping the scale out of the same route, and the monolith arm reaches it the same
way (VulkanRenderer.cpp's `regionWasTransformed` branch). They are gone; the
scratch is still sized by the SOURCE rect, so only the second leg sees the scale.

The filter only started mattering with that scale - at 1:1 LINEAR and NEAREST read
the same texel - so the second leg now asks whether the format can be filtered
before it passes VK_FILTER_LINEAR, which is undefined behaviour on a format whose
optimalTilingFeatures lack SAMPLED_IMAGE_FILTER_LINEAR. GL ignores filter for a
multisample resolve anyway, so the fallback is NEAREST and not a refusal.

The half with no arm: a MULTISAMPLE DESTINATION. vkCmdCopyImage is the only
command that writes one, it takes a single extent, and GL 4.6 core 18.3.1 makes a
blit whose rectangles differ in size INVALID_OPERATION when either framebuffer is
multisampled. There is no picture to get right, so rule I (a): MGLOG_E_ONCE with
both rectangles and both sample counts, the error GL names, the destination left
alone, the session kept.

Two entries on each of the three Magma arms (`MsBlit.`), split-only because the
monolith arm drops the scale (its vkCmdResolveImage lands 1:1 at the destination
offset), so an equality between the arms would assert that the wire arm is as
wrong. The scale and the flip share one case over one source: dropping the scale
lands an 8x4 band in a 4x2 attachment, dropping the flip returns the right two
colours in the wrong order, and the second blit's expectation is the first's with
the rows swapped.

Red once (R-16, executed and reverted), twice:
  * restore the two size-equality clauses ->
    DirectVulkan.Split.MsBlit.…ScalesAndFlipsThroughTheResolveScratch dies as
    Fatal{UnmigratedVerb, "Magma:multisample-resolve-region"};
  * restore MagmaWireFatal("multisample-blit-shape@P7") ->
    DirectVulkan.Split.MsBlit.…DeclinesAndKeepsTheSession dies by that name.
Reverted, 6/6 green on the three arms.

Measured on the way in: the decline's GL error reaches the client with a LATER
reply, not with the blit call, so the case's glFinish is load-bearing - without it
the INVALID_OPERATION surfaced attached to the 1:1 resolve twenty lines down.

Gates: integration-magma-split 90 (+2), -spawn 69 (+2), -tcp 71 (+2),
integration-magma-full-split 523 (+2), ctest -L unit 2420, all green;
fatal_census 79 abort sites / 44 family words / 0 unmarked; link_ratchet
--assert-monotone unchanged at 186; G1 pull .text 0xa52203 with an nm
--defined-only list identical to p7-before/pull-syms.txt (30570 lines, 0 diffs) -
every line of this commit is inside WireFramebuffer.inc's
`#if MOBILEGL_BUILD_DISAGGREGATED` or in the test tree.
This commit is contained in:
2026-09-22 18:29:22 -04:00
parent 22761a692d
commit 452d33b578
3 changed files with 287 additions and 12 deletions
@@ -646,11 +646,23 @@
// A resolve cannot flip axes or convert RGBA to a BGRA/default
// attachment. Resolve into the source format first, then use
// the existing single-sample blit for those GL semantics.
//
// P7 wave 2-B2, CONTRACT-P7 §3.2 (`multisample-blit-shape`): AND IT CANNOT SCALE.
// vkCmdResolveImage carries ONE extent and one offset per side, so this arm used
// to refuse every rectangle whose destination was a different size from its
// source - a shape glBlitFramebuffer names, and one the monolith arm reaches by
// this very route (VulkanRenderer.cpp's `regionWasTransformed` branch resolves
// into a scratch and blits THAT). The two legs split the work the same way here:
// the resolve reads the source band AT RAW OFFSETS into a scratch sized by the
// SOURCE rect, and only the second leg - an ordinary vkCmdBlitImage on a
// single-sampled image, which is the one thing it is for - sees the scale and the
// flip. So the size equality drops out of this guard; what stays is the part
// neither leg can express (a rotated default source, a 3D source) or that names
// texels the source does not have.
const Int64 width = std::abs(static_cast<Int64>(sx1) - sx0);
const Int64 height = std::abs(static_cast<Int64>(sy1) - sy0);
if (!width || !height) return;
if (source.isDefault || source.is3D || width != std::abs(static_cast<Int64>(dx1) - dx0) ||
height != std::abs(static_cast<Int64>(dy1) - dy0) ||
if (!width || !height || dx0 == dx1 || dy0 == dy1) return;
if (source.isDefault || source.is3D ||
std::min(sx0, sx1) < 0 || std::min(sy0, sy1) < 0 ||
static_cast<Uint64>(std::max(sx0, sx1)) > source.extent.width ||
static_cast<Uint64>(std::max(sy0, sy1)) > source.extent.height)
@@ -708,9 +720,21 @@
destination.is3D ? 0u : destination.layer, 1};
const Int32 z = destination.is3D ? static_cast<Int32>(destination.layer) : 0;
copy.dstOffsets[0] = {dx0, dy0, z}; copy.dstOffsets[1] = {dx1, dy1, z + 1};
// The filter only started MATTERING with the scale above: at 1:1 LINEAR and
// NEAREST read the same texel, so nothing here ever asked whether the format
// could be filtered. VK_FILTER_LINEAR on a source whose optimalTilingFeatures
// lack SAMPLED_IMAGE_FILTER_LINEAR is undefined behaviour, and GL says filter
// is ignored for a multisample resolve in the first place - so a format that
// cannot be filtered takes NEAREST rather than the command being illegal.
VkFormatProperties scratchProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &scratchProperties);
const VkFilter blitFilter =
filter == GL_LINEAR && (scratchProperties.optimalTilingFeatures &
VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT)
? VK_FILTER_LINEAR : VK_FILTER_NEAREST;
vkCmdBlitImage(m_frameContext.GetCurrent().commandBuffer, resolved.image,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &copy, filter == GL_LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
1, &copy, blitFilter);
if (dst.Kind == MG_Pipe::kMGPipeSurfaceKindTexture)
m_textureManager->MarkWireTextureGpuWritten(destination.storage, destination.level, destination.layer, 1);
}
@@ -821,17 +845,41 @@
}
auto command = m_frameContext.GetCurrent().commandBuffer;
if (source.samples != VK_SAMPLE_COUNT_1_BIT || destination.samples != VK_SAMPLE_COUNT_1_BIT) {
// P7 wave 2-B2, CONTRACT-P7 §3.2: WHAT IS LEFT HERE IS A MULTISAMPLE DESTINATION.
// Every blit that resolves a multisample SOURCE into a single-sample destination
// returned above - the depth/stencil arm at the top of this lambda through
// ResolveWireDepthStencil, the colour arm below it through its resolve scratch -
// and both of those now carry their own scale and flip. So the samples of the two
// sides can only differ here by the DESTINATION being multisampled, which is the
// shape this arm declines rather than the one it performs.
const Int32 width = region.srcOffsets[1].x - region.srcOffsets[0].x;
const Int32 height = region.srcOffsets[1].y - region.srcOffsets[0].y;
if (width <= 0 || height <= 0 || width != region.dstOffsets[1].x - region.dstOffsets[0].x ||
height != region.dstOffsets[1].y - region.dstOffsets[0].y || source.format != destination.format)
MagmaWireFatal("multisample-blit-shape@P7");
if (source.samples != VK_SAMPLE_COUNT_1_BIT && destination.samples == VK_SAMPLE_COUNT_1_BIT &&
aspect == VK_IMAGE_ASPECT_COLOR_BIT) {
VkImageResolve resolve{region.srcSubresource, region.srcOffsets[0],
region.dstSubresource, region.dstOffsets[0], {static_cast<Uint32>(width), static_cast<Uint32>(height), 1}};
vkCmdResolveImage(command, source.image, srcLayout, destination.image, dstLayout, 1, &resolve);
} else if (source.samples == destination.samples) {
height != region.dstOffsets[1].y - region.dstOffsets[0].y || source.format != destination.format) {
// `multisample-blit-shape@P7` RETIRES: the half that had a shape neither
// vkCmdResolveImage nor vkCmdBlitImage could express is done above, and this
// is the half that has none - vkCmdCopyImage is the only command that writes
// a multisampled image, it takes one extent, and GL 4.6 core 18.3.1 makes a
// multisample blit whose rectangles differ in size (or whose formats differ)
// INVALID_OPERATION rather than a picture. Rule I (a): say it once, record
// the error GL names, leave the destination alone, keep the session.
MGLOG_E_ONCE("DirectVulkan::BlitWireFramebuffers: a %dx%d source rectangle "
"(format %d, %u sample(s)) cannot reach a %dx%d destination "
"rectangle (format %d, %u sample(s)) - a blit onto a multisample "
"attachment carries no scale, no flip and no format conversion; "
"declining the blit",
width, height, static_cast<Int>(source.format),
static_cast<Uint32>(source.samples),
region.dstOffsets[1].x - region.dstOffsets[0].x,
region.dstOffsets[1].y - region.dstOffsets[0].y,
static_cast<Int>(destination.format),
static_cast<Uint32>(destination.samples));
RecordTextureCopyError("BlitWireFramebuffers", ErrorCode::InvalidOperation,
"glBlitFramebuffer onto a multisampled draw framebuffer requires "
"identical source and destination rectangles and formats.");
return;
}
if (source.samples == destination.samples) {
VkImageCopy copy{region.srcSubresource, region.srcOffsets[0], region.dstSubresource,
region.dstOffsets[0], {static_cast<Uint32>(width), static_cast<Uint32>(height), 1}};
vkCmdCopyImage(command, source.image, srcLayout, destination.image, dstLayout, 1, &copy);
@@ -3080,6 +3080,26 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
${mglItestArmProperties}
ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MGITEST_MAGMA_FORCE_DEFAULT_BLIT_SCRATCH=${defaultBlitScratch}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-defaultblit${mglItestArmLogTag}-scratch${defaultBlitScratch}.log")
endforeach()
# ---- P7 wave 2-B2: `multisample-blit-shape@P7`'s retirement and its decline ----------
#
# SPLIT-ONLY, like the in-place copy pair below and for the same reason: the retired arm
# does something the monolith arm does not. A scaled multisample colour blit takes the
# monolith's `vkCmdResolveImage` at 1:1 into the destination's offset - the scale is
# dropped - so there is no `DirectVulkan.` reading to register these beside, and an
# equality between the two arms would be an assertion that the wire arm is as wrong.
#
# NO KNOB: every driver that can host a 4x multisample RGBA8 renderbuffer reaches both
# entries with the plain Fm environment, and one that cannot says so as a SKIP.
foreach(multisampleBlitCase MultisampleColorBlitScalesAndFlipsThroughTheResolveScratch
MultisampleBlitOntoAMultisampleDestinationDeclinesAndKeepsTheSession)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectVulkan.${mglItestArm}.MsBlit."
TEST_FILTER "F1WireScenario.${multisampleBlitCase}"
DISCOVERY_TIMEOUT 30
PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT}
${mglItestArmProperties}
ENVIRONMENT "${mglItestMagmaWireEnvironment}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/p7-msblit${mglItestArmLogTag}-${multisampleBlitCase}.log")
endforeach()
foreach(inPlaceCase CopyImageInPlaceAcrossLevelsAndLayers
CopyImageInPlaceOverlapDeclinesAndLeavesTheLevelAlone)
gtest_discover_tests(MobileGLIntegrationTest
@@ -1630,6 +1630,213 @@ TEST_F(F1WireScenario, ColorBlitToDefaultFromANonSampleableSourceDegrades) {
Gl().EndFrame();
}
namespace {
// An 8x4 RGBA8 colour renderbuffer at `samples`, bottom half red and top half green in GL row
// order. TWO HALVES AND NOT ONE COLOUR, for the reason the shader-mip case gives: a pass that
// loses GL's row order averages a flat fill back to the same number everywhere and a
// single-colour assertion calls that correct. Returns false when this driver cannot host the
// attachment, which is a skip and not a failure.
bool MakeMultisampleHalvesFbo(int samples, GLuint& fbo, GLuint& renderbuffer) {
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glGenRenderbuffers(1, &renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, 8, 4);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GLenum(GL_FRAMEBUFFER_COMPLETE)) return false;
while (glGetError() != GLenum(GL_NO_ERROR)) {}
glDisable(GL_SCISSOR_TEST);
glEnable(GL_SCISSOR_TEST);
glScissor(0, 0, 8, 2);
glClearColor(1, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
glScissor(0, 2, 8, 2);
glClearColor(0, 1, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_SCISSOR_TEST);
return true;
}
// A single-sample RGBA8 destination of the given size, cleared to a colour that appears in no
// assertion below - so "the blit did nothing" and "the blit landed the wrong band" read
// differently from each other.
void MakeSingleSampleTarget(int width, int height, const GLfloat (&clear)[4], GLuint& fbo, GLuint& tex) {
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width, height);
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
glClearColor(clear[0], clear[1], clear[2], clear[3]);
glClear(GL_COLOR_BUFFER_BIT);
}
} // namespace
// P7 wave 2-B2, CONTRACT-P7 §3.2: `multisample-blit-shape@P7` RETIRES.
//
// vkCmdResolveImage carries ONE extent and one offset per side. It can neither scale nor flip,
// so the wire arm refused every multisample colour blit whose destination rectangle was a
// different size from its source - which glBlitFramebuffer names, and which the MONOLITH arm
// already reaches by the route this package gives the wire arm (VulkanRenderer.cpp's
// `regionWasTransformed` branch: resolve into a scratch at raw offsets, then blit the scratch).
// The scratch was already here for the flip; the two size equalities were the only thing
// keeping the scale out.
//
// THE SCALE AND THE FLIP IN ONE CASE, over the same source, because they fail differently: a
// blit that dropped the scale lands a 8x4 band in a 4x2 attachment (and the driver refuses the
// command), while one that dropped the flip returns the right two colours in the wrong order.
// The second blit's expectation is the first's, rows swapped, so neither can pass the other's
// assertion.
//
// Red once (executed, reverted): restore the two `width != |dx1-dx0|` clauses in
// WireFramebuffer.inc's colour resolve arm and both blits die as
// Fatal{UnmigratedVerb, "Magma:multisample-resolve-region"}.
TEST_F(F1WireScenario, MultisampleColorBlitScalesAndFlipsThroughTheResolveScratch) {
if (!Ready()) return;
GLuint msFbo = 0, msRenderbuffer = 0;
if (!MakeMultisampleHalvesFbo(4, msFbo, msRenderbuffer)) {
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glDeleteFramebuffers(1, &msFbo);
glDeleteRenderbuffers(1, &msRenderbuffer);
GTEST_SKIP() << "this driver cannot host a 4x multisample RGBA8 renderbuffer";
}
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsBlitShape.setup";
// Half the source in each axis: every destination texel is the resolve of a 2x2 source box
// that lies wholly inside ONE half, so the boundary never falls inside a destination texel
// and the two colours cannot mix.
constexpr GLfloat kBlue[4]{0, 0, 1, 1};
GLuint dstFbo = 0, dstTexture = 0;
MakeSingleSampleTarget(4, 2, kBlue, dstFbo, dstTexture);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsBlitShape.target";
constexpr std::array<GLubyte, 4> kRed{255, 0, 0, 255};
constexpr std::array<GLubyte, 4> kGreen{0, 255, 0, 255};
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, dstFbo);
glDisable(GL_SCISSOR_TEST);
const auto before = PeekSplitRuntime().emitSeq;
glBlitFramebuffer(0, 0, 8, 4, 0, 0, 4, 2, GL_COLOR_BUFFER_BIT, GL_NEAREST);
EXPECT_GT(PeekSplitRuntime().emitSeq, before) << "F1.MsBlitShape.wire";
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsBlitShape.scaled.error";
glBindFramebuffer(GL_READ_FRAMEBUFFER, dstFbo);
for (int x = 0; x < 4; ++x) {
EXPECT_EQ(ReadOnePixel(x, 0), kRed) << "F1.MsBlitShape scaled bottom row, x=" << x;
EXPECT_EQ(ReadOnePixel(x, 1), kGreen) << "F1.MsBlitShape scaled top row, x=" << x;
}
// The same source, the same destination size, with the destination rectangle inverted on
// Y. The scratch holds the resolved band at its own origin either way; only the second leg
// sees the inversion, which is what vkCmdBlitImage's reversed offsets express.
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, dstFbo);
glClearColor(kBlue[0], kBlue[1], kBlue[2], kBlue[3]);
glClear(GL_COLOR_BUFFER_BIT);
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
glBlitFramebuffer(0, 0, 8, 4, 0, 2, 4, 0, GL_COLOR_BUFFER_BIT, GL_NEAREST);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsBlitShape.flipped.error";
glBindFramebuffer(GL_READ_FRAMEBUFFER, dstFbo);
for (int x = 0; x < 4; ++x) {
EXPECT_EQ(ReadOnePixel(x, 0), kGreen) << "F1.MsBlitShape flipped bottom row, x=" << x;
EXPECT_EQ(ReadOnePixel(x, 1), kRed) << "F1.MsBlitShape flipped top row, x=" << x;
}
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glDeleteFramebuffers(1, &dstFbo);
glDeleteTextures(1, &dstTexture);
glDeleteFramebuffers(1, &msFbo);
glDeleteRenderbuffers(1, &msRenderbuffer);
glBindTexture(GL_TEXTURE_2D, texture);
Gl().EndFrame();
}
// The surviving half of `multisample-blit-shape@P7`, and it is a DECLINE (rule I (a)).
//
// A MULTISAMPLE DESTINATION is the other side of the same guard and has no arm at all:
// vkCmdCopyImage is the only command that writes a multisampled image, it takes one extent, and
// GL 4.6 core 18.3.1 makes a blit whose rectangles differ in size INVALID_OPERATION when either
// framebuffer is multisampled - so there is no picture to get right, only a shape that used to
// end the session. What this case pins is that it no longer does, and that the destination is
// exactly as the clear left it.
//
// THE DESTINATION IS READ THROUGH A RESOLVE, because glReadPixels on a multisampled framebuffer
// is INVALID_OPERATION: the 1:1 resolve into a single-sample attachment is the retired arm
// above, so this case also proves the decline did not disturb the arm next to it.
//
// Red once (executed, reverted): restore MagmaWireFatal("multisample-blit-shape@P7") and this
// case dies as Fatal{UnmigratedVerb, "Magma:multisample-blit-shape@P7"}.
TEST_F(F1WireScenario, MultisampleBlitOntoAMultisampleDestinationDeclinesAndKeepsTheSession) {
if (!Ready()) return;
GLuint msFbo = 0, msRenderbuffer = 0;
glGenFramebuffers(1, &msFbo);
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
glGenRenderbuffers(1, &msRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, msRenderbuffer);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_RGBA8, 4, 2);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRenderbuffer);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GLenum(GL_FRAMEBUFFER_COMPLETE)) {
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glDeleteFramebuffers(1, &msFbo);
glDeleteRenderbuffers(1, &msRenderbuffer);
GTEST_SKIP() << "this driver cannot host a 4x multisample RGBA8 renderbuffer";
}
while (glGetError() != GLenum(GL_NO_ERROR)) {}
glDisable(GL_SCISSOR_TEST);
glClearColor(1, 0, 1, 1); // magenta: the colour the declined blit must leave behind
glClear(GL_COLOR_BUFFER_BIT);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsBlitDecline.setup";
// A single-sample 8x4 source, flat yellow, so a blit that ran at all is visible.
constexpr GLfloat kYellow[4]{1, 1, 0, 1};
GLuint srcFbo = 0, srcTexture = 0;
MakeSingleSampleTarget(8, 4, kYellow, srcFbo, srcTexture);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsBlitDecline.source";
glBindFramebuffer(GL_READ_FRAMEBUFFER, srcFbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, msFbo);
const auto before = PeekSplitRuntime().emitSeq;
glBlitFramebuffer(0, 0, 8, 4, 0, 0, 4, 2, GL_COLOR_BUFFER_BIT, GL_NEAREST);
EXPECT_GT(PeekSplitRuntime().emitSeq, before) << "F1.MsBlitDecline.wire";
// THE glFinish IS LOAD-BEARING, and it is what the first run of this case measured: the
// decline records its error where the blit is PERFORMED - the server's apply thread - and
// that error reaches this client's queue with a later reply, not with the blit call. Without
// the round trip the check below reads GL_NO_ERROR and the INVALID_OPERATION surfaces at
// whatever the next synchronising call happens to be, which is how it first showed up:
// attached to the resolve twenty lines down, a call that is not wrong about anything.
glFinish();
EXPECT_EQ(FirstGLError(), GLenum(GL_INVALID_OPERATION))
<< "F1.MsBlitDecline: the decline must record the error GL names for the shape";
// Resolve the multisample destination 1:1 into a single-sample attachment and read THAT:
// the round trip is also the proof that the session is still answering.
constexpr GLfloat kBlue[4]{0, 0, 1, 1};
GLuint readFbo = 0, readTexture = 0;
MakeSingleSampleTarget(4, 2, kBlue, readFbo, readTexture);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, readFbo);
glBlitFramebuffer(0, 0, 4, 2, 0, 0, 4, 2, GL_COLOR_BUFFER_BIT, GL_NEAREST);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsBlitDecline.resolve";
glBindFramebuffer(GL_READ_FRAMEBUFFER, readFbo);
constexpr std::array<GLubyte, 4> kMagenta{255, 0, 255, 255};
for (int y = 0; y < 2; ++y)
for (int x = 0; x < 4; ++x)
EXPECT_EQ(ReadOnePixel(x, y), kMagenta)
<< "F1.MsBlitDecline: the declined blit wrote (" << x << "," << y << ")";
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.MsBlitDecline: the session survived";
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glDeleteFramebuffers(1, &readFbo);
glDeleteTextures(1, &readTexture);
glDeleteFramebuffers(1, &srcFbo);
glDeleteTextures(1, &srcTexture);
glDeleteFramebuffers(1, &msFbo);
glDeleteRenderbuffers(1, &msRenderbuffer);
glBindTexture(GL_TEXTURE_2D, texture);
Gl().EndFrame();
}
namespace {
// One 8x8x2 RGBA8 array texture with two levels, filled so that EVERY subresource this pair
// of cases touches is distinguishable from every other one. A flat fill would let a copy that