mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user