Files
MobileGL/MobileGL/MG_Backend/DirectGLES/DirectGLESState.cpp
T
swung0x48 d1293354d1 [Fix] (CMakeLists.txt, MG_Backend/DirectGLES, MG_Pipe, MG_Test/Backend, MG_Test/ShaderTranspiler, MG_Util/ShaderTranspiler): give integer attributes their GL value in Espryt
- every integer array reaches the driver with its input's signedness, at the pointer, zero-stride and client-memory call sites, so each fetch is one ES defines
- 8/16-bit data of the other signedness is widened back by the program's runtime-width seeds, armed on the first draw that needs them and set as two uniforms per draw
- capture programs with integer inputs are armed at their first build; arming is part of the ESSL translation key
- a build whose seeds cannot be verified keeps the driver's own value for narrow data; one that fails to link is rebuilt without them
- the program's integer input masks are memoized on its shader record
2026-10-19 02:53:01 -04:00

1494 lines
98 KiB
C++

// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESState.cpp
// 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
#include "DirectGLESInternal.h"
namespace MobileGL::MG_Backend::DirectGLES {
// Cached addresses of the frontend's framebuffer binding slots. The frontend
// getter linear-scans its slot array per call and the draw path asks for these
// slots several times per draw (FBO sync, FBO texture-attachment sync, the
// draw-buffer broadcast memo, the draw-time bind). The slots are by-value
// members of the GLContext, so their addresses are a pure function of the
// context's address: the cache is keyed on the raw context pointer, and a
// recreated context landing on the SAME address has its slots at the same
// addresses again - the cached pointers cannot go stale. Invalidation is
// exactly the pointer compare below.
using FbBindingSlot =
std::remove_reference_t<decltype(MG_Pipe::gPipeInputs->GetFramebufferBindingSlot(FramebufferTarget::Draw))>;
// P2 step e4. On the {slot, gen} arm this is an ORDINARY read of pushed state and the cache
// above is not consulted, which closes the P1 accessor bypass: the cached raw pointer ran
// the checked accessor once per context change and then handed out the pointee forever, so
// at all five call sites the per-verb poison stamp (MGP_INPUT_CHECK) and the verify
// read-hook (MGP_INPUT_VERIFY_READ) were skipped. A verb that legitimately never fills
// GetFramebufferBindingSlot could not be caught here, and a verify build compared the field
// only where the slow accessor was used.
//
// The cache stays on the LEGACY arm, gated on the same subsystem bit as the rest of this
// slice, so that MOBILEGL_PIPE_PUSH=0 keeps being the faithful all-subsystems-pull control
// ConfigLoader.cpp documents - "reproduces P1's behaviour exactly" has to include this
// path, or the integrator's A/B measures e4 on both arms and attributes it to neither. In
// the pull build gPipeInputs is the live GLContext, there is no poison to bypass and the
// frontend getter still linear-scans, so the cache is exactly the code it was.
static inline FbBindingSlot& GetFramebufferBindingSlotChecked(FramebufferTarget target) {
if (EsprytSlotTablesEnabled()) {
return MG_Pipe::gPipeInputs->GetFramebufferBindingSlot(target);
}
// No legacy arm compiled: EsprytSlotTablesEnabled() is unconditionally true above.
return MG_Pipe::gPipeInputs->GetFramebufferBindingSlot(target);
}
static Bool IsDualSourceBlendFactor(BlendFactor v) {
switch (v) {
case BlendFactor::Src1Color:
case BlendFactor::OneMinusSrc1Color:
case BlendFactor::Src1Alpha:
case BlendFactor::OneMinusSrc1Alpha:
return true;
default:
return false;
}
}
namespace FramebufferImpl {
Array<SyncedFramebufferSerialMemo, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fboSyncedSerials{};
// Whether the last SyncCurrentFBO found the two records describing the two bindings.
// BindCurrentFBO's handle arm reads it instead of re-asking, because the whole value
// of that arm is that it does NOT read the binding slot - and because every entry
// point that binds runs the sync first, immediately before (PrepareForDraw, Clear, the
// blits, the DSA clears, the readbacks). It starts false and every decline clears it,
// so a bind that somehow arrives without a preceding sync takes the pre-handle arm
// rather than trusting a record nothing checked.
Bool g_fboRecordsTrusted = false;
static void StampSyncedFramebufferSerial(FramebufferTarget target, Uint64 serial) {
auto& memo = g_fboSyncedSerials[SizeT(target)];
memo.serial = serial;
memo.backendIdGeneration = g_attachmentBackendIdGeneration;
memo.contextGeneration = g_backendContextGeneration;
memo.valid = true;
}
// The seam check, and it is the reason this arm can be trusted at all on a tree where
// the client half is mid-landing. The record says which framebuffer each binding
// holds; the binding slot says the same thing. If they disagree, the record is stale
// or mis-keyed and driving the driver from it would sync - or skip - the wrong
// framebuffer, which is the one failure this path could produce that no pixel test on
// this tree could see. Disagreement DECLINES the whole arm rather than fixing up one
// target, because the two records are emitted together and one of them being wrong
// says nothing good about the other.
//
// The default framebuffer is compared through IsDefault rather than through Fbo,
// because the reserved handle {0,1} is not the handle the twin table minted for the
// default FramebufferObject; comparing it against HandleOf would fail for the one case
// the reserved handle exists to make easy. This is the identity comparison D-C1
// retires, kept here ONLY as a consistency check and only until package D's twin API
// takes the record instead of the object - at which point there is no second answer
// left to disagree with.
//
// P5e (fb, CONTRACT-P5E.md §5.4): MONOLITH-ONLY. The sentence above ends "...and only
// until package D's twin API takes the record instead of the object - at which point
// there is no second answer left to disagree with". That point is this package: the
// twin's sync takes the handle, the record was resolved from the applier's OWN bound
// handle, and the only thing this could still consult is the binding slot - a
// BARRIER_PULLED row an apply thread may not read. So under a transport the check does
// not run at all, rather than running on a value it is not allowed to have.
static Bool FramebufferRecordMatchesBinding(FramebufferTarget target,
const MG_Pipe::MGPFramebufferState& record) {
return true;
}
static Bool SyncedFramebufferSerialIsCurrent(FramebufferTarget target, Uint64 serial) {
const auto& memo = g_fboSyncedSerials[SizeT(target)];
return memo.valid && memo.serial == serial && memo.backendIdGeneration == g_attachmentBackendIdGeneration &&
memo.contextGeneration == g_backendContextGeneration;
}
// The handle arm of SyncCurrentFBO. Returns false when it declined - which is not a
// failure and is the only honest answer while no client has emitted yet.
//
// NO LIVE RECORD IS A MISS, NEVER A HIT, and it is the same rule the vertex-input arm
// states at SyncVaoAttributeBuffersByHandle. What is NOT the test, and this is a trap
// worth naming: FramebufferSerial is not "has the client ever described a framebuffer".
// MGPipeApplierReset ADVANCES every P4a working-state serial unconditionally
// (PipeApply.cpp), because a cleared window is itself a change a twin has to hear
// about - so the serial is non-zero after the first make-current even though no
// set_framebuffer_state has ever been applied, and gating on it would take this arm
// with two empty records and leave every framebuffer unsynced. The record's own Fbo
// handle is the test, and BOTH bindings have to be described before this arm can run
// at all, because it answers for both in one pass: an emitter walks {Draw, Read} and a
// Target = Both record writes both, so a half-described applier is the transitional
// state of a tree whose client half has not landed yet, and that state belongs to the
// pre-handle arm.
static Bool SyncCurrentFBOByRecord() {
g_fboRecordsTrusted = false;
const auto& st = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPFramebufferState* const drawRecord = BoundFramebufferRecord(FramebufferTarget::Draw);
const MG_Pipe::MGPFramebufferState* const readRecord = BoundFramebufferRecord(FramebufferTarget::Read);
const Bool drawUnrecorded = drawRecord == nullptr || MG_Pipe::MGPipeHandleIsNull(drawRecord->Fbo);
const Bool readUnrecorded = readRecord == nullptr || MG_Pipe::MGPipeHandleIsNull(readRecord->Fbo);
// P4a decline-site F2: S - BOTH CASES ARE LOUD, the half-described one since v2
// (ID-19) and the NEITHER-DESCRIBED one from this verification round.
//
// EXACTLY ONE recorded: an emitter walks {Draw, Read} together and a Target = Both
// record writes both, so no correct emitter can produce it. It is the shape a
// PARTIALLY LANDED emitter produces and it is invisible in a pixel test because
// the arm just declines.
//
// NEITHER recorded WAS the transitional state of a tree whose client half had not
// landed - on this branch, before B and C were integrated, every draw was that,
// which is why v2 left it silent. Package B now emits set_framebuffer_state for
// every bound framebuffer and this function only runs with bit 9 (and therefore
// 10, 11 and 7) set, so reaching a draw with NEITHER binding described means the
// record for the bound framebuffer never arrived, or arrived at a slot whose
// generation has already moved - the applier counts that second cause separately
// in StaleFramebufferRecordLookups, so the two are told apart without a third
// message. Both are seam defects and both now say so, in the words section 8's
// one grep looks for.
if (drawUnrecorded || readUnrecorded) {
if (drawUnrecorded != readUnrecorded) {
const MG_Pipe::MGPFramebufferState& described = *(drawUnrecorded ? readRecord : drawRecord);
MGLOG_E_ONCE("A framebuffer record does not describe the binding it names: the %s "
"binding has no record while %s names {slot %u, gen %u} - a "
"half-described applier; running the pre-handle framebuffer sync.",
drawUnrecorded ? "DRAW" : "READ", drawUnrecorded ? "READ" : "DRAW",
static_cast<unsigned>(described.Fbo.Slot), static_cast<unsigned>(described.Fbo.Gen));
} else {
MGLOG_E_ONCE(
"A framebuffer record does not describe the binding it names: NEITHER "
"the DRAW nor the READ binding has one while the framebuffer "
"subsystem bit is set (bound handles {slot %u, gen %u} / "
"{slot %u, gen %u}, %llu stale-generation lookup(s) so far); "
"running the pre-handle framebuffer sync.",
static_cast<unsigned>(st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)].Slot),
static_cast<unsigned>(st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)].Gen),
static_cast<unsigned>(st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Read)].Slot),
static_cast<unsigned>(st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Read)].Gen),
static_cast<unsigned long long>(st.StaleFramebufferRecordLookups));
}
return false;
}
// P4a decline-site F3: S - the framebuffer seam, already loud; the round keeps this
// wording VERBATIM because it is the stem the other two seams now share.
if (!FramebufferRecordMatchesBinding(FramebufferTarget::Draw, *drawRecord) ||
!FramebufferRecordMatchesBinding(FramebufferTarget::Read, *readRecord)) {
MGLOG_E_ONCE("A framebuffer record does not describe the binding it names; "
"running the pre-handle framebuffer sync.");
return false;
}
g_fboRecordsTrusted = true;
const FramebufferTarget fboTargets[] = {FramebufferTarget::Draw, FramebufferTarget::Read};
for (auto& target : fboTargets) {
if (SyncedFramebufferSerialIsCurrent(target, st.FramebufferSerial)) continue;
// Not a third accessor call: these are the two pointers F2 proved non-null a
// few lines up, so nothing here dereferences an unchecked result.
const MG_Pipe::MGPFramebufferState& record =
target == FramebufferTarget::Draw ? *drawRecord : *readRecord;
if (record.IsDefault != 0) {
// The default framebuffer, said by the record rather than by comparing the
// bound object against pDefaultFramebufferInfo->defaultFBO - which is one
// of the four identity comparisons the reserved handle {0,1} exists to
// retire (MGPipeHandles.h). Nothing to sync except the widened-attachment
// masks, which only SyncToBackend ever writes and would otherwise still
// describe the user FBO that was draw-bound before. The window surface is a
// real RGBA buffer, so nothing here is ever widened.
if (target == FramebufferTarget::Draw) {
g_alphaWidenedDrawBufferMask = 0;
g_integerColorDrawBufferMask = 0;
}
StampSyncedFramebufferSerial(target, st.FramebufferSerial);
continue;
}
// MONOLITH GLUE, and named as such: on the monolith arm the twin's SyncToBackend
// still reads the frontend object, and the bound object is still this call's
// ARGUMENT. The IDENTITY is already the handle - GetOrCreateByHandle(record.Fbo) -
// so a recycled FBO cannot be mistaken for its predecessor here.
//
// A1, TAKEN AT THE VERIFICATION ROUND AGAINST WHAT D ACTUALLY BUILT. The twin
// is resolved BY HANDLE ONLY: `BackendPtr* GetOrCreateByHandle(MGPipeHandle)`
// (Managers.h ~453) returns a POINTER rather than the reference
// GetOrCreate(StatePtr) returns, deliberately, because it has three ways to
// decline - the legacy arm, a slot past the table's sanity bound, and a
// generation BEHIND the live entry's - and every one of them has to be visible
// here rather than answered with a parked twin.
//
// P5e (fb, CONTRACT-P5E.md §5.4): AND ON THE RECORD ARM THE OBJECT IS GONE TOO.
// `SyncToBackendByHandle(record.Fbo, target)` takes the record's own handle, so
// there is no binding slot to read, no object to hand over, and no state note
// to leave behind - NoteStateForHandle was the server's last cross-record hold
// on a frontend framebuffer, and the named blit that needed it now reads the
// record too. What is left frontend under a transport is nothing at all.
//
// THE POINTER-INVALIDATION CONTRACT (Managers.h ~381-385): a handle-arm result
// is a stable array element that only a table-GROWING GetOrCreate can move.
// GetOrCreateByHandle is exactly such a call, so its result is used and dropped
// inside this iteration and never held across another registry call.
auto* const twinSlot = g_backendFramebufferObjects.GetOrCreateByHandle(record.Fbo);
// A1's third decline. Bit 9 implies bit 10 implies bit 7, so the slot tables are
// armed whenever this function runs and the legacy-arm cause cannot fire here;
// what is left is a slot past the sanity bound or a generation behind the live
// entry's, and both are seam defects - a handle the emitter minted for a
// recycled slot and never described, or one it had already retired. The whole
// arm declines rather than this one target, for F4's reason.
if (twinSlot == nullptr) {
MGLOG_E_ONCE("A framebuffer record does not describe the binding it names: the "
"%s record's handle {slot %u, gen %u} is refused by the twin slot "
"table (live generation %u); running the pre-handle framebuffer "
"sync.",
target == FramebufferTarget::Read ? "READ" : "DRAW",
static_cast<unsigned>(record.Fbo.Slot), static_cast<unsigned>(record.Fbo.Gen),
static_cast<unsigned>(g_backendFramebufferObjects.LiveGenAt(record.Fbo.Slot)));
g_fboRecordsTrusted = false;
return false;
}
auto& backendObj = *twinSlot;
if (!backendObj) {
backendObj = MakeShared<BackendFramebufferObject>();
}
// THE "SAME FBO AS DRAW" SKIP IS A FIELD, not a pointer comparison against the
// object the previous iteration happened to sync. Target = Both is the client
// saying one object is bound to both bindings, so the attachment and
// draw-buffer work the DRAW pass already did is not repeated - and the read
// buffer, which is READ-target-specific and is what that skip used to drop, is
// applied unconditionally on this path.
//
// ID-27 (wire review v2, MAJOR-1): THE QUESTION IS ASKED OF THE BOUND HANDLES,
// NOT OF THE RECORD'S STORED TARGET, and it is no longer possible to ask it any
// other way in this file. `MGPFramebufferState::Target` is the target of the
// LAST EMISSION that wrote the record, and since ID-19(c) that emission may be
// a `Named` one from any of B's sixteen DSA sites - so a framebuffer really
// bound to both bindings can carry `Target == Named` and a framebuffer bound to
// neither can carry `Target == Both` left over from when it was. The skip's
// real question is "is one object bound to both bindings", and the applier
// answers it directly: BoundFramebuffer[Draw] == BoundFramebuffer[Read]. Both
// are non-null here (F2 rejected a null on either), and a handle compares by
// {slot, gen}, so a recycled slot is not its predecessor.
//
// MINOR-1's correction is RETIRED by P5e: the read buffer is no longer applied
// "FROM THE FRONTEND OBJECT" on this arm - ApplyReadBufferFromRecord resolves it
// out of MGPFramebufferState::ReadSurface, which was already the record's
// answer, and the by-handle entry is what reaches it here.
const auto& boundHandles = st.BoundFramebuffer;
const Bool sameObjectOnBothBindings = target == FramebufferTarget::Read &&
boundHandles[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)] ==
boundHandles[SizeT(MG_Pipe::MGPipeFramebufferTarget::Read)];
if (sameObjectOnBothBindings) {
backendObj->SyncReadBufferToBackendByHandle(record.Fbo);
} else {
backendObj->SyncToBackendByHandle(record.Fbo, target);
}
StampSyncedFramebufferSerial(target, st.FramebufferSerial);
continue;
}
return true;
}
void SyncCurrentFBO() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// The handle arm first, and it declines rather than half-running: with no record
// yet it hands the walk straight back to the pre-handle arm below, unchanged.
// P4a decline-site F1: M - the mask says this family is not switched on, and it
// IS D's FramebufferSubsystemEnabled() now. Silent, confirmed at the
// verification round.
if (SyncCurrentFBOByRecord()) return;
// P5e (fb, CONTRACT-P5E.md §5.4): AND UNDER A TRANSPORT THE DECLINE IS THE END OF
// IT. The fallback below reads GetFramebufferBindingSlot, a BARRIER_PULLED row; the
// value it would read belongs to a client this apply is no longer synchronous with,
// so "run the pre-handle sync" stops being a safe default and becomes a wrong
// picture taken from torn state. Every cause of the decline is already named by the
// MGLOG_E_ONCE that produced it (F2's half-described / neither-described applier,
// F4's unbound binding, A1's refused handle); this turns that log into the abort the
// contract asks for, with the verb in the message so the strict lane's marker table
// points at the site.
RefuseFramebufferBindingSlotRead();
}
} // namespace FramebufferImpl
namespace RenderStateImpl {
static Uint16 g_syncedRenderStateVersion = 0;
static Bool g_hasSyncedRenderState = false;
RenderStateParameters g_syncedRenderStateParameters;
static IntVec4 g_syncedBackendViewport = IntVec4(-1, -1, -1, -1);
// The RESOLVED scissor rectangle last pushed (see the scissor block in SyncRenderState);
// an impossible value so the first sync always pushes.
static IntVec4 g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1);
// GLES starts with sRGB framebuffer encoding on, so the first sync always has to push the
// frontend's (desktop-GL default) disabled state down.
static Bool g_syncedSrgbFramebufferWrites = true;
// Set when the shadow below stops describing the real ES context. The ES context
// OUTLIVES every MobileGL context, so a MobileGL context switch leaves it holding the
// previous context's enable state while the frontend's parameter block AND its
// version counter both restart from defaults. Every "differs from what I last pushed"
// test in SyncRenderState would then agree that nothing needs pushing, and the
// leftover state silently applies to the new context - the class of bug the CTS
// caught as GL_FRAMEBUFFER_SRGB surviving from vertex_attrib_binding into
// direct_state_access.renderbuffers_storage. One unconditional push settles the whole
// block rather than the one cap that happened to be noticed.
static Bool g_forceFullRenderStateResync = true;
// Which draw buffers' alpha channel the colour mask last pushed to the driver had forced
// OFF - i.e. the value of `appliedWidenMask` in the last SyncRenderState that reached the
// colour-mask block. NOT derivable from the frontend parameter block: it depends on the
// bound DRAW framebuffer's attachment formats and on whether the caller is a draw or a
// clear, neither of which bumps the frontend render-state version. Without it a
// clear-then-draw pair on an unchanged parameter block early-outs and the draw inherits
// the clear's undoctored mask.
Uint32 g_syncedColorMaskAlphaWidenMask = 0;
// Scratch for the dual-source-blend decline path in the blend block below. File-scope
// rather than a local so the ordinary draw pays nothing for it: it is written only on a
// driver with no GL_EXT_blend_func_extended that is also handed a GL_SRC1_* factor, and
// SyncRenderState runs on the GL thread only.
static Array<PerBufferBlendState, MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS>
g_dualSourceDeclinedBlendStates;
#if MOBILEGL_BUILD_DISAGGREGATED
// The context epoch the shadow was last synced in (SyncRenderState).
static ContextEpoch g_syncedRenderStateEpoch{};
#endif
void InvalidateSyncedRenderState() {
g_forceFullRenderStateResync = true;
g_hasSyncedRenderState = false;
g_syncedBackendViewport = IntVec4(-1, -1, -1, -1);
g_syncedBackendScissorBox = IntVec4(-1, -1, -1, -1);
}
// Whether the shadow's bits are the current native context's. An invalidation (a context
// switch, the backend's turn moving to another session's thread) only raises flags - the
// bits stay, and they are then another context's.
static Bool SyncedRenderStateDescribesCurrentContext() {
if (!g_hasSyncedRenderState || g_forceFullRenderStateResync) return false;
#if MOBILEGL_BUILD_DISAGGREGATED
if (g_syncedRenderStateEpoch != CurrentContextEpoch()) return false;
#endif
return true;
}
Bool DriverScissorTestEnabled() {
if (SyncedRenderStateDescribesCurrentContext())
return (g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
return g_GLESFuncs.glIsEnabled != nullptr && g_GLESFuncs.glIsEnabled(GL_SCISSOR_TEST) == GL_TRUE;
}
void SetSyncedScissorTestForTesting(Bool enabled) {
g_syncedRenderStateParameters.ScissorTestEnabledMask = enabled ? 1u : 0u;
g_hasSyncedRenderState = true;
g_forceFullRenderStateResync = false;
#if MOBILEGL_BUILD_DISAGGREGATED
g_syncedRenderStateEpoch = CurrentContextEpoch();
#endif
}
void SyncRenderState(Bool forColorClear) {
#if MOBILEGL_BUILD_DISAGGREGATED
const auto epoch = CurrentContextEpoch();
if (g_syncedRenderStateEpoch != epoch) {
InvalidateSyncedRenderState();
g_syncedRenderStateEpoch = epoch;
}
#endif
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
Uint16 currentRenderStateVersion = MG_Pipe::gPipeInputs->GetRenderStateParametersVersion();
const Bool forceFullPush = g_forceFullRenderStateResync;
g_forceFullRenderStateResync = false;
// The alpha discipline for widened colour attachments (see the header comment on
// SyncRenderState): a DRAW must not be able to move the stored alpha off 1.0, a CLEAR
// is what puts it there. So the draw path masks alpha off on every widened draw
// buffer and the clear path masks nothing.
const Uint32 appliedWidenMask = forColorClear ? 0u : FramebufferImpl::g_alphaWidenedDrawBufferMask;
const Bool colorMaskWidenDirty = appliedWidenMask != g_syncedColorMaskAlphaWidenMask;
if (!forceFullPush && !colorMaskWidenDirty && g_hasSyncedRenderState &&
currentRenderStateVersion == g_syncedRenderStateVersion) {
// Gate 1 of section 2.3.1: the steady-state cost of this whole function is
// the one Uint16 read above plus this compare.
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytRenderState, /*hit=*/true);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 1);
}
return;
}
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytRenderState, /*hit=*/false);
// The version read above, the parameter-block fetch and the viewport fetch
// below - the three accessor calls this function makes unconditionally on a
// miss. The conditional sRGB capability read further down is deliberately
// NOT counted (see the inventory in PipeStats.cpp).
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3);
}
const auto& parameters = MG_Pipe::gPipeInputs->GetRenderStateParameters();
// The frontend has ONE version for the whole parameter block, so a per-draw blend
// toggle used to re-diff all ~40 pieces of state field by field on every draw
// (Blaze3D brackets every batch with glEnable/glDisable(GL_BLEND), making this the
// hottest thing mc_state_toggle did). Split the struct into three contiguous byte
// spans - the head (viewport/point/line/polygon-offset scalars), the blend array,
// and everything after it - and let one memcmp per span decide whether its blocks
// run at all. memcmp can false-DIFFER on padding bytes (harmless: the field-wise
// block runs and finds nothing) but can never false-match, and after the first full
// sync the tail memcpy below makes the shadow byte-identical, padding included, so
// in the steady state a span memcmp is exact. Blocks whose inputs are NOT in the
// parameter struct (the surface-size viewport fallback, the sRGB context
// capability) stay outside the gates.
static_assert(std::is_trivially_copyable_v<RenderStateParameters>,
"span memcmp/memcpy below treats the parameter block as raw bytes");
constexpr SizeT kBlendSpanBegin = offsetof(RenderStateParameters, BlendStates);
constexpr SizeT kBlendSpanEnd = offsetof(RenderStateParameters, LogicOp);
const auto* currentBytes = reinterpret_cast<const unsigned char*>(&parameters);
const auto* syncedBytes = reinterpret_cast<const unsigned char*>(&g_syncedRenderStateParameters);
const Bool headSpanDirty =
!g_hasSyncedRenderState || std::memcmp(currentBytes, syncedBytes, kBlendSpanBegin) != 0;
const Bool blendSpanDirty =
!g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanBegin, syncedBytes + kBlendSpanBegin,
kBlendSpanEnd - kBlendSpanBegin) != 0;
const Bool tailSpanDirty =
!g_hasSyncedRenderState || std::memcmp(currentBytes + kBlendSpanEnd, syncedBytes + kBlendSpanEnd,
sizeof(RenderStateParameters) - kBlendSpanEnd) != 0;
IntVec4 backendViewport = MG_Pipe::gPipeInputs->GetViewport();
if (backendViewport.z() <= 0 || backendViewport.w() <= 0) {
Int surfaceWidth = 0;
Int surfaceHeight = 0;
if (QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) {
backendViewport = IntVec4(0, 0, surfaceWidth, surfaceHeight);
}
}
if (backendViewport != g_syncedBackendViewport) {
g_GLESFuncs.glViewport(backendViewport.x(), backendViewport.y(), backendViewport.z(),
backendViewport.w());
g_syncedBackendViewport = backendViewport;
}
// Every capability bool (and the scissor-test mask below) lives after LogicOp in the
// struct, i.e. in the tail span.
if (tailSpanDirty) {
#define SYNC_CAPABILITY(cap_mg, cap_gl) \
if (forceFullPush || parameters.cap_mg##Enabled != g_syncedRenderStateParameters.cap_mg##Enabled) { \
if (parameters.cap_mg##Enabled) { \
g_GLESFuncs.glEnable(cap_gl); \
} else { \
g_GLESFuncs.glDisable(cap_gl); \
} \
}
SYNC_CAPABILITY(DepthTest, GL_DEPTH_TEST);
SYNC_CAPABILITY(ColorLogicOp, GL_COLOR_LOGIC_OP);
SYNC_CAPABILITY(Dither, GL_DITHER);
SYNC_CAPABILITY(Multisample, GL_MULTISAMPLE);
SYNC_CAPABILITY(SampleAlphaToCoverage, GL_SAMPLE_ALPHA_TO_COVERAGE);
SYNC_CAPABILITY(SampleCoverage, GL_SAMPLE_COVERAGE);
SYNC_CAPABILITY(SampleMask, GL_SAMPLE_MASK);
SYNC_CAPABILITY(PolygonOffsetFill, GL_POLYGON_OFFSET_FILL);
SYNC_CAPABILITY(RasterizerDiscard, GL_RASTERIZER_DISCARD);
SYNC_CAPABILITY(StencilTest, GL_STENCIL_TEST);
SYNC_CAPABILITY(CullFace, GL_CULL_FACE);
#undef SYNC_CAPABILITY
// GL_SCISSOR_TEST is per-viewport enable state (ARB_viewport_array), so it is a
// 16-bit mask and not a "<Name>Enabled" bool the macro above could key off. ES
// has exactly one scissor rectangle and one scissor enable, so only bit 0 - the
// index every ES draw rasterizes against - can be forwarded; a program that
// enables the test for viewport 3 alone gets viewport 0's answer here. That is
// the same limitation as the unemulated gl_ViewportIndex on this backend and is
// why the multi-viewport half of KHR-GL43.viewport_array stays red on Espryt.
{
const Bool scissorTest = (parameters.ScissorTestEnabledMask & 1u) != 0;
const Bool syncedScissorTest = (g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
if (forceFullPush || scissorTest != syncedScissorTest) {
scissorTest ? g_GLESFuncs.glEnable(GL_SCISSOR_TEST) : g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
}
}
}
if (tailSpanDirty && g_GLESCapabilities.SupportsClipDistance) {
// gl_ClipDistance clipping is per-distance enable state in GL, and ES reaches it
// only through GL_EXT_clip_cull_distance. That extension reuses the desktop enum
// values for CLIP_DISTANCE0_EXT..7_EXT, but the token is absent from the ES
// headers this file compiles against, hence the local name. Without the
// extension there is nowhere to put the state and the shader could not have
// compiled either, so the whole block is gated rather than silently no-op'ing.
constexpr GLenum kClipDistance0 = 0x3000;
constexpr Uint kClipDistanceCount = 8;
const Uint32 mask = parameters.ClipDistanceEnabledMask;
const Uint32 syncedMask = g_syncedRenderStateParameters.ClipDistanceEnabledMask;
if (forceFullPush || mask != syncedMask) {
const Uint32 changed = forceFullPush ? ~0u : (mask ^ syncedMask);
for (Uint i = 0; i < kClipDistanceCount; ++i) {
const Uint32 bit = 1u << i;
if ((changed & bit) == 0) continue;
const GLenum cap = static_cast<GLenum>(kClipDistance0 + i);
(mask & bit) ? g_GLESFuncs.glEnable(cap) : g_GLESFuncs.glDisable(cap);
}
}
}
{ // sRGB framebuffer writes. GLES core always encodes a write into an sRGB attachment,
// while GL_FRAMEBUFFER_SRGB is disabled by default in desktop GL and the frontend
// never turns it on, so the driver has to be told to write raw. Without this a render
// into an sRGB colour buffer comes back encoded once too often (the shader's own
// decode on the next fetch then leaves the value one conversion short).
const Bool srgbWrites = MG_Pipe::gPipeInputs->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb);
if (g_GLESCapabilities.SupportsSrgbWriteControl &&
(forceFullPush || srgbWrites != g_syncedSrgbFramebufferWrites)) {
srgbWrites ? g_GLESFuncs.glEnable(GL_FRAMEBUFFER_SRGB) : g_GLESFuncs.glDisable(GL_FRAMEBUFFER_SRGB);
g_syncedSrgbFramebufferWrites = srgbWrites;
}
}
if (tailSpanDirty) { // Primitive restart. GLES core has only GL_PRIMITIVE_RESTART_FIXED_INDEX (fixed
// all-ones value); both the fixed cap and the (fixed-valued) arbitrary GL_PRIMITIVE_RESTART map to
// it. An arbitrary non-fixed restart index is rejected at draw time (see DrawElements).
const Bool restart = parameters.PrimitiveRestartFixedIndexEnabled || parameters.PrimitiveRestartEnabled;
const Bool syncedRestart = g_syncedRenderStateParameters.PrimitiveRestartFixedIndexEnabled ||
g_syncedRenderStateParameters.PrimitiveRestartEnabled;
if (forceFullPush || restart != syncedRestart) {
restart ? g_GLESFuncs.glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX)
: g_GLESFuncs.glDisable(GL_PRIMITIVE_RESTART_FIXED_INDEX);
}
}
const auto& ToGLBoolean = [](Bool b) -> GLboolean { return b ? GL_TRUE : GL_FALSE; };
// Which draw buffers the blend block below DECLINED (see it for why). Needed again at
// the shadow write-back at the end of this function: the span memcpy there clones the
// FRONTEND block, which for a declined draw buffer is not what the driver was handed.
Uint32 dualSourceDeclinedMask = 0;
if (blendSpanDirty) { // Blend State
using FBO = MG_State::GLState::FramebufferObject;
auto& syncedStates = g_syncedRenderStateParameters.BlendStates;
// Dual-source blending (GL_SRC1_* factors from glBlendFunc paired with
// glBindFragDataLocationIndexed) needs GL_EXT_blend_func_extended; GLES core has none.
// Detected at load and surfaced in the POST. There is no fallback that BLENDS
// correctly, so a draw that asks for a SRC1 factor on a driver without the extension
// gets the blend DECLINED: that draw buffer is pushed with blending off and neutral
// One/Zero factors, and the loss is logged once. The two rejected alternatives are
// both worse - pushing GL_SRC1_* at glBlendFuncSeparate leaves the driver to raise
// GL_INVALID_ENUM and keep whatever factors were there before (a silent mis-blend
// against stale state), and throwing, which is what this did until now, takes the
// whole process down over one unsupported blend factor. Declining is defined,
// survivable and visible in the log.
//
// NOT gated on Enabled, deliberately, and the same way the Vulkan twin is not gated
// on effectiveBlendEnabled: what has to be kept away from the driver is the FACTOR
// ENUM, and the factor push below never consults Enabled - one glBlendFuncSeparate
// serves every draw buffer when they agree, and the per-index arm diffs factors
// alone. So `glDisable(GL_BLEND); glBlendFunc(GL_SRC1_ALPHA, ...)` followed by any
// draw OR clear would otherwise hand a GL_SRC1_ALPHA to a driver that answers
// GL_INVALID_ENUM, leaving a spurious error in ITS queue for the next internal
// no-error probe to read as its own, and leaving this shadow recording factors the
// ES context rejected. Blending being off makes the picture unaffected; it does not
// make the enum acceptable.
const auto* effectiveBlendStates = &parameters.BlendStates;
if (!g_GLESCapabilities.SupportsDualSourceBlend) {
Uint32 declinedWithBlendingOnMask = 0;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
const auto& s = parameters.BlendStates[i];
if (IsDualSourceBlendFactor(s.SrcFactorRGB) || IsDualSourceBlendFactor(s.DstFactorRGB) ||
IsDualSourceBlendFactor(s.SrcFactorAlpha) || IsDualSourceBlendFactor(s.DstFactorAlpha)) {
dualSourceDeclinedMask |= 1u << i;
if (s.Enabled) declinedWithBlendingOnMask |= 1u << i;
}
}
if (dualSourceDeclinedMask != 0) {
// Two masks in the message because they mean different things to whoever
// reads the log: the second one is where a PICTURE was lost. A draw buffer
// in the first mask but not the second had blending off anyway, so nothing
// was blended and nothing was dropped - only the unusable enum was kept out
// of the driver.
MGLOG_E_ONCE("SyncRenderState: a GL_SRC1_* (dual-source) blend factor was set on draw buffer "
"mask 0x%x, but the GLES driver does not expose GL_EXT_blend_func_extended (see "
"the dual-source blend row in the driver POST). Those draw buffers are pushed "
"with neutral One/Zero factors instead. Blending was actually ENABLED on mask "
"0x%x, and only there is anything lost: the fragment's first output is written "
"unblended and the second source is dropped.",
dualSourceDeclinedMask, declinedWithBlendingOnMask);
g_dualSourceDeclinedBlendStates = parameters.BlendStates;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
if ((dualSourceDeclinedMask & (1u << i)) == 0) continue;
auto& s = g_dualSourceDeclinedBlendStates[i];
// Both halves, for the same reason the Vulkan arm neutralises both: the
// enable so nothing blends against a source the driver cannot produce,
// the factors so no GL_SRC1_* enum is ever handed over. Clearing Enabled
// on a buffer that was already off is a no-op, which is what makes one
// ungated rule serve both cases.
s.Enabled = false;
s.SrcFactorRGB = BlendFactor::One;
s.DstFactorRGB = BlendFactor::Zero;
s.SrcFactorAlpha = BlendFactor::One;
s.DstFactorAlpha = BlendFactor::Zero;
}
effectiveBlendStates = &g_dualSourceDeclinedBlendStates;
}
}
// The rest of the block reads the EFFECTIVE state. The per-field writes it makes
// into `syncedStates` are provisional - the span memcpy at the end of this function
// overwrites the whole blend span with the frontend's own bytes - so the declined
// draw buffers are put back there, see the write-back below.
const auto& targetStates = *effectiveBlendStates;
Bool allEnabled = true;
Bool allDisabled = true;
Bool anyCapDirty = forceFullPush;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
Bool enabled = targetStates[i].Enabled;
if (enabled)
allDisabled = false;
else
allEnabled = false;
if (enabled != syncedStates[i].Enabled) {
anyCapDirty = true;
}
}
if (anyCapDirty) {
if (allEnabled) {
g_GLESFuncs.glEnable(GL_BLEND);
for (auto& s : syncedStates)
s.Enabled = true;
} else if (allDisabled) {
g_GLESFuncs.glDisable(GL_BLEND);
for (auto& s : syncedStates)
s.Enabled = false;
} else {
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
if (forceFullPush || targetStates[i].Enabled != syncedStates[i].Enabled) {
syncedStates[i].Enabled = targetStates[i].Enabled;
syncedStates[i].Enabled ? g_GLESFuncs.glEnablei(GL_BLEND, i)
: g_GLESFuncs.glDisablei(GL_BLEND, i);
}
}
}
}
Bool allFuncsSame = true;
Bool anyFuncDirty = forceFullPush;
const auto& first = targetStates[0];
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
const auto& cur = targetStates[i];
const auto& syn = syncedStates[i];
Bool isDiffFromSyn =
(cur.SrcFactorRGB != syn.SrcFactorRGB || cur.DstFactorRGB != syn.DstFactorRGB ||
cur.SrcFactorAlpha != syn.SrcFactorAlpha || cur.DstFactorAlpha != syn.DstFactorAlpha);
if (isDiffFromSyn) anyFuncDirty = true;
if (allFuncsSame && i > 0) {
if (cur.SrcFactorRGB != first.SrcFactorRGB || cur.DstFactorRGB != first.DstFactorRGB ||
cur.SrcFactorAlpha != first.SrcFactorAlpha || cur.DstFactorAlpha != first.DstFactorAlpha) {
allFuncsSame = false;
}
}
}
if (anyFuncDirty) {
if (allFuncsSame) {
g_GLESFuncs.glBlendFuncSeparate(MG_Util::ConvertBlendFactorToGLEnum(first.SrcFactorRGB),
MG_Util::ConvertBlendFactorToGLEnum(first.DstFactorRGB),
MG_Util::ConvertBlendFactorToGLEnum(first.SrcFactorAlpha),
MG_Util::ConvertBlendFactorToGLEnum(first.DstFactorAlpha));
for (auto& syn : syncedStates) {
syn.SrcFactorRGB = first.SrcFactorRGB;
syn.DstFactorRGB = first.DstFactorRGB;
syn.SrcFactorAlpha = first.SrcFactorAlpha;
syn.DstFactorAlpha = first.DstFactorAlpha;
}
} else {
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
const auto& cur = targetStates[i];
auto& syn = syncedStates[i];
if (forceFullPush || cur.SrcFactorRGB != syn.SrcFactorRGB ||
cur.DstFactorRGB != syn.DstFactorRGB || cur.SrcFactorAlpha != syn.SrcFactorAlpha ||
cur.DstFactorAlpha != syn.DstFactorAlpha) {
syn.SrcFactorRGB = cur.SrcFactorRGB;
syn.DstFactorRGB = cur.DstFactorRGB;
syn.SrcFactorAlpha = cur.SrcFactorAlpha;
syn.DstFactorAlpha = cur.DstFactorAlpha;
g_GLESFuncs.glBlendFuncSeparatei(
i, MG_Util::ConvertBlendFactorToGLEnum(cur.SrcFactorRGB),
MG_Util::ConvertBlendFactorToGLEnum(cur.DstFactorRGB),
MG_Util::ConvertBlendFactorToGLEnum(cur.SrcFactorAlpha),
MG_Util::ConvertBlendFactorToGLEnum(cur.DstFactorAlpha));
}
}
}
}
Bool allEquationsSame = true;
Bool anyEquationDirty = forceFullPush;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
const auto& cur = targetStates[i];
const auto& syn = syncedStates[i];
if (cur.ColorEquation != syn.ColorEquation || cur.AlphaEquation != syn.AlphaEquation) {
anyEquationDirty = true;
}
if (allEquationsSame && i > 0) {
if (cur.ColorEquation != first.ColorEquation || cur.AlphaEquation != first.AlphaEquation) {
allEquationsSame = false;
}
}
}
if (anyEquationDirty) {
if (allEquationsSame) {
g_GLESFuncs.glBlendEquationSeparate(MG_Util::ConvertBlendEquationToGLEnum(first.ColorEquation),
MG_Util::ConvertBlendEquationToGLEnum(first.AlphaEquation));
for (auto& syn : syncedStates) {
syn.ColorEquation = first.ColorEquation;
syn.AlphaEquation = first.AlphaEquation;
}
} else {
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
const auto& cur = targetStates[i];
auto& syn = syncedStates[i];
if (forceFullPush || cur.ColorEquation != syn.ColorEquation ||
cur.AlphaEquation != syn.AlphaEquation) {
syn.ColorEquation = cur.ColorEquation;
syn.AlphaEquation = cur.AlphaEquation;
g_GLESFuncs.glBlendEquationSeparatei(
i, MG_Util::ConvertBlendEquationToGLEnum(cur.ColorEquation),
MG_Util::ConvertBlendEquationToGLEnum(cur.AlphaEquation));
}
}
}
}
}
if (tailSpanDirty) { // Depth state
if (forceFullPush || parameters.DepthFunc != g_syncedRenderStateParameters.DepthFunc) {
g_GLESFuncs.glDepthFunc(MG_Util::ConvertDepthTestFuncToGLEnum(parameters.DepthFunc));
}
if (forceFullPush || parameters.DepthMask != g_syncedRenderStateParameters.DepthMask) {
g_GLESFuncs.glDepthMask(parameters.DepthMask ? GL_TRUE : GL_FALSE);
}
if (forceFullPush || parameters.DepthRanges[0] != g_syncedRenderStateParameters.DepthRanges[0]) {
g_GLESFuncs.glDepthRangef(parameters.DepthRanges[0].x(), parameters.DepthRanges[0].y());
}
}
if (tailSpanDirty) { // Stencil state
for (SizeT faceIndex = 0; faceIndex < parameters.StencilStates.size(); ++faceIndex) {
const StencilFaceState& current = parameters.StencilStates[faceIndex];
const StencilFaceState& synced = g_syncedRenderStateParameters.StencilStates[faceIndex];
const GLenum glFace = faceIndex == 0 ? GL_FRONT : GL_BACK;
if (forceFullPush || current.Func != synced.Func || current.Ref != synced.Ref ||
current.ValueMask != synced.ValueMask) {
g_GLESFuncs.glStencilFuncSeparate(glFace, MG_Util::ConvertDepthTestFuncToGLEnum(current.Func),
current.Ref, current.ValueMask);
}
if (forceFullPush || current.WriteMask != synced.WriteMask) {
g_GLESFuncs.glStencilMaskSeparate(glFace, current.WriteMask);
}
if (forceFullPush || current.FailOp != synced.FailOp ||
current.PassDepthFailOp != synced.PassDepthFailOp ||
current.PassDepthPassOp != synced.PassDepthPassOp) {
g_GLESFuncs.glStencilOpSeparate(
glFace, MG_Util::ConvertStencilOperationToGLEnum(current.FailOp),
MG_Util::ConvertStencilOperationToGLEnum(current.PassDepthFailOp),
MG_Util::ConvertStencilOperationToGLEnum(current.PassDepthPassOp));
}
}
}
if (tailSpanDirty || colorMaskWidenDirty) { // Color mask. Uniform masks use the non-indexed glColorMask
// (works everywhere); divergent per-draw-buffer masks use the indexed glColorMaski when
// draw_buffers_indexed is available, otherwise fall back to broadcasting draw buffer 0.
// Mirrors the blend block.
using FBO = MG_State::GLState::FramebufferObject;
const auto& targetMasks = parameters.ColorMasks;
const auto& syncedMasks = g_syncedRenderStateParameters.ColorMasks;
// What the DRIVER is told for draw buffer i. Identical to the application's mask
// except on a widened attachment during a draw, where alpha is forced off; the
// frontend's own array is never written, so glGet(GL_COLOR_WRITEMASK) keeps
// answering with the application's value.
const auto driverMask = [&](Uint i) -> BoolVec4 {
BoolVec4 m = targetMasks[i];
if (i < 32 && (appliedWidenMask & (1u << i)) != 0) {
m.w() = false;
}
return m;
};
Bool anyDirty = forceFullPush || colorMaskWidenDirty;
Bool allSame = true;
const BoolVec4 driverMask0 = driverMask(0);
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
if (targetMasks[i] != syncedMasks[i]) anyDirty = true;
if (i > 0 && driverMask(i) != driverMask0) allSame = false;
}
if (anyDirty) {
if (allSame || !g_GLESCapabilities.SupportsIndexedColorMask) {
// Without draw_buffers_indexed there is only one mask for the whole
// framebuffer, so a widened draw buffer 0 costs every other buffer its
// alpha writes. ES 3.2 makes glColorMaski core and ES 3.1 has it as
// EXT/OES; the only devices that reach this line are ES 3.0-class, where
// MRT with a mixed widened/native colour attachment set is already rare.
const BoolVec4& m = driverMask0;
g_GLESFuncs.glColorMask(ToGLBoolean(m.x()), ToGLBoolean(m.y()), ToGLBoolean(m.z()),
ToGLBoolean(m.w()));
} else {
const auto colorMaskiFn = g_GLESFuncs.glColorMaski ? g_GLESFuncs.glColorMaski
: g_GLESFuncs.glColorMaskiEXT ? g_GLESFuncs.glColorMaskiEXT
: g_GLESFuncs.glColorMaskiOES;
for (Uint i = 0; i < FBO::MAX_DRAW_BUFFERS; ++i) {
// colorMaskWidenDirty forces every slot: the previous push may have
// been the non-indexed glColorMask above (which set all of them), and
// the per-slot diff below only knows about the application's array.
if (forceFullPush || colorMaskWidenDirty || targetMasks[i] != syncedMasks[i]) {
const BoolVec4 m = driverMask(i);
colorMaskiFn(i, ToGLBoolean(m.x()), ToGLBoolean(m.y()), ToGLBoolean(m.z()),
ToGLBoolean(m.w()));
}
}
}
}
g_syncedColorMaskAlphaWidenMask = appliedWidenMask;
}
if (tailSpanDirty) { // Polygon mode. GLES core has no glPolygonMode; use NV/ANGLE_polygon_mode when
// present.
// Without the extension the mode stays FILL and non-FILL requests are dropped.
if ((forceFullPush || parameters.PolygonModeFront != g_syncedRenderStateParameters.PolygonModeFront) &&
g_GLESCapabilities.SupportsPolygonMode) {
const auto polygonModeFn =
g_GLESFuncs.glPolygonModeNV ? g_GLESFuncs.glPolygonModeNV : g_GLESFuncs.glPolygonModeANGLE;
polygonModeFn(GL_FRONT_AND_BACK, parameters.PolygonModeFront);
}
}
if (tailSpanDirty) { // Clear values
if (forceFullPush || parameters.ClearColor != g_syncedRenderStateParameters.ClearColor) {
const FloatVec4& clearCol = parameters.ClearColor;
g_GLESFuncs.glClearColor(clearCol.x(), clearCol.y(), clearCol.z(), clearCol.w());
}
if (forceFullPush || parameters.ClearDepth != g_syncedRenderStateParameters.ClearDepth) {
g_GLESFuncs.glClearDepthf(parameters.ClearDepth);
}
if (forceFullPush || parameters.ClearStencil != g_syncedRenderStateParameters.ClearStencil) {
g_GLESFuncs.glClearStencil(static_cast<GLint>(parameters.ClearStencil));
}
if (forceFullPush || parameters.BlendColor != g_syncedRenderStateParameters.BlendColor) {
const FloatVec4& blendColor = parameters.BlendColor;
g_GLESFuncs.glBlendColor(blendColor.x(), blendColor.y(), blendColor.z(), blendColor.w());
}
}
if (tailSpanDirty) { // Cull face mode
if (forceFullPush ||
parameters.CullFaceModeSetting != g_syncedRenderStateParameters.CullFaceModeSetting) {
const CullFaceMode& cfm = parameters.CullFaceModeSetting;
g_GLESFuncs.glCullFace(MG_Util::ConvertCullFaceModeToGLEnum(cfm));
}
if (forceFullPush ||
parameters.FrontFaceModeSetting != g_syncedRenderStateParameters.FrontFaceModeSetting) {
const FrontFaceMode& ffm = parameters.FrontFaceModeSetting;
g_GLESFuncs.glFrontFace(MG_Util::ConvertFrontFaceModeToGLEnum(ffm));
}
}
if (tailSpanDirty) { // Scissor box. Resolved and shadowed like the viewport above, and
// for the same reason: what has to reach the driver is NOT simply the parameter
// field. RenderStateParameters::ScissorBoxes starts all-zero, which means "the
// application has never called glScissor" - it is not a GL scissor box. GL's
// initial box is the whole window, which the frontend has no way to spell before a
// surface exists. The pre-resync code got away with pushing the field verbatim only
// by accident: the shadow held the same default, the field never compared unequal,
// and the ES context kept its own correct default. Under the forced full push that
// accident is gone, glScissor(0,0,0,0) shrinks the scissor to an EMPTY rectangle,
// and everything drawn with GL_SCISSOR_TEST enabled before the app's first
// glScissor is clipped away - Minecraft 26.2 keeps only its unscissored sky and
// hand and loses the terrain and the whole GUI.
//
// The condition is the WRITTEN FLAG, not the extent. An empty rectangle is a
// perfectly legal thing to ask for - glScissor(0,0,0,0) means "the scissor test
// rejects every fragment" - so testing `width <= 0 || height <= 0` substituted the
// whole surface for a deliberately empty box and inverted the request into "accept
// every fragment", no matter how many times the application had already called
// glScissor. KHR-GL43.viewport_array.scissor_zero_dimension is exactly that: all 16
// boxes zero-sized with the test enabled, requiring the draw to be clipped away
// entirely. Reading the flag preserves the Minecraft protection bit-for-bit - before
// the first glScissor the bit is clear and the surface size is still substituted -
// while an explicit empty box now reaches the driver verbatim. Negative extents
// cannot arrive here at all: all three entry points reject them with
// GL_INVALID_VALUE before storing (GL_RenderState.cpp's ValidateNonNegativeExtent).
IntVec4 backendScissorBox = parameters.ScissorBoxes[0];
if ((parameters.ScissorBoxWrittenMask & 1u) == 0) {
Int surfaceWidth = 0;
Int surfaceHeight = 0;
if (QueryCurrentSurfaceSize(surfaceWidth, surfaceHeight)) {
backendScissorBox = IntVec4(0, 0, surfaceWidth, surfaceHeight);
}
}
// Compared against what was actually PUSHED, not against the parameter field, so
// the resolved value and the diff can never disagree.
if (backendScissorBox != g_syncedBackendScissorBox) {
g_GLESFuncs.glScissor(backendScissorBox.x(), backendScissorBox.y(), backendScissorBox.z(),
backendScissorBox.w());
g_syncedBackendScissorBox = backendScissorBox;
}
}
if (tailSpanDirty) { // Logic op (first field of the tail span)
// glLogicOp is GLES 1.x / EXT only - eglGetProcAddress returns null for it on a
// plain ES 3.x driver. Before the forced resync it was reached only when an app
// actually set a logic op; now every MakeCurrent would call it, so the null check
// is mandatory rather than defensive.
if (g_GLESFuncs.glLogicOp &&
(forceFullPush || parameters.LogicOp != g_syncedRenderStateParameters.LogicOp)) {
g_GLESFuncs.glLogicOp(MG_Util::ConvertLogicOperationToGLEnum(parameters.LogicOp));
}
}
if (headSpanDirty) { // Polygon offset (head-span scalars, like line width / point size below)
if (forceFullPush ||
parameters.PolygonOffsetFactor != g_syncedRenderStateParameters.PolygonOffsetFactor ||
parameters.PolygonOffsetUnits != g_syncedRenderStateParameters.PolygonOffsetUnits ||
parameters.PolygonOffsetClamp != g_syncedRenderStateParameters.PolygonOffsetClamp) {
// glPolygonOffset IS glPolygonOffsetClampEXT(factor, units, 0), so where the
// driver has the clamp entry point it carries all three values; without it the
// clamp cannot be expressed and the offset goes through unclamped.
if (g_GLESCapabilities.SupportsPolygonOffsetClamp) {
g_GLESFuncs.glPolygonOffsetClampEXT(parameters.PolygonOffsetFactor,
parameters.PolygonOffsetUnits,
parameters.PolygonOffsetClamp);
} else {
g_GLESFuncs.glPolygonOffset(parameters.PolygonOffsetFactor, parameters.PolygonOffsetUnits);
}
}
}
if (headSpanDirty) { // Clip control. GL_ARB_clip_control is only advertised where the
// driver has GL_EXT_clip_control, so without it the state can only be the default.
if (g_GLESCapabilities.SupportsClipControl &&
(forceFullPush || parameters.ClipOrigin != g_syncedRenderStateParameters.ClipOrigin ||
parameters.ClipDepthMode != g_syncedRenderStateParameters.ClipDepthMode)) {
g_GLESFuncs.glClipControlEXT(parameters.ClipOrigin, parameters.ClipDepthMode);
}
}
if (headSpanDirty) { // Line width
if (forceFullPush || parameters.LineWidth != g_syncedRenderStateParameters.LineWidth) {
g_GLESFuncs.glLineWidth(parameters.LineWidth);
}
}
if (headSpanDirty) { // Point size (GLES 1.x only - ES 2+ sets it from gl_PointSize,
// so the entry point is absent on most drivers; see the glLogicOp note above)
if (g_GLESFuncs.glPointSize &&
(forceFullPush || parameters.PointSize != g_syncedRenderStateParameters.PointSize)) {
g_GLESFuncs.glPointSize(parameters.PointSize);
}
}
if (tailSpanDirty) { // Sample coverage
if (forceFullPush ||
parameters.SampleCoverageValue != g_syncedRenderStateParameters.SampleCoverageValue ||
parameters.SampleCoverageInvert != g_syncedRenderStateParameters.SampleCoverageInvert) {
g_GLESFuncs.glSampleCoverage(parameters.SampleCoverageValue,
ToGLBoolean(parameters.SampleCoverageInvert));
}
}
if (tailSpanDirty) { // Sample mask
if (g_GLESFuncs.glSampleMaski &&
(forceFullPush || parameters.SampleMaskValue != g_syncedRenderStateParameters.SampleMaskValue)) {
g_GLESFuncs.glSampleMaski(0, parameters.SampleMaskValue);
}
}
if (tailSpanDirty) { // Sample shading (ARB_sample_shading; ES 3.2 core)
// Both halves are gated on the same entry point rather than on a version check:
// GL_SAMPLE_SHADING and glMinSampleShading arrived together (ES 3.2 core /
// OES_sample_shading), so a null pointer means glEnable(GL_SAMPLE_SHADING) would
// only push an INVALID_ENUM into the driver's queue. This is NOT part of the
// SYNC_CAPABILITY block above for exactly that reason - that macro has nowhere to
// put a guard.
if (g_GLESFuncs.glMinSampleShading) {
if (forceFullPush ||
parameters.SampleShadingEnabled != g_syncedRenderStateParameters.SampleShadingEnabled) {
if (parameters.SampleShadingEnabled) {
g_GLESFuncs.glEnable(GL_SAMPLE_SHADING);
} else {
g_GLESFuncs.glDisable(GL_SAMPLE_SHADING);
}
}
if (forceFullPush ||
parameters.MinSampleShadingValue != g_syncedRenderStateParameters.MinSampleShadingValue) {
g_GLESFuncs.glMinSampleShading(parameters.MinSampleShadingValue);
}
}
}
if (tailSpanDirty && g_GLESCapabilities.SupportsFragmentShadingRate) { // GL_EXT_fragment_shading_rate
// The frontend stores the packed rate and op indices; the driver takes the enums.
// Only forwarded where the driver has the extension, which is also the only place
// the frontend advertised it - so a non-default value cannot exist anywhere else.
if (forceFullPush || parameters.ShadingRate != g_syncedRenderStateParameters.ShadingRate) {
const GLenum rate = MGUnpackShadingRate(parameters.ShadingRate);
g_GLESFuncs.glShadingRateEXT(rate != GL_NONE ? rate : GL_SHADING_RATE_1X1_PIXELS_EXT);
}
if (forceFullPush ||
parameters.ShadingRateCombinerOp0 != g_syncedRenderStateParameters.ShadingRateCombinerOp0 ||
parameters.ShadingRateCombinerOp1 != g_syncedRenderStateParameters.ShadingRateCombinerOp1) {
g_GLESFuncs.glShadingRateCombinerOpsEXT(
MGShadingRateCombinerOpEnum(parameters.ShadingRateCombinerOp0),
MGShadingRateCombinerOpEnum(parameters.ShadingRateCombinerOp1));
}
}
g_syncedRenderStateVersion = currentRenderStateVersion;
// Byte copy, not member copy: it also clones the frontend struct's padding bytes,
// which is what lets the span memcmps above answer "unchanged" exactly instead of
// tripping on indeterminate padding every draw. Only the dirty spans need copying -
// a clean span's bytes are already identical by the very memcmp that skipped it.
auto* syncedBytesMut = reinterpret_cast<unsigned char*>(&g_syncedRenderStateParameters);
if (headSpanDirty) {
std::memcpy(syncedBytesMut, currentBytes, kBlendSpanBegin);
}
if (blendSpanDirty) {
std::memcpy(syncedBytesMut + kBlendSpanBegin, currentBytes + kBlendSpanBegin,
kBlendSpanEnd - kBlendSpanBegin);
// ...except for a draw buffer whose dual-source blend was DECLINED, where the
// frontend block is precisely what did NOT reach the driver. The shadow has to hold
// what was pushed or the next diff compares against state the ES context never got:
// going from a SRC1 factor to an ordinary one leaves Enabled equal on both sides,
// the enable block finds nothing to do, and blending stays off from the decline.
// The span stays permanently "dirty" against the frontend as a result, which costs
// one memcmp plus this block per render-state VERSION change - the top-of-function
// version early-out still skips repeat draws entirely.
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
if ((dualSourceDeclinedMask & (1u << i)) == 0) continue;
g_syncedRenderStateParameters.BlendStates[i] = g_dualSourceDeclinedBlendStates[i];
}
}
if (tailSpanDirty) {
std::memcpy(syncedBytesMut + kBlendSpanEnd, currentBytes + kBlendSpanEnd,
sizeof(RenderStateParameters) - kBlendSpanEnd);
}
g_hasSyncedRenderState = true;
}
} // namespace RenderStateImpl
namespace PrgramImpl {
// The twin SyncCurrentProgram resolved for the draw/dispatch being prepared,
// consumed by BindCurrentProgramWithResources and the post-draw draw-parameter
// updates in the same GL entry point — they all used to repeat the registry
// Find. Valid only while the keyed frontend program is the current draw
// program: every consumer compares the raw key against GetProgramForDraw()
// before trusting the twin, and SyncCurrentProgram rewrites the pair at the
// top of every PrepareForDraw/PrepareForCompute, so a recycled address can
// never be consumed (the stale pair is overwritten before any consumer runs).
static const MG_State::GLState::ProgramObject* g_currentDrawFrontendProgram = nullptr;
BackendProgramObjectImpl* g_currentDrawBackendProgram = nullptr;
// P5e (pg), CONTRACT-P5E.md §5.5: THE SAME STASH, KEYED ON THE HANDLE. The raw frontend
// pointer above is one of the five frontend-keyed reads this family retires: every
// consumer compared it against GetProgramForDraw().get(), which under run-ahead is a
// pointer to an object the client may already have relinked or freed. The handle arm
// compares against MGPipeApplier().DrawProgram instead - a {slot, gen} the record
// carried, which cannot be recycled behind the server's back because the gen moves with
// the slot.
//
// BOTH HALVES SURVIVE because the two arms do (ruling 1): the pointer is the monolith
// glue's key and the handle is the transport's, and SyncCurrentProgram* clears the one
// it does not use so a stale key can never be consumed.
MG_Pipe::MGPipeHandle g_currentDrawProgramHandle = MG_Pipe::kMGPipeNullHandle;
// Memo of the per-draw enabled-draw-buffers walk feeding g_fragColorBroadcastCount:
// the answer is a pure function of WHICH FBO is bound and its draw-buffer edits, so
// it is keyed exactly like SyncCurrentFBO's synced trio - object pointer (identity),
// slot version (a recycled heap address cannot re-match: every real rebind bumps
// it), and object version (every attachment/draw-buffer/read-buffer edit bumps it,
// the same documented invariant the FBO sync memo already leans on). A null FBO
// keys on (nullptr, slot version, 0).
static const MG_State::GLState::FramebufferObject* g_broadcastMemoFbo = nullptr;
static Uint16 g_broadcastMemoSlotVersion = 0;
static Uint16 g_broadcastMemoObjectVersion = 0;
Bool g_broadcastMemoValid = false;
static Uint g_broadcastMemoCount = 1;
// P4a e1 (D-C4). The handle arm's key is the framebuffer record's ContentHash, and it
// is a COMPLETE key for this answer rather than a cheaper approximation of the trio
// above: the hash is taken over every field the record carries - Fbo included, so a
// recycled framebuffer handle cannot be suppressed against its predecessor's record,
// and DrawBuffers[8] included, so an unchanged hash provably means the draw-buffer
// array did not move, which provably means the broadcast count did not move.
//
// The ORDERING that made this read come from the frontend is preserved and is what
// makes reading a record legal here: the client emits every set_* BEFORE the verb and
// the server specialises AT the verb, so by the time this runs the record already
// describes the framebuffer this draw will use - which is exactly the property the
// frontend read was buying (a program compiled against a stale count would otherwise
// not be relinked until the draw after the one that needed it).
//
// Separate statics rather than reusing the trio's: FramebufferSerial goes from 0 to
// non-zero exactly once in a process's life, so both arms can run in one run, and one
// set of fields carrying two key shapes is how a stale half is compared against a live
// one.
static Uint64 g_broadcastMemoContentHash = 0;
Bool g_broadcastMemoHandleValid = false;
static Uint g_broadcastMemoHandleCount = 1;
// P4a e4 (D-H6, D-H7). The ShaderCso record behind a handle, WITH the composite band
// resolved: the band starts at kMGPipeShaderCsoCompositeSlotBase and the applier keeps
// it in a second dense table indexed by (slot - base), because one composite in the
// slot-indexed vector would grow it to ~983k records. The SERVER STILL NEVER LEARNS IT
// IS A COMPOSITE - a composite handle is an ordinary ShaderCso handle and every call
// names it as one; this is an indexing detail, and it lives in one place so that only
// one reader has to know it.
const MG_Pipe::MGPipeShaderCsoRecord* FindShaderCsoRecord(MG_Pipe::MGPipeHandle handle) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr;
const auto& st = MG_Pipe::MGPipeApplier();
const Vector<MG_Pipe::MGPipeShaderCsoRecord>* table = nullptr;
Uint32 index = 0;
if (MG_Pipe::MGPipeIsCompositeShaderSlot(handle.Slot)) {
table = &st.CompositeShaderCsos;
index = handle.Slot - MG_Pipe::kMGPipeShaderCsoCompositeSlotBase;
} else {
table = &st.ShaderCsos;
index = handle.Slot;
}
// P4a decline-site P2: S - FLIPPED AT THE VERIFICATION ROUND to loud-once, and it
// TELLS THE TWO BANDS APART as the review requires. An out-of-range ORDINARY slot
// means the create_shader_state for this program never arrived - a client seam.
// An out-of-range COMPOSITE index is a different finding entirely: that table is
// dense and its band base is a fixed contract constant, so a handle above its end
// is a mis-decoded slot, i.e. a CONTRACT-side bug, and saying "the record has not
// arrived" about it would send the reader to the wrong package.
if (index >= table->size()) {
if (MG_Pipe::MGPipeIsCompositeShaderSlot(handle.Slot)) {
MGLOG_E_ONCE("A program record does not describe the binding it names: composite "
"ShaderCso {slot %u, gen %u} decodes to band index %u but the "
"composite table holds %llu - the band base or the decode is wrong, "
"not the emission; running the frontend's own uniform block.",
static_cast<unsigned>(handle.Slot), static_cast<unsigned>(handle.Gen),
static_cast<unsigned>(index), static_cast<unsigned long long>(table->size()));
} else {
MGLOG_E_ONCE("A program record does not describe the binding it names: ShaderCso "
"{slot %u, gen %u} is above the applier's table of %llu record(s), so "
"no create_shader_state for it has been applied; running the "
"frontend's own uniform block.",
static_cast<unsigned>(handle.Slot), static_cast<unsigned>(handle.Gen),
static_cast<unsigned long long>(table->size()));
}
return nullptr;
}
const MG_Pipe::MGPipeShaderCsoRecord& record = (*table)[index];
// P4a decline-site P3: S - FLIPPED AT THE VERIFICATION ROUND to loud-once, and it
// is ID-8's stale-generation refusal: every GetOrCreate(handle) in the phase owes
// one. On this tree a bound program whose record is dead, or whose generation has
// moved under the handle, is a death/recycle seam - the emitter kept handing out
// a handle it had retired, or minted a successor at the slot and never described
// it - and the two causes are named apart because they point at different halves
// of the client.
if (!record.Live || record.Gen != handle.Gen) {
MGLOG_E_ONCE("A program record does not describe the binding it names: ShaderCso "
"{slot %u, gen %u} finds a %s record (generation %u); running the "
"frontend's own uniform block.",
static_cast<unsigned>(handle.Slot), static_cast<unsigned>(handle.Gen),
record.Live ? "live but differently-generationed" : "dead",
static_cast<unsigned>(record.Gen));
return nullptr;
}
return &record;
}
// The record whose GLOBAL CONSTANTS may be uploaded for this program, or null.
//
// Four things have to hold before the default uniform block is taken off a record
// rather than off the frontend program, and each of them is a way the two could
// legitimately disagree on a tree whose client half is mid-landing:
//
// * the program has a handle at all, and the record at it is live;
// * the record's own Desc names that same handle - the identity check, and the seam
// where a mis-keyed emission becomes visible instead of becoming a wrong upload;
// * the version is not the ~0u NEVER-UPLOADED SENTINEL, which the client must never
// emit and which is what the record starts at;
// * the block image is at least as long as the program says the block is, because
// the upload copies GetUBOSize() bytes and a short record would read past it.
//
// Anything else and the frontend's own MapUBO answers, exactly as it does today.
// P5e (pg): `uboSize` is what the block image is checked against, and it is a PARAMETER
// now rather than a `program->GetUBOSize()` read inside the body - on the handle arm the
// size is the record's own Desc.GlobalUboSize and there is no ProgramObject to ask.
// `program` is null on that arm and is used for nothing but the monolith identity
// resolution below.
const MG_Pipe::MGPipeShaderCsoRecord* ResolveGlobalConstantsRecordForHandle(MG_Pipe::MGPipeHandle handle,
Uint uboSize) {
const MG_Pipe::MGPipeShaderCsoRecord* const record = FindShaderCsoRecord(handle);
// P4a decline-site P4: S - FOLDED INTO P2/P3 AT THE VERIFICATION ROUND, which is
// what the review asked for and is why this one stays silent: both reasons
// FindShaderCsoRecord can answer null have just spoken for themselves, with the
// band and the generation named. A third line here would say less and fire on
// every one of them.
if (record == nullptr) return nullptr;
// P4a decline-site P5: S - THE PROGRAM SEAM, loud (MAJOR-3) and in the same
// words as the framebuffer and image seams, so one grep polices all three. No
// further flip. Unlike the image seam this one has no eager funnel to be wrong
// at: the only caller is the global-UBO upload at the draw validate point, where
// the record for THIS draw has been applied, so a descriptor that names a
// different handle than the slot it sits in is always a mis-keyed emission.
if (record->Desc.Cso != handle) {
MGLOG_E_ONCE("A program record does not describe the binding it names: the ShaderCso "
"record at {slot %u, gen %u} names {slot %u, gen %u} in its own "
"descriptor; running the frontend's own uniform block.",
static_cast<unsigned>(handle.Slot), static_cast<unsigned>(handle.Gen),
static_cast<unsigned>(record->Desc.Cso.Slot), static_cast<unsigned>(record->Desc.Cso.Gen));
return nullptr;
}
// P4a decline-site P6: M, then S - FLIPPED AT THE VERIFICATION ROUND, and the
// condition the review names is already the CALLER'S, so the test is not repeated
// here. The ~0u NEVER-UPLOADED SENTINEL is what the record starts at and is
// legitimate before the first set_global_constants for a program. But the only
// caller of this function is the global-UBO upload, which runs inside
// `currentProgram->GetUBOSize() > 0 && backendProgram.HasGlobalUboBlock()` - so
// reaching this line at all means bit 12 is on, this program HAS a default
// uniform block, and it is being drawn with no global constants ever emitted for
// it. That is the seam defect, and it says so.
if (record->GlobalConstantsVersion == ~Uint32{0}) {
MGLOG_E_ONCE("A program record does not describe the binding it names: ShaderCso "
"{slot %u, gen %u} still carries the never-uploaded sentinel at a draw "
"of a program with a %u-byte default uniform block; running the "
"frontend's own uniform block.",
static_cast<unsigned>(handle.Slot), static_cast<unsigned>(handle.Gen),
static_cast<unsigned>(uboSize));
return nullptr;
}
// P4a decline-site P7: S, and LOUD (ID-19) - this one is not a missing record,
// it is PROTOCOL CORRUPTION. The upload copies GetUBOSize() bytes out of
// GlobalConstants, so a shorter block image is a read past the end of the
// applier's own buffer; no correct emitter can produce it, and quietly handing
// the draw back to MapUBO() would file a memory-safety bug as a perf regression.
//
// THE VERDICT IS THE ONE THE CONTRACT ALREADY SPELLS, and deliberately not a new
// one: PipeApply.cpp's "the verdict of every trip wire in this file, in one
// place" says a poison or verify build STOPS and writes the Fatal{} marker G4
// greps the retrace logs for, while a shipped push build logs at error level and
// carries on from a defined state. So the gate lanes - which is where a seam
// defect has to be caught - never reach MapUBO() at all, and a shipped build does
// not abort a running game over a client-side bug. If the integrator wants the
// stop on every arm, it is one #if away and this comment is where to say so.
if (record->GlobalConstants.size() < static_cast<SizeT>(uboSize)) {
#if MOBILEGL_PIPE_POISON || MOBILEGL_PIPE_VERIFY
MGLOG_F("MGPipe: Fatal{ProtocolCorruption} "
"A program record does not describe the binding it names: the ShaderCso "
"record {slot %u, gen %u} carries a %llu-byte global-constant block for a "
"%llu-byte default uniform block.",
static_cast<unsigned>(handle.Slot), static_cast<unsigned>(handle.Gen),
static_cast<unsigned long long>(record->GlobalConstants.size()),
static_cast<unsigned long long>(uboSize));
std::abort();
#else
MGLOG_E_ONCE("A program record does not describe the binding it names: the ShaderCso "
"record {slot %u, gen %u} carries a %llu-byte global-constant block for a "
"%llu-byte default uniform block; running the frontend's own uniform block.",
static_cast<unsigned>(handle.Slot), static_cast<unsigned>(handle.Gen),
static_cast<unsigned long long>(record->GlobalConstants.size()),
static_cast<unsigned long long>(uboSize));
#endif
return nullptr;
}
return record;
}
// Read from the frontend rather than from the backend framebuffer sync, which
// only runs later in PrepareForDraw: a program compiled against a stale count
// would not be relinked until the draw after the one that needed it.
//
// P5e (pg): ITS OWN FUNCTION AND ITS OWN POLICY, because the three callers want three
// different answers when the record arm has nothing to say:
//
// FrontendFallback - the monolith arm, unchanged: read the draw-FBO binding slot.
// RefuseFallback - the handle arm AT A DRAW. With a live wire that binding slot is a
// BARRIER_PULLED row the client may already have moved, so a count
// read from it would compile the program against a framebuffer that
// is not the one being drawn to - silently, and only sometimes.
// The record is the answer or there is no answer (rule F).
// RecordOnly - the handle arm AT A DISPATCH. A compute program has no fragment
// stage and a dispatch has no draw framebuffer, so this count is not
// an input to what is being built; PrepareForCompute also does not
// run SyncCurrentFBO, so the framebuffer records are legitimately
// untrusted here and a refusal would abort a correct program. The
// previous resolution stands, which is what the monolith arm
// effectively does too when nothing rebound between the two calls.
enum class BroadcastCountPolicy {
FrontendFallback,
RefuseFallback,
RecordOnly
};
static void ResolveFragColorBroadcastCount(BroadcastCountPolicy policy) {
Bool broadcastCountResolved = false;
// The trust latch is fresh here and costs nothing: PrepareForDraw runs
// SyncCurrentFBO immediately before this, so the records have just been
// checked against the two bindings.
if (FramebufferImpl::g_fboRecordsTrusted) {
const MG_Pipe::MGPFramebufferState* const recordPtr = BoundFramebufferRecord(FramebufferTarget::Draw);
// P4a decline-site F7: FLIPPED AT THE VERIFICATION ROUND from a silent
// decline to an assertion, because it is unreachable and a silent decline
// here is indistinguishable from a legitimate one. g_fboRecordsTrusted
// implies a record with a non-null handle on BOTH targets:
// SyncCurrentFBOByRecord sets the latch only after F2 has rejected a null
// pointer or handle on either, and it runs immediately before this in
// PrepareForDraw. Kept as a checked decline rather than deleted so a
// release build cannot dereference null if that ordering ever changes.
MOBILEGL_ASSERT(recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo));
if (recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo)) {
const MG_Pipe::MGPFramebufferState& record = *recordPtr;
if (!g_broadcastMemoHandleValid || g_broadcastMemoContentHash != record.ContentHash) {
Uint enabledDrawBuffers = 0;
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
// -1 is FramebufferAttachmentType::None on the wire; every
// other value is an attachment index.
if (record.DrawBuffers[i] >= 0) {
enabledDrawBuffers = i + 1;
}
}
g_broadcastMemoContentHash = record.ContentHash;
g_broadcastMemoHandleCount = std::max<Uint>(enabledDrawBuffers, 1);
g_broadcastMemoHandleValid = true;
}
g_fragColorBroadcastCount = g_broadcastMemoHandleCount;
broadcastCountResolved = true;
}
}
if (policy == BroadcastCountPolicy::RecordOnly) return;
if (policy == BroadcastCountPolicy::RefuseFallback && !broadcastCountResolved) {
// The record arm declined under a transport. g_fboRecordsTrusted is set by
// SyncCurrentFBOByRecord, which PrepareForDraw runs immediately before this,
// so reaching here means the framebuffer family's own records are missing or
// untrusted - and the frontend fallback below is exactly the read this phase
// exists to retire.
MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, "
"\"GetFramebufferBindingSlot@SyncCurrentProgram\"} - the draw "
"framebuffer's record did not answer the fragColor broadcast count on "
"the handle arm, and the frontend binding slot is a row the client owns");
std::abort();
}
if (!broadcastCountResolved) {
const auto& drawSlot = GetFramebufferBindingSlotChecked(FramebufferTarget::Draw);
const auto& drawFBO = drawSlot.GetBoundObject();
const Uint16 slotVersion = drawSlot.GetVersion();
const Uint16 objectVersion = drawFBO ? drawFBO->GetObjectVersion() : 0;
if (!g_broadcastMemoValid || g_broadcastMemoFbo != drawFBO.get() ||
g_broadcastMemoSlotVersion != slotVersion || g_broadcastMemoObjectVersion != objectVersion) {
Uint enabledDrawBuffers = 0;
if (drawFBO) {
const auto& drawBuffers = drawFBO->GetDrawBuffers();
for (Uint i = 0; i < MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
if (drawBuffers[i] != FramebufferAttachmentType::None) {
enabledDrawBuffers = i + 1;
}
}
}
g_broadcastMemoFbo = drawFBO.get();
g_broadcastMemoSlotVersion = slotVersion;
g_broadcastMemoObjectVersion = objectVersion;
g_broadcastMemoCount = std::max<Uint>(enabledDrawBuffers, 1);
g_broadcastMemoValid = true;
}
g_fragColorBroadcastCount = g_broadcastMemoCount;
}
}
// P5e (pg), CONTRACT-P5E.md §5.5: THE SAME SYNC, FROM THE HANDLE THE RECORD CARRIED.
// Two overloads and not an `#if` inside one body (ruling 1 / ID-81): the frontend one
// above is visibly the monolith-glue half, this one names no frontend object at all.
//
// WHAT THE NINE-CLAUSE CONDITION BECOMES, and the clause COUNT does not shrink - its
// inputs move (D-H5, §5.5):
// GetLinkVersion() -> record.Serial vs GetSyncedShaderCsoSerial()
// GetImageUnitVersion() -> record.BindingsSerial vs GetSyncedBindingsSerial()
// ComputeShaderStorage...() -> record.Signature
// GetLinkStatus/SpirvStatus -> Desc.LinkStatus / Desc.SpirvStatus, above
// and the clamp masks, the broadcast count, ImageUnitFormatsStillMatch and the three
// patch clauses are unchanged - they are backend globals, fb's row and VALUE rows of the
// residual block, none of which this family owns.
void SyncCurrentProgramByHandle(MG_Pipe::MGPipeHandle cso, Bool forDraw, const IntegerAttributePlan* plan) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
g_currentDrawFrontendProgram = nullptr;
g_currentDrawBackendProgram = nullptr;
g_currentDrawProgramHandle = MG_Pipe::kMGPipeNullHandle;
const MG_Pipe::MGPipeShaderCsoRecord* const record = FindShaderCsoRecord(cso);
// THE SAME THREE-PART TEST THE FRONTEND ARM MAKES, from the descriptor. LinkStatus
// is a field rather than an implication (ID-88): create_shader_state is re-issued at
// every link that moves the link version and a FAILED relink of a bound program
// moves it too, so "a record exists" and "the program linked" are different
// statements. A program the frontend reports unlinked draws nothing, which is
// exactly what the monolith arm does with GetLinkStatus() == false.
if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) {
g_GLESFuncs.glUseProgram(0);
g_lastUsedBackendProgramId = 0;
return;
}
// REFUSED at a draw, record-only at a dispatch: see the policy's own note.
ResolveFragColorBroadcastCount(forDraw ? BroadcastCountPolicy::RefuseFallback
: BroadcastCountPolicy::RecordOnly);
BackendProgramObjectImpl* const twin = ResolveProgramTwin(cso);
if (twin == nullptr) {
// ResolveProgramTwin has already named the handle. A draw with no twin binds
// nothing, which is the visible no-op Use() makes of an unusable program rather
// than a draw with somebody else's shader.
g_GLESFuncs.glUseProgram(0);
g_lastUsedBackendProgramId = 0;
return;
}
// Sticky for the twin's lifetime: a draw that needs the seeds rebuilds the program once.
if (plan != nullptr && plan->NeedsSeeds()) twin->NarrowIntegerSeeds().wanted = true;
if (!twin->GetBackendProgramId() || twin->GetSyncedShaderCsoSerial() != record->Serial ||
twin->NarrowIntegerSeeds().Stale() || twin->GetSyncedBindingsSerial() != record->BindingsSerial ||
twin->GetSnormFallbackClampOutputMask() != g_snormFallbackClampOutputMask ||
twin->GetUnormFallbackClampOutputMask() != g_unormFallbackClampOutputMask ||
twin->GetFragColorBroadcastCount() != g_fragColorBroadcastCount ||
twin->GetShaderStorageBlockBindingSignature() != record->Signature ||
!twin->ImageUnitFormatsStillMatch() ||
(twin->GetPassthroughTessControlPatchVertices() >= 0 &&
(twin->GetPassthroughTessControlPatchVertices() !=
static_cast<Int>(MG_Pipe::gPipeInputs->GetPatchVertices()) ||
!BitwiseEqual(twin->GetPassthroughTessControlOuterLevel(),
MG_Pipe::gPipeInputs->GetPatchDefaultOuterLevel()) ||
!BitwiseEqual(twin->GetPassthroughTessControlInnerLevel(),
MG_Pipe::gPipeInputs->GetPatchDefaultInnerLevel())))) {
twin->SyncToBackendByHandle(cso);
}
g_currentDrawProgramHandle = cso;
g_currentDrawBackendProgram = twin;
}
} // namespace PrgramImpl
} // namespace MobileGL::MG_Backend::DirectGLES