Magma: an enabled scissor test over a never-written box clips nothing instead of everything (DefaultScissorFollowsSurfaceScenario runs on DirectVulkan instead of skipping).

This commit is contained in:
2026-10-09 12:54:23 -04:00
parent 2bbb67e528
commit fcf5ae5659
4 changed files with 22 additions and 9 deletions
@@ -670,6 +670,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
vkCmdSetLineWidth(commandBuffer, lineWidth);
}
// Whether the GL scissor test clips viewport index `index`. The scissor rectangles start
// all-zero, and all-zero there means "never written" (RenderState's constructor), not an
// empty box: GL's initial box is the whole window, so until the application's first
// glScissor an enabled test clips nothing. Read as a box it would clip EVERYTHING, so every
// reader of the box asks this first. The resolution matches the viewport's (ComputeGLViewport
// resolves a never-written viewport to the current framebuffer's extent).
static Bool GLScissorTestClips(Uint32 index = 0) {
const RenderStateParameters& parameters = MG_Pipe::gPipeInputs.GetRenderStateParameters();
const Uint32 bit = 1u << index;
return (parameters.ScissorTestEnabledMask & bit) != 0 && (parameters.ScissorBoxWrittenMask & bit) != 0;
}
static VkRect2D MakeClampedScissorRect(const IntVec4& scissorBox, const IntVec2& framebufferExtent) {
const Int x0 = std::max<Int>(0, scissorBox.x());
const Int y0 = std::max<Int>(0, scissorBox.y());
@@ -3393,7 +3405,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkSurfaceTransformFlagBitsKHR preTransform,
Bool isDefaultFbo) const {
const auto& parameters = MG_Pipe::gPipeInputs.GetRenderStateParameters();
if ((parameters.ScissorTestEnabledMask & (1u << index)) == 0) {
if (!GLScissorTestClips(index)) {
VkRect2D full{};
full.offset = {0, 0};
full.extent = {static_cast<Uint32>(extent.x()), static_cast<Uint32>(extent.y())};
@@ -3515,7 +3527,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
key.stencilWriteMask[face] = p.StencilStates[face].WriteMask;
key.stencilRef[face] = p.StencilStates[face].Ref;
}
key.scissorEnabled = (p.ScissorTestEnabledMask & 1u) != 0;
key.scissorEnabled = GLScissorTestClips(0);
key.scissorBox[0] = p.ScissorBoxes[0].x();
key.scissorBox[1] = p.ScissorBoxes[0].y();
key.scissorBox[2] = p.ScissorBoxes[0].z();
@@ -1046,7 +1046,7 @@
if (count == 0) return remaining & ~handled;
VkRect2D area{{0, 0}, {key.width, key.height}};
if (MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
if (GLScissorTestClips()) {
const auto& box = MG_Pipe::gPipeInputs.GetScissorBox();
const Int32 x0 = std::max(box.x(), 0), y0 = std::max(box.y(), 0);
const Int32 x1 = std::min(box.x() + box.z(), static_cast<Int32>(key.width));
@@ -369,7 +369,7 @@
const VkCommandBuffer passBuffer = active->commandBuffer;
const Uint64 passHash = active->hash;
VkRect2D area{{0, 0}, {key.width, key.height}};
if (MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
if (GLScissorTestClips()) {
const auto& box = MG_Pipe::gPipeInputs.GetScissorBox();
const Int32 x0 = std::max(box.x(), 0), y0 = std::max(box.y(), 0);
const Int32 x1 = std::min(box.x() + box.z(), static_cast<Int32>(key.width));
@@ -469,7 +469,7 @@
const ClearAttachmentPayload& payload, GLint drawbuffer) {
GLbitfield remaining = payload.mask & (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
if (!remaining || fbo.IsDefault || drawbuffer >= 0 || m_wirePreparationDepth != 0) return remaining;
const Bool scissor = MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::ScissorTest);
const Bool scissor = GLScissorTestClips();
const auto wholeImage = [&](const WireImage& image) {
if (!scissor) return true;
const auto& box = MG_Pipe::gPipeInputs.GetScissorBox();
@@ -614,7 +614,7 @@
if (!image.image) return;
m_textureManager->FlushPendingUploads();
VkRect2D area{{0, 0}, image.extent};
if (MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
if (GLScissorTestClips()) {
const auto& box = MG_Pipe::gPipeInputs.GetScissorBox();
const Int32 x0 = std::max(box.x(), 0), y0 = std::max(box.y(), 0);
const Int32 x1 = std::min(box.x() + box.z(), static_cast<Int32>(image.extent.width));
@@ -1263,7 +1263,7 @@
RetireWireDrawPass();
// The scissor test clips blit writes: intersect the destination rectangle with
// the scissor box and shrink the source proportionally.
if (MG_Pipe::gPipeInputs.IsCapabilityEnabled(CapabilityInput::ScissorTest)) {
if (GLScissorTestClips()) {
const IntVec4& scissor = MG_Pipe::gPipeInputs.GetScissorBox();
const auto clipAxis = [](GLint& d0, GLint& d1, GLint& s0, GLint& s1, GLint clipLo, GLint clipHi) -> Bool {
const Bool dstFlipped = d1 < d0;
@@ -13,7 +13,9 @@
// keeps the surface size in a memo that a swap, a change of draw surface, or a surface's creation
// or destruction invalidates. This case makes a second, larger surface current on the same
// context and clears it with the scissor test on: the clear must reach the larger surface's far
// corner, which a size remembered from the first surface would cut off.
// corner, which a size remembered from the first surface would cut off. DirectVulkan resolves a
// never-written box to the whole framebuffer it draws into, so the same clear reaches the corner
// there too (it used to read the all-zero box as an empty one and clear nothing).
#include <array>
@@ -36,7 +38,6 @@ class DefaultScissorFollowsSurfaceScenario : public ScenarioTest {};
TEST_F(DefaultScissorFollowsSurfaceScenario, AClearOnALargerSurfaceMadeCurrentReachesItsFarCorner) {
if (!Ready() || IsSkipped()) return;
if (Gl().BackendName() != "DirectGLES") GTEST_SKIP() << "the substituted scissor box is DirectGLES's";
const EGLDisplay display = eglGetCurrentDisplay();
const EGLSurface first = eglGetCurrentSurface(EGL_DRAW);
const EGLContext context = eglGetCurrentContext();