Files
MobileGL/MobileGL/MG_Remote/Client/SlotCaps.h
T

152 lines
10 KiB
C

// MobileGL - MobileGL/MG_Remote/Client/SlotCaps.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
// THE 41 NULL CHECKS. Owner: package c1 (CONTRACT-P5.md §7, ID-14).
//
// FORTY-ONE OF THE SIXTY-NINE GLFunctionsTable SLOTS ARE NULL-CHECKED AT THEIR CALL SITE, AND
// SEVERAL OF THOSE CHECKS ARE CAPABILITY PROBES RATHER THAN SAFETY CHECKS. R-4 forbids a null
// slot in the client's emit table - so in that table every one of those probes would answer
// "supported", and whatever fallback sits behind it would silently disappear. That is not a
// theoretical risk: it is how a split lane produces a plausible picture for the wrong reason.
// ARCHITECTURE.md:114 already said what replaces the probe - "CallMask replaces 'is this table
// slot null' as the implicit capability probe" - and this header is the concrete list.
//
// TWO SPELLINGS, AND WHICH ONE A SITE TAKES IS DECIDED BY THE SLOT'S CLASS, NOT BY TASTE:
//
// MGL_BACKEND_SLOT_CAP(Slot, CapBit)
// The capability has a published bit. Under split the answer is the SERVER's bit;
// under monolith it is exactly today's null check, character for character.
// The three cases CONTRACT-P5.md §7 names by hand are all of this shape:
// GL_Query.cpp:481 / :558 / :785 BeginOcclusionQuery -> kCapOcclusionQuery
// GL_Query.cpp:534 BeginXfbPrimitivesQuery -> kCapXfbPrimitivesQuery
// PipeFill.cpp SubDataResident -> kCapResidentSubData
// (the third is not a GLFunctionsTable slot but an op-table one, so it reads the bit
// directly in PipeFill.cpp rather than through this header).
//
// MGL_BACKEND_SLOT_LOCAL(Slot)
// The capability has NO published bit and the slot is CONTRACT-P5.md §7 class C -
// Fatal{UnmigratedVerb} in the client's table. Under split the honest answer is
// "absent", which is precisely what the monolith nullptr meant, so every fallback the
// site already has survives instead of being replaced by an abort. Under monolith it is
// again today's null check.
//
// WHY "ABSENT" AND NOT "LET IT FATAL". A Fatal is loud, and for the 28 UNGUARDED slots it is
// strictly better than today (a null there is already an immediate crash with no diagnostic).
// But a guarded site is guarded because the frontend has a real answer for the absent case -
// always-signaled syncs, a CPU readback, CPU primitive accounting - and turning that answer
// into an abort is a behaviour change nobody asked for, in the direction that stops a lane
// dead. The class-C slot IS absent from this client; saying so is the accurate answer, not a
// weakening of R-4.
//
// THE TEST THAT DECIDES WHETHER A GUARDED SITE IS CONVERTED AT ALL, and it is the half of the
// walk the contract leaves to whoever does it: A PROBE MOVES ONLY WHERE "ABSENT" IS A CORRECT
// AND SUFFICIENT ANSWER. Where the fallback behind the probe produces a RIGHT result, "absent"
// is the accurate description of a client that does not have the slot, and converting keeps
// the lane running. Where the fallback produces a SILENTLY WRONG result, converting would
// manufacture exactly the defect R-4 exists to prevent, and the probe is left alone so the
// class-C slot Fatals by name instead.
//
// Converted, because the fallback is right:
// FenceSync used this fallback through P5. P5b §9 moved it to class B: the frontend
// now calls the emitter and the server preserves the optional/native-null fallback.
// GL_Texture.cpp:6537 GetTextureImage -> the frontend's own CPU readback, which is exact
// GL_Texture.cpp:6799 GetTexImage -> the same
// GL_Getter.cpp x2 GetGpuTimestampNs -> 0, which BackendObject.h:192 already names as
// the unsupported answer
// GL_Query.cpp the four query probes -> CPU primitive accounting / target rejection /
// COUNTER_BITS 0, all of them spec answers
//
// NOT converted, deliberately, because "absent" would be wrong rather than quiet:
// GL_Drawing.cpp:1274/:1371/:1420/:1435/:1641/:1673 the transform-feedback span family. The
// frontend reads a null EndTransformFeedback as "this backend does NOT own the capture,
// so reorder the captured records for it" (FixupGsStripCaptureOrder, :1290). Under split
// the server's backend DOES own it, so answering "absent" would reorder a capture that
// was already in GL order - a silently corrupt buffer. Transform feedback is off P5's
// reduced path (BRIEF §4's exclusion list) and its slots are class C, so the first XFB
// call aborts by name, which is the outcome R-4 asks for.
// GL_Drawing.cpp:844 PatchParameteri. "Absent" means the patch size is never set and every
// tessellation draw silently uses the previous one. Class C; it aborts by name.
//
// P5b t2 CLOSED THE FIRST OF THOSE TWO, AND THE OTHER SIX SITES NEEDED NOTHING (CONTRACT-P5B.md
// §2 t2, §6.5). The six SPAN sites - :1274 Begin, :1371 End, :1420 Pause, :1435 Resume, :1641
// Delete, :1673 Bind - are guards over a slot that is now class B in the client's table, so they
// simply call the emitter; their `if (const auto f = ...)` shape is left exactly as it was,
// because under split the slot is non-null and under monolith nothing moved. THE PROBE AT :1290
// IS THE ONE THAT HAD TO CHANGE, and it is the reason this header said the XFB span family would
// be the first to need a bit: it is not a guard on a call, it is a QUESTION ABOUT THE BACKEND
// asked of a table that under split belongs to the client. It now reads
// MGL_BACKEND_SLOT_CAP(EndTransformFeedback, kCapBackendOwnsXfbCapture), the bit the server sets
// from ITS table in MG_Backend/Init.cpp's InitSplitRoles. Espryt registers the slot and answers
// yes; Magma registers no XFB slot, answers no, and the client keeps reordering for it exactly
// as it does under monolith. GL_Drawing.cpp:844's PatchParameteri stays a plain guard for the
// same reason as the six: the slot is class B now, so "absent" never arises.
//
// The one XFB slot still class C is DeleteTransformFeedback, which CONTRACT-P5B.md gives no row
// (unmeasured); :1641's guard therefore still reaches Fatal{UnmigratedVerb} by name, which is
// the outcome R-4 asks for.
//
// WHAT THIS HEADER DELIBERATELY DOES NOT DO. It does not touch the 28 unguarded slots: those
// have no probe to convert, and calling one reaches Fatal{UnmigratedVerb, "<slot>"} by name,
// which is R-4's intent. And it does not invent a cap bit - a new MGPCapBit is an
// MGPipeTypes.h edit and that file is c0's, so a family that needs one goes through the
// integrator (the XFB span family is the first that will). It did: P5b's contract granted t2
// exactly that one bit, kCapBackendOwnsXfbCapture (CONTRACT-P5B.md §6.5, §8), and t2 added it.
//
// G1: in a build without MOBILEGL_BUILD_DISAGGREGATED both macros expand to the null check the
// site already had, so the pull build's code generation is unchanged.
#pragma once
#include <Includes.h>
#include <Config.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/MGPipeTypes.h>
#include <MG_Remote/Client/CapsMirror.h>
#define MGL_BACKEND_SLOT_CAP(Slot, CapBit) \
(::MobileGL::MG_Config::Transport != ::MobileGL::MG_Config::TransportMode::Monolith \
? ::MobileGL::MG_Remote::Client::CapsMirrorInstance().HasCap(CapBit) \
: ::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot != nullptr)
#define MGL_BACKEND_SLOT_LOCAL(Slot) \
(::MobileGL::MG_Config::Transport == ::MobileGL::MG_Config::TransportMode::Monolith && \
::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot != nullptr)
// The POINTER-valued forms, for the several sites shaped `if (const auto f = TABLE.GL.Slot)`.
// They exist for G1 and for nothing else: a site rewritten from that shape into
// `if (MGL_BACKEND_SLOT_CAP(...)) { const auto f = TABLE.GL.Slot; ... }` is semantically the
// same and generated DIFFERENT CODE - the first measurement of this change moved
// GL_Getter.cpp's GetInteger64v by -150 bytes and GetIntegerv by +2, which is two "resized"
// symbols and a red G1. In a pull build these two expand to the slot expression ITSELF, so the
// init-statement survives verbatim and the pull build's code generation cannot move.
#define MGL_BACKEND_SLOT_PTR_CAP(Slot, CapBit) \
(::MobileGL::MG_Config::Transport != ::MobileGL::MG_Config::TransportMode::Monolith \
? (::MobileGL::MG_Remote::Client::CapsMirrorInstance().HasCap(CapBit) \
? ::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot \
: nullptr) \
: ::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot)
#define MGL_BACKEND_SLOT_PTR_LOCAL(Slot) \
(::MobileGL::MG_Config::Transport != ::MobileGL::MG_Config::TransportMode::Monolith \
? nullptr \
: ::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot)
#else
#define MGL_BACKEND_SLOT_CAP(Slot, CapBit) \
(::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot != nullptr)
#define MGL_BACKEND_SLOT_LOCAL(Slot) \
(::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot != nullptr)
#define MGL_BACKEND_SLOT_PTR_CAP(Slot, CapBit) \
(::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot)
#define MGL_BACKEND_SLOT_PTR_LOCAL(Slot) \
(::MobileGL::MG_Backend::gBackendFunctionsTable.GL.Slot)
#endif