[MG_Backend, MG_Test] (P7): the wire arm's shader colour blit samples an unscaled blit through its nearest sampler, so a 1:1 GL_LINEAR glBlitFramebuffer into the default framebuffer is an exact copy

This commit is contained in:
2026-09-23 18:59:20 -04:00
parent cfe830cf44
commit 74636c8432
5 changed files with 199 additions and 10 deletions
@@ -52,6 +52,7 @@
#include <MG_Impl/Pipe/SlotAllocator.h>
// P7 wave 2 package B3: rule I's tally for WireDraw.inc's silent draw drops.
#include "WireDeclineTally.h"
#include "WireColorBlitFilter.h"
#include "WireDepthResolveArm.h"
#include "WireDepthResolveProbe.h"
#endif
@@ -340,7 +340,14 @@
alloc.descriptorSetCount = 1;
alloc.pSetLayouts = &cache.descriptorLayout;
VK_VERIFY(vkAllocateDescriptorSets(m_device, &alloc, &descriptorSet));
const VkDescriptorImageInfo sampled{filter == GL_LINEAR ? cache.linear : cache.nearest,
// GL_LINEAR filters only a STRETCHED blit; an unscaled one (mirrored or not, on any of the
// four transforms) is a copy and samples NEAREST, which is exact where linear filtering
// at a quantised coordinate is not (WireColorBlitFilter.h). The format check above still
// asks for SAMPLED_IMAGE_FILTER_LINEAR whenever GL said GL_LINEAR, so which blits reach
// this pass is unchanged.
const VkFilter samplerFilter = WireColorBlitFilterFor({sx0, sy0, sx1, sy1}, {dx0, dy0, dx1, dy1},
filter, transform);
const VkDescriptorImageInfo sampled{samplerFilter == VK_FILTER_LINEAR ? cache.linear : cache.nearest,
sourceView, sourceLayout};
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
write.dstSet = descriptorSet;
@@ -0,0 +1,66 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireColorBlitFilter.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "../VkIncludes.h"
// P7 g3-blit (g3det, gate 3): WHICH SAMPLER THE WIRE ARM'S SHADER COLOUR BLIT READS ITS SOURCE
// THROUGH.
//
// GL applies a blit's `filter` only where the image is STRETCHED (GL 4.6 core 18.3.1). A blit whose
// source and destination rectangles are the same size is a copy, whatever `filter` says, and so is
// a mirrored one. The wire arm's shader blit (WireColorBlit.inc, BlitWireColorImage) draws a quad
// and samples `textureLod(sourceImage, texCoord, 0)` at interpolated coordinates, and it used the
// LINEAR sampler for every GL_LINEAR blit. At 1:1 every fragment centre maps to a texel centre, so
// in exact arithmetic linear filtering reads the same single texel. Adreno 830 quantises the
// coordinate to sub-texel precision: a fragment that lands a fraction off the centre blends in a
// small weight of a neighbour. minecraft-1.21.1-neoforge-create-instancing-in-world ends with a
// 1:1 GL_LINEAR glBlitFramebuffer into FB 0, and on the wire arm 712 high-contrast pixels moved by
// 1 LSB toward a neighbour. The monolith's vkCmdBlitImage copied them exactly
// (~/w7/logs/g3det/VERDICT.md section 3). NEAREST within half a texel of the centre is exact, so an
// unscaled blit takes NEAREST. Lavapipe samples texel centres exactly, so no host picture can see
// the difference; this pure half is what the unit test pins
// (MG_Test/Pipeline/PipelineQuirkTest.cpp, WireColorBlitSamples*).
//
// THE SURFACE TRANSFORM DOES NOT TURN A COPY INTO A STRETCH. BlitWireColorImage applies the
// quarter and half turns in NDC, over a viewport of the native image, and for a quarter turn it
// takes the logical extent as the native extent swapped. A native pixel then has exactly the pitch
// of a logical one, and the rotation maps pixel centres onto pixel centres. So the scale test is on
// GL's own (logical) rectangles for identity and all three rotations. BlitWireColorImage declines
// any other transform before it chooses a sampler; for one, this answers with the filter's own
// mapping, as before this package.
#if MOBILEGL_BUILD_DISAGGREGATED
namespace MobileGL::MG_Backend::DirectVulkan {
// A glBlitFramebuffer rectangle as GL names it: two corners, either order.
struct WireBlitRect {
GLint x0 = 0, y0 = 0, x1 = 0, y1 = 0;
};
// Same extent on both axes, orientation aside. The extents are taken in 64 bits: GL accepts any
// GLint corner, and INT_MAX - INT_MIN does not fit in one.
inline Bool WireBlitIsUnscaled(const WireBlitRect& source, const WireBlitRect& destination) {
const auto extent = [](GLint a, GLint b) {
return std::abs(static_cast<Int64>(b) - static_cast<Int64>(a));
};
return extent(source.x0, source.x1) == extent(destination.x0, destination.x1) &&
extent(source.y0, source.y1) == extent(destination.y0, destination.y1);
}
inline VkFilter WireColorBlitFilterFor(const WireBlitRect& source, const WireBlitRect& destination,
GLenum filter, VkSurfaceTransformFlagBitsKHR transform) {
if (filter != GL_LINEAR) return VK_FILTER_NEAREST;
const Bool expressed = transform == VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR ||
transform == VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR ||
transform == VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR ||
transform == VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR;
if (expressed && WireBlitIsUnscaled(source, destination)) return VK_FILTER_NEAREST;
return VK_FILTER_LINEAR;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
#endif
@@ -1105,12 +1105,23 @@
// P7 wave 2-B, CONTRACT-P7 §3.2: `default-color-blit-shape@P7` RETIRES, in three
// pieces, because the one Fatal covered three unrelated situations.
//
// The shader blit is here for ROTATION and for nothing else. It can only SAMPLE
// its source - sampler2D, a float result, one of two 2D view types - so an
// RGBA8UI attachment, a 3D slice or an array layer made it return false, and the
// refusal fired even on a surface with NO rotation, where the ordinary
// vkCmdBlitImage arm twenty lines down would have done the blit correctly and
// does handle those shapes.
// The shader blit EXISTS for rotation, because an axis-aligned vkCmdBlitImage cannot
// express a quarter turn. It is still TAKEN FIRST on every transform it can
// express, identity included. That has been the routing since 38919d453: every
// host lane (all identity) and the Redmi pbuffer run their default-framebuffer
// colour blits through it, F1WireScenario.ColorBlitToDefaultHonorsScissorAndReversedRect
// among them. The ordinary vkCmdBlitImage arm below is reached on identity only
// when this pass declines the source. Because identity blits run here, the pass
// must copy exactly wherever GL says the blit is a copy: an unscaled blit samples
// NEAREST whatever `filter` says (WireColorBlitFilter.h, g3-blit; on Adreno 830,
// linear sampling of a 1:1 GL_LINEAR blit moved create-instancing's edge pixels by
// 1 LSB, where the monolith's vkCmdBlitImage is exact).
//
// The pass can only SAMPLE its source - sampler2D, a float result, one of two 2D
// view types - so an RGBA8UI attachment, a 3D slice or an array layer makes it
// return false. That refusal used to fire even on a surface with NO rotation,
// where the vkCmdBlitImage arm below does the blit correctly and does handle
// those shapes.
const auto transform = m_swapchainObject.GetPreTransform();
const Bool knownRotation = transform == VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR ||
transform == VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR ||
@@ -1154,9 +1165,11 @@
static_cast<Uint32>(transform));
return;
}
// Identity: fall through to the ordinary vkCmdBlitImage arm below. It already
// maps GL's bottom row onto the native image's last row for a default target,
// and vkCmdBlitImage takes integer formats and array layers natively. Its 3D
// Identity, and the shader pass above declined this source (an integer format, a
// 3D slice, a view type it cannot sample): fall through to the ordinary
// vkCmdBlitImage arm below. It already maps GL's bottom row onto the native
// image's last row for a default target, and vkCmdBlitImage takes integer
// formats and array layers natively. Its 3D
// addressing was wrong and is fixed with this package - see the srcBaseLayer /
// srcZ note down there; the bug was invisible until now because a 3D source hit
// the Fatal above and never reached that arm.
@@ -13,6 +13,7 @@
#include <MG_Backend/DirectVulkan/Renderer/ProgramFactory.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Backend/DirectVulkan/Renderer/WireRenderPassCompatibility.h>
#include <MG_Backend/DirectVulkan/Renderer/WireColorBlitFilter.h>
#include <MG_Backend/DirectVulkan/Renderer/WireDepthResolveArm.h>
#include <MG_Backend/DirectVulkan/Renderer/WirePlaceholderKey.h>
#endif
@@ -895,3 +896,104 @@ TEST(PipelineQuirkTest, WirePlaceholderKeysArePrivatePerShapeAndUnitAndBounded)
GTEST_SKIP() << "the wire placeholders exist only in the disaggregated build";
#endif
}
// P7 g3-blit (g3det, ~/w7/logs/g3det/VERDICT.md section 3): WHICH SAMPLER THE WIRE ARM'S SHADER
// COLOUR BLIT USES (WireColorBlitFilter.h). GL filters only a STRETCHED blit, so a 1:1 blit is a copy
// whatever `filter` says. The wire pass sampled every GL_LINEAR blit through its linear sampler; on
// Adreno 830 the sub-texel coordinate quantisation moved create-instancing's 1:1 GL_LINEAR blit into
// FB 0 by 1 LSB on 712 edge pixels, while the monolith's vkCmdBlitImage copied them exactly.
// Lavapipe samples texel centres exactly and shows the same picture on both samplers, so no host
// lane can see this; the choice is pinned here instead.
//
// Red once (executed, reverted): WireColorBlitFilterFor reduced to the old mapping
// (`filter == GL_LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST`) fails all 29 sampler expectations
// of the first test and none of the second.
#if MOBILEGL_BUILD_DISAGGREGATED
namespace {
using MobileGL::MG_Backend::DirectVulkan::WireBlitIsUnscaled;
using MobileGL::MG_Backend::DirectVulkan::WireBlitRect;
using MobileGL::MG_Backend::DirectVulkan::WireColorBlitFilterFor;
// Identity and the three turns BlitWireColorImage expresses.
constexpr VkSurfaceTransformFlagBitsKHR kExpressedTransforms[] = {
VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR, VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR,
VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR, VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR};
} // namespace
#endif
TEST(PipelineQuirkTest, WireColorBlitSamplesNearestWhereTheBlitIsACopy) {
#if MOBILEGL_BUILD_DISAGGREGATED
// create-instancing's last pass (trace call 530331): glBlitFramebuffer(0,0,854,480 -> 0,0,854,480,
// GL_COLOR_BUFFER_BIT, GL_LINEAR) into FB 0, on an identity pbuffer.
EXPECT_EQ(WireColorBlitFilterFor({0, 0, 854, 480}, {0, 0, 854, 480}, GL_LINEAR,
VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR),
VK_FILTER_NEAREST)
<< "a 1:1 GL_LINEAR blit is a copy (GL 4.6 core 18.3.1); linear sampling of it is not exact on Adreno";
const struct {
const char* what;
WireBlitRect source, destination;
} copies[] = {
{"offset on both sides", {10, 20, 110, 70}, {300, 5, 400, 55}},
{"source reversed on x", {854, 0, 0, 480}, {0, 0, 854, 480}},
{"destination reversed on both axes", {0, 0, 6, 4}, {48, 36, 42, 32}},
{"both reversed", {6, 4, 0, 0}, {42, 32, 48, 36}},
{"one texel", {3, 3, 4, 4}, {0, 0, 1, 1}},
{"negative corners", {-8, -4, 8, 4}, {100, 100, 116, 108}},
// 64-bit extents: INT_MAX - INT_MIN overflows a GLint.
{"full GLint range", {INT_MIN, 0, INT_MAX, 1}, {INT_MAX, 1, INT_MIN, 0}},
};
for (const auto& copy : copies) {
EXPECT_TRUE(WireBlitIsUnscaled(copy.source, copy.destination)) << copy.what;
// A quarter turn maps logical pixel centres onto native ones (the native extent is the
// logical one swapped), so a copy is still a copy on every rotated surface.
for (const auto transform : kExpressedTransforms) {
EXPECT_EQ(WireColorBlitFilterFor(copy.source, copy.destination, GL_LINEAR, transform), VK_FILTER_NEAREST)
<< copy.what << ", surface transform 0x" << std::hex << static_cast<Uint32>(transform);
}
}
#else
GTEST_SKIP() << "the wire shader blit exists only in the disaggregated build";
#endif
}
TEST(PipelineQuirkTest, WireColorBlitKeepsLinearWhereTheBlitStretches) {
#if MOBILEGL_BUILD_DISAGGREGATED
const struct {
const char* what;
WireBlitRect source, destination;
} stretches[] = {
{"2x on both axes", {0, 0, 854, 480}, {0, 0, 1708, 960}},
{"half on y only", {0, 0, 854, 480}, {0, 0, 854, 240}},
{"one texel wider on x only", {0, 0, 854, 480}, {0, 0, 855, 480}},
// GL has no transpose: swapped extents are a stretch on both axes, not a rotation.
{"extents transposed", {0, 0, 480, 854}, {0, 0, 854, 480}},
{"reversed and stretched", {0, 0, 6, 4}, {48, 36, 36, 28}},
// What GenerateMipmap's attachable arm asks for (source level -> the next level).
{"a mip step", {0, 0, 64, 32}, {0, 0, 32, 16}},
{"a 1xN mip step", {0, 0, 1, 8}, {0, 0, 1, 4}},
{"full GLint range against one texel", {INT_MIN, 0, INT_MAX, 1}, {0, 0, -1, 1}},
};
for (const auto& stretch : stretches) {
EXPECT_FALSE(WireBlitIsUnscaled(stretch.source, stretch.destination)) << stretch.what;
for (const auto transform : kExpressedTransforms) {
EXPECT_EQ(WireColorBlitFilterFor(stretch.source, stretch.destination, GL_LINEAR, transform),
VK_FILTER_LINEAR)
<< stretch.what << ": GL_LINEAR filters a stretched blit, surface transform 0x" << std::hex
<< static_cast<Uint32>(transform);
EXPECT_EQ(WireColorBlitFilterFor(stretch.source, stretch.destination, GL_NEAREST, transform),
VK_FILTER_NEAREST)
<< stretch.what << ": GL_NEAREST never becomes linear";
}
}
for (const auto transform : kExpressedTransforms) {
EXPECT_EQ(WireColorBlitFilterFor({0, 0, 854, 480}, {0, 0, 854, 480}, GL_NEAREST, transform), VK_FILTER_NEAREST);
}
// A transform the pass cannot express is declined before a sampler is chosen; the helper keeps
// the filter's own mapping for one rather than claiming the copy is exact.
EXPECT_EQ(WireColorBlitFilterFor({0, 0, 854, 480}, {0, 0, 854, 480}, GL_LINEAR,
VK_SURFACE_TRANSFORM_HORIZONTAL_MIRROR_BIT_KHR),
VK_FILTER_LINEAR);
#else
GTEST_SKIP() << "the wire shader blit exists only in the disaggregated build";
#endif
}