mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
- 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
1494 lines
98 KiB
C++
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*>(¶meters);
|
|
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 = ¶meters.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
|