mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
1845 lines
123 KiB
C++
1845 lines
123 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectGLES/DirectGLESBind.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 {
|
|
namespace BufferImpl {
|
|
// P5e (vi), CONTRACT-P5E §5.1: THE SAME MEMO, THE SAME THREE-VALUE KEY, AND A WALK THAT
|
|
// READS NO FRONTEND AT ALL. The arm above keeps the frontend attribute walk because the
|
|
// push-monolith build has to keep its bytes (§5.8); this one is what a server with no
|
|
// client objects on its side runs, and the difference between them is exactly three
|
|
// substitutions:
|
|
//
|
|
// the walk st.VertexBuffers[Start .. Start+Count) instead of
|
|
// GetAllAttributes(): the SAME set, because the emitter resolves the
|
|
// attributes on the client and publishes one entry per attribute slot
|
|
// (VertexInputEmit.h's EmitVertexBuffers), with Res == null for a
|
|
// client-memory array and for a disabled slot - which is why the null
|
|
// skip below is not a hole but the record's own "nothing to ensure".
|
|
// the dedupe a {slot, gen} compare instead of a raw address compare. An address
|
|
// a successor object can reproduce is the identity hazard this whole
|
|
// phase is about; a handle cannot be reproduced (Gen moves on reuse).
|
|
// the ensure EnsureBufferResourceForHandle(nullptr, Res). The frontend argument
|
|
// is only ever used for BufferObject::SyncPersistentMappedRange, which
|
|
// that function already skips under a transport (D-N), so passing
|
|
// nullptr removes a read rather than dropping work - and (nullptr, h)
|
|
// is already the live shape of the indirect-buffer ensure two hundred
|
|
// lines below.
|
|
//
|
|
// The IBO half lives in SyncNeccessaryBuffers beside the legacy one, for the reason it
|
|
// always did: the index slot is not part of the configuration (D5).
|
|
void SyncVaoAttributeBuffersByRecord(VertexArrayImpl::BackendVertexArrayObject::ResolvedDrawBuffers* memo,
|
|
Uint64 bufferEpoch) {
|
|
const auto& st = MG_Pipe::MGPipeApplier();
|
|
const MG_Pipe::MGPipeHandle elements = st.BoundVertexElements;
|
|
Uint64 elementsSerial = 0;
|
|
Bool haveElementsRecord = false;
|
|
if (!MG_Pipe::MGPipeHandleIsNull(elements) && elements.Slot < st.VertexElementsCsos.size()) {
|
|
const auto& record = st.VertexElementsCsos[elements.Slot];
|
|
if (record.Live && record.Gen == elements.Gen) {
|
|
elementsSerial = record.ContentSerial;
|
|
haveElementsRecord = true;
|
|
}
|
|
}
|
|
const Uint64 buffersSerial = st.VertexBuffersSerial;
|
|
|
|
// NO LIVE ELEMENTS RECORD IS A MISS, NEVER A HIT - the arm above states the whole
|
|
// argument (MGPipeApplierReset empties the table at every context change and the
|
|
// key would then never move again).
|
|
const Bool memoKeyIsMeaningful = haveElementsRecord;
|
|
|
|
if (memo && memo->valid && memoKeyIsMeaningful && memo->elementsHandle == elements &&
|
|
memo->elementsSerial == elementsSerial && memo->buffersSerial == buffersSerial) {
|
|
if (memo->vboCleanEpoch != bufferEpoch) {
|
|
Bool allClean = true;
|
|
for (Uint i = 0; i < memo->count; ++i) {
|
|
auto& entry = memo->entries[i];
|
|
// nullptr, not entry.frontend: the frontend question inside
|
|
// IsBufferDrawCleanByHandle is monolith-only already
|
|
// (askTheObjectWhetherItIsMapped), and a raw client address held across
|
|
// records is precisely what §4.4 stops the apply thread dereferencing.
|
|
if (IsBufferDrawCleanByHandle(entry.handle, entry.resource, nullptr)) continue;
|
|
allClean = false;
|
|
// Re-ensured BY HANDLE. The legacy repair went back through the memoed
|
|
// attribute index to re-read attrib.Buffer; here the entry already
|
|
// names the resource the record named, so the repair needs nothing the
|
|
// walk did not already have.
|
|
entry.resource = EnsureBufferResourceForHandle(nullptr, entry.handle);
|
|
}
|
|
memo->vboCleanEpoch = allClean ? bufferEpoch : 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Full walk, once per distinct buffer, rebuilding the memo as it goes. WHICH
|
|
// buffers is ResolveDrawVertexBuffersFromRecord's answer and only its answer - see
|
|
// Managers.h: that separation is what lets a unit case drive the source decision
|
|
// this package changed without an ES context, and what makes a revert of it visible
|
|
// rather than duplicated.
|
|
DrawVertexBufferRequest requests[MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS];
|
|
const Uint syncedBufferCount = ResolveDrawVertexBuffersFromRecord(
|
|
st, requests, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS);
|
|
for (Uint i = 0; i < syncedBufferCount; ++i) {
|
|
auto* resource = EnsureBufferResourceForHandle(nullptr, requests[i].Res);
|
|
if (memo) {
|
|
auto& entry = memo->entries[i];
|
|
// Explicitly cleared rather than left as whatever the previous build of
|
|
// this memo stored: nothing on this arm may dereference it, and a stale
|
|
// address that merely happens never to be read is the kind of thing a
|
|
// later reader takes for a live one.
|
|
entry.frontend = nullptr;
|
|
entry.attribIndex = static_cast<Uint8>(requests[i].BindingIndex);
|
|
entry.resource = resource;
|
|
entry.handle = requests[i].Res;
|
|
}
|
|
}
|
|
if (memo) {
|
|
memo->count = syncedBufferCount;
|
|
memo->elementsHandle = elements;
|
|
memo->elementsSerial = elementsSerial;
|
|
memo->buffersSerial = buffersSerial;
|
|
memo->valid = memoKeyIsMeaningful;
|
|
// Rebuilt via the ensure, not probed clean: the next probe pass stamps it.
|
|
memo->vboCleanEpoch = 0;
|
|
}
|
|
}
|
|
|
|
// `vaoConfigVersion` is the caller's early read of currentVAOObject->GetConfigVersion():
|
|
// the VAO's config fields live on a cache line the draw path touches nowhere else, and
|
|
// cycling section VAOs makes that a guaranteed miss - reading it at the top of
|
|
// PrepareForDraw overlaps the miss with the program/texture-key work instead of
|
|
// stalling the memo check below. Nothing between the read and here can move it
|
|
// (only frontend GL entry points mutate VAO config, none run inside a preparation).
|
|
void SyncNeccessaryBuffers(const SharedPtr<MG_State::GLState::VertexArrayObject>& currentVAOObject,
|
|
VertexArrayImpl::BackendVertexArrayObject* vaoTwin, Uint32 vaoConfigVersion,
|
|
Bool includeIBO = false, Bool includeIndirectBuffer = false) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
ProcessDeferredBufferReleases();
|
|
#if MOBILEGL_BUILD_DISAGGREGATED
|
|
ProcessDeferredT0Retires(); // P11 B2
|
|
#endif
|
|
|
|
// All buffers we need are:
|
|
// 1.VBO 2.IBO (if needed) 3.UBO 4.IndirectBuffer (if needed)
|
|
// PBO is not needed since it should be handled in frontend
|
|
|
|
// VBO. The distinct-buffer set is memoed on the VAO's backend twin: cycling
|
|
// hundreds of section VAOs re-walked 32 cold VertexAttribute slots per draw
|
|
// only to rediscover the same one or two static buffers. While the config
|
|
// version holds (every attach/enable/disable bumps it, and it pins each
|
|
// memoed frontend pointer via the attribute SharedPtrs), the walk reduces to
|
|
// an IsBufferDrawClean probe per distinct buffer; only dirty entries take
|
|
// EnsureBufferResource, re-fetched through their attribute index.
|
|
// Pre-pass epoch read (acquire), stamped into the memo only after a pass in
|
|
// which EVERY probe came up clean: while the stamp still equals the current
|
|
// epoch, no path that can dirty any buffer has run (see the mutation-site
|
|
// enumeration at CurrentBufferMutationEpoch's declaration in Managers.h),
|
|
// so the probes themselves are skipped. A mid-pass bump lands after this
|
|
// read, makes the stamp stale, and re-runs the probes next draw.
|
|
const Uint64 bufferEpoch = CurrentBufferMutationEpoch();
|
|
auto* memo = vaoTwin ? &vaoTwin->GetResolvedDrawBuffersMemo() : nullptr;
|
|
const Uint32 configVersion = vaoConfigVersion;
|
|
SyncVaoAttributeBuffersByRecord(memo, bufferEpoch);
|
|
|
|
// IBO, memoed by bound-object identity (its slot version is not covered by
|
|
// the config version). A stale identity hit is impossible in effect: the
|
|
// clean probe re-validates the resource against the LIVE bound object.
|
|
if (includeIBO) {
|
|
// P5e (vi), CONTRACT-P5E §5.1: the index buffer is st.IndexBuffer.Res and the
|
|
// "has it moved" question is st.IndexBufferSerial. The identity compare stays -
|
|
// it always did, because the index slot is outside the configuration version
|
|
// (D5) - but it now compares a handle the RECORD named instead of a frontend
|
|
// address the slot was probed for, and the serial joins it because this arm has
|
|
// no live slot to re-read: see ResolvedDrawBuffers::iboSerial in Managers.h.
|
|
//
|
|
// A null Res is "no element buffer bound" and is a no-op here exactly as the
|
|
// absent `possibleIBO` is below: SyncToBackendFromApplier is what binds
|
|
// GL_ELEMENT_ARRAY_BUFFER back to 0 for such a draw, and it keys on the same
|
|
// serial.
|
|
const DrawIndexBufferRequest request = ResolveDrawIndexBufferFromRecord(MG_Pipe::MGPipeApplier());
|
|
const MG_Pipe::MGPipeHandle iboHandle = request.Res;
|
|
const Uint64 iboSerial = request.Serial;
|
|
if (!MG_Pipe::MGPipeHandleIsNull(iboHandle)) {
|
|
const Bool identityHolds =
|
|
memo != nullptr && memo->iboHandle == iboHandle && memo->iboSerial == iboSerial;
|
|
if (identityHolds && memo->iboCleanEpoch == bufferEpoch) {
|
|
// probed fully clean at this epoch; nothing can have dirtied it
|
|
} else if (identityHolds && IsBufferDrawCleanByHandle(iboHandle, memo->iboResource, nullptr)) {
|
|
memo->iboCleanEpoch = bufferEpoch;
|
|
} else {
|
|
auto* resource = EnsureBufferResourceForHandle(nullptr, iboHandle);
|
|
if (memo) {
|
|
memo->iboHandle = iboHandle;
|
|
memo->iboSerial = iboSerial;
|
|
memo->iboFrontend = nullptr; // see Entry::frontend's note
|
|
memo->iboResource = resource;
|
|
// Repaired, not probed clean: stamp on the next clean probe.
|
|
memo->iboCleanEpoch = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Indirect Buffer Object - must also be bound to GL_DRAW_INDIRECT_BUFFER on the ES
|
|
// context since indirect draws now execute natively on the GPU.
|
|
if (includeIndirectBuffer) {
|
|
// P5c (hd, CONTRACT-P5C §3.5): under an active transport the buffer is the verb
|
|
// record's handle; the frontend binding slot and the client allocator are
|
|
// never read (T2). The twin's ensure is what SyncBoundBuffer's did.
|
|
const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
|
|
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
|
|
auto* resource = EnsureBufferResourceForHandle(nullptr, handle);
|
|
if (resource && resource->id != 0) {
|
|
BindBufferId(GL_DRAW_INDIRECT_BUFFER, resource->id);
|
|
}
|
|
}
|
|
}
|
|
|
|
// UBO binding points are (re)established per draw by BindCurrentProgramWithResources at
|
|
// their compacted link-time points: CacheResourceLocations glUniformBlockBinding's the
|
|
// transpiled ESSL blocks to points 0,1,2,... (layout(binding=N) is stripped from the
|
|
// ESSL), so those compacted points are the only ones the shader reads. A frontend-indexed
|
|
// sync here would bind points the shader never reads and is unconditionally overwritten by
|
|
// the program rebind that always follows in PrepareForDraw - i.e. redundant for draws - so
|
|
// it is intentionally omitted (see SyncComputeBuffers for the compute path, which needs it).
|
|
// SSBOs are different: their block bindings are baked into the ESSL at compile time and
|
|
// BindCurrentProgramWithResources binds no SSBO points, so this is their sole draw-path
|
|
// binder (e.g. Flywheel's indirect vertex shaders pull instance data from storage buffers).
|
|
// P5e (sb, §5.6): the record arm, selected by transport + bit 13. Same `if (…) else`
|
|
// shape the indirect buffer above already uses.
|
|
SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassShaderStorage, GL_SHADER_STORAGE_BUFFER);
|
|
}
|
|
} // namespace BufferImpl
|
|
|
|
namespace XfbImpl {
|
|
namespace {
|
|
// Binds a scratch buffer, sized for `capacityVertices` gap-free records, to
|
|
// capture point 0 in place of the application's buffers. Returns false when the
|
|
// scratch storage cannot be provided, in which case the caller falls back to the
|
|
// direct binding (which produces a wrong layout, but is what happened before).
|
|
Bool BindScatterCaptureBuffer(SizeT packedStride, SizeT capacityVertices) {
|
|
auto& g_scatterBufferId = CurrentXfb().serverScatterBuffer;
|
|
auto& g_scatterBufferSize = CurrentXfb().serverScatterSize;
|
|
if (packedStride == 0 || capacityVertices == 0) return false;
|
|
if (g_GLESFuncs.glGenBuffers == nullptr || g_GLESFuncs.glBufferData == nullptr) return false;
|
|
const SizeT required = packedStride * capacityVertices;
|
|
if (g_scatterBufferId == 0) {
|
|
g_GLESFuncs.glGenBuffers(1, &g_scatterBufferId);
|
|
if (g_scatterBufferId == 0) return false;
|
|
g_scatterBufferSize = 0;
|
|
}
|
|
if (g_scatterBufferSize < required) {
|
|
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, g_scatterBufferId);
|
|
g_GLESFuncs.glBufferData(BufferImpl::TempBufferTarget, static_cast<GLsizeiptr>(required), nullptr,
|
|
GL_DYNAMIC_COPY);
|
|
g_scatterBufferSize = required;
|
|
}
|
|
// Point 0 carries every captured varying: the gl_NextBuffer / gl_SkipComponents
|
|
// entries are consumed at link time and never reach the driver, so the ES
|
|
// program is declared INTERLEAVED over a single buffer and point 0 is the only
|
|
// point it can write (GL 4.6 core 13.2.2).
|
|
//
|
|
// The other points are therefore left exactly as they are. Clearing them - which
|
|
// this used to do, across the application's whole touched high-water mark - is
|
|
// both unnecessary (the ES program cannot write an unused point) and the precise
|
|
// trigger for the Mali G1-Ultra capture loss: see
|
|
// SyncTransformFeedbackBindingPoints for the mechanism and the device evidence.
|
|
// KHR-GL46.transform_feedback.capture_special_interleaved_test is the case that
|
|
// reaches this path.
|
|
BufferImpl::BindBufferRangeCached(GL_TRANSFORM_FEEDBACK_BUFFER, 0, g_scatterBufferId, 0,
|
|
static_cast<GLsizeiptr>(required));
|
|
return true;
|
|
}
|
|
} // namespace
|
|
|
|
// Tail of PrepareForDraw: the program is bound and every buffer the draw needs
|
|
// is up to date, so the capture buffers can be bound and the span opened.
|
|
void StartPendingTransformFeedback() {
|
|
auto& xfb = CurrentXfb();
|
|
// A span that was paused before its first draw must not open here: the draw is
|
|
// not captured, and opening the span would also subject it to the capture
|
|
// primitive-mode rule the paused draw is exempt from.
|
|
if (!xfb.pending || xfb.paused) return;
|
|
XfbProgramSource program;
|
|
const MG_Pipe::MGPStreamOutputBegin* span = nullptr;
|
|
const auto& state = MG_Pipe::MGPipeApplier();
|
|
const auto found = state.StreamOutputSpans.find(state.BoundStreamOutputLifetimeId);
|
|
if (found != state.StreamOutputSpans.end()) {
|
|
span = &found->second;
|
|
const auto* record = PipeShaderCsoRecordForHandle(span->CaptureProgram);
|
|
if (record) program.Archive = record->Archive;
|
|
}
|
|
if (!program) {
|
|
// The pending flag is deliberately NOT consumed here. It used to be cleared
|
|
// before this check, so a single draw that could not see the capture program
|
|
// retired the span permanently: every later draw of the same span found
|
|
// pending==false, the driver Begin never happened, and End found started==false
|
|
// and skipped the readback - a whole capture lost with no GL error anywhere.
|
|
// The frontend only reaches a draw with an active span after glBeginTransformFeedback
|
|
// stored a program, so this is a "cannot happen" that must stay recoverable.
|
|
MGLOG_E_ONCE("StartPendingTransformFeedback: an active capture span has no capture program; the "
|
|
"driver span stays closed and this draw is not captured");
|
|
return;
|
|
}
|
|
xfb.pending = false;
|
|
|
|
// Snapshot what the driver is about to capture into. GL forbids rebinding the
|
|
// capture buffers while the span is open, so this stays valid until End, and
|
|
// recording it here keeps End independent of the frontend capture state.
|
|
const SizeT bufferCount = program->GetTransformFeedbackBufferCount();
|
|
for (SizeT i = 0; i < bufferCount; ++i) {
|
|
if (!span || i >= 4) continue;
|
|
const auto& range = span->Targets[i];
|
|
if (MG_Pipe::MGPipeHandleIsNull(range.Res) || range.Size == 0) continue;
|
|
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, range.Res);
|
|
if (!resource || resource->id == 0) continue;
|
|
xfb.targets.push_back({nullptr, resource->id, static_cast<SizeT>(range.Offset),
|
|
static_cast<SizeT>(range.Offset + range.Size), i, range.Res});
|
|
BufferImpl::BindBufferRangeCached(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLuint>(i), resource->id,
|
|
static_cast<GLintptr>(range.Offset),
|
|
static_cast<GLsizeiptr>(range.Size));
|
|
continue;
|
|
}
|
|
|
|
// A layout with holes or several interleaved buffers is not expressible on ES:
|
|
// capture gap-free into scratch storage and place the records at End instead.
|
|
xfb.scattered = false;
|
|
xfb.scatterProgram.reset();
|
|
xfb.scatterArchive.reset();
|
|
xfb.scatterCapacityVertices = 0;
|
|
if (program->NeedsScatteredTransformFeedbackCapture()) {
|
|
SizeT capacityVertices = ~SizeT(0);
|
|
for (const auto& target : xfb.targets) {
|
|
// By BUFFER index. Reading the stride at the target's POSITION made a
|
|
// capture list beginning with gl_NextBuffer - buffer 0 has stride 0 and
|
|
// nothing bound, so target 0 describes buffer 1 - read stride 0, skip every
|
|
// target, and leave the capacity at zero.
|
|
const SizeT stride = program->GetTransformFeedbackStride(static_cast<Uint32>(target.bufferIndex));
|
|
if (stride == 0) continue;
|
|
capacityVertices = std::min<SizeT>(capacityVertices, (target.end - target.start) / stride);
|
|
}
|
|
if (capacityVertices == ~SizeT(0)) capacityVertices = 0;
|
|
if (BindScatterCaptureBuffer(program->GetTransformFeedbackPackedStride(), capacityVertices)) {
|
|
xfb.scattered = true;
|
|
xfb.scatterProgram = program.Frontend;
|
|
xfb.scatterArchive = program.Archive;
|
|
xfb.scatterCapacityVertices = capacityVertices;
|
|
} else {
|
|
// NO SPAN RATHER THAN A SPAN THAT WRITES SOMEWHERE ELSE. The ES program for a
|
|
// scattered capture is a single-buffer INTERLEAVED one (the gl_NextBuffer /
|
|
// gl_SkipComponents entries are consumed at link time and never reach the
|
|
// driver), so it writes capture point 0 and nothing else. Point 0 here is
|
|
// either unbound or - the dangerous case - still holds whatever an earlier
|
|
// capture in this process bound there, because the frontend's own
|
|
// glBindBufferBase is state-only and nothing else in the backend touches the
|
|
// indexed points. Opening the span would then have the driver capture over an
|
|
// application buffer that has nothing to do with this draw, and the frontend
|
|
// shadow would never learn of it.
|
|
//
|
|
// Leaving the span closed reproduces exactly what the old high-water clear
|
|
// loop achieved by binding 0 here and letting the driver refuse the Begin -
|
|
// the capture records nothing - without issuing a capture-point bind the
|
|
// application did not ask for, which is the thing that loses captures whole
|
|
// on Mali (see SyncTransformFeedbackBindingPoints).
|
|
MGLOG_E_ONCE("StartPendingTransformFeedback: no scratch storage for a scattered capture "
|
|
"(packed stride %zu, capacity %zu vertices); leaving the driver span CLOSED so the "
|
|
"capture cannot land in a stale binding. Nothing will be captured.",
|
|
program->GetTransformFeedbackPackedStride(), capacityVertices);
|
|
xfb.targets.clear();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// A capture program with buffers bound must have produced at least one target;
|
|
// an empty list means End has nothing to mirror back and the application will
|
|
// read its buffer's pre-draw bytes however well the GPU captured.
|
|
if (xfb.targets.empty()) {
|
|
MGLOG_E_ONCE("StartPendingTransformFeedback: opening a capture span with NO capture targets "
|
|
"(program declares %zu capture buffer(s), none of them resolved to a bound backend "
|
|
"buffer with a non-empty range); nothing will be read back",
|
|
bufferCount);
|
|
}
|
|
|
|
g_GLESFuncs.glBeginTransformFeedback(xfb.primitiveMode);
|
|
// Unchecked before. Every ES error condition here (already active, a current
|
|
// program with no capture set, a capture point the program uses with no buffer)
|
|
// ends the same way: the driver records nothing, GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN
|
|
// reads 0 and the application sees no error at all - MobileGL's own error state is
|
|
// separate from the driver's, so a driver rejection here is invisible to it.
|
|
if (const GLenum beginError = TakeXfbDriverError(); beginError != GL_NO_ERROR) {
|
|
// The mode is printed as a number as well as a name: GL_POINTS is 0, which the
|
|
// enum converter spells "GL_FALSE", and a reader chasing a lost capture should
|
|
// not have to know that.
|
|
MGLOG_E_ONCE("StartPendingTransformFeedback: the ES driver REJECTED "
|
|
"glBeginTransformFeedback(%s / 0x%04x) with %s - nothing will be captured. Backend "
|
|
"program %u, %zu capture buffer(s), %zu target(s), mode=%s.",
|
|
MG_Util::ConvertGLEnumToString(xfb.primitiveMode).c_str(),
|
|
static_cast<unsigned>(xfb.primitiveMode),
|
|
MG_Util::ConvertGLEnumToString(beginError).c_str(), PrgramImpl::g_lastUsedBackendProgramId,
|
|
bufferCount, xfb.targets.size(),
|
|
MG_Util::ConvertGLEnumToString(program->GetTransformFeedbackBufferMode()).c_str());
|
|
}
|
|
xfb.started = true;
|
|
}
|
|
} // namespace XfbImpl
|
|
|
|
namespace VertexArrayImpl {
|
|
// P5e (vi): the record arm's entry, and it takes NO frontend object - CONTRACT-P5E §4.1
|
|
// ("the object parameter is deleted from the transport overload, not defaulted to
|
|
// null"). A null twin here is the null BoundVertexElements, i.e. "no VAO bound", which
|
|
// PrepareForDraw turns into BindBackendVAOId(0) exactly as a null frontend VAO did;
|
|
// logging about it would fire on every draw of a session that legitimately has no
|
|
// vertex-elements CSO yet.
|
|
void SyncCurrentVAOFromRecords(BackendVertexArrayObject* vaoTwin, const IntegerAttributePlan& plan) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
if (!vaoTwin) return;
|
|
vaoTwin->SyncToBackendFromApplier(plan);
|
|
}
|
|
|
|
// GL: a shader input whose generic attribute array is DISABLED reads that attribute's *current
|
|
// value* (context state set by glVertexAttrib*, default (0,0,0,1)) rather than any buffer.
|
|
// MobileGL stores those values in MG_State only, so without this step the ES driver would feed
|
|
// the shader its own current values, which MobileGL never writes -- i.e. always (0,0,0,1).
|
|
// SyncToBackend has already issued glDisableVertexAttribArray for these locations, so the ES
|
|
// current value is what the shader will actually read.
|
|
void SyncCurrentVertexAttributeValues(BackendVertexArrayObject* vaoTwin,
|
|
const SharedPtr<MG_State::GLState::ProgramObject>& program) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
if (!program) return;
|
|
if (!vaoTwin) return;
|
|
|
|
// P5e (vi), CONTRACT-P5E §5.1: the bound-VAO row is retired here. On the record arm
|
|
// the twin IS the bound vertex-elements CSO (the caller resolved it from
|
|
// st.BoundVertexElements), so "is a VAO bound" is already answered by vaoTwin being
|
|
// non-null and GetBoundVertexArray() has nothing left to add.
|
|
//
|
|
// WHAT IS NOT RETIRED HERE, and it is S4/pg's not vi's: the two program reads below
|
|
// (GetActiveAttributeLocationMask, GetAttribType). They are frontend object rows of
|
|
// the PROGRAM family and stay until pg carries the attribute mask and types in the
|
|
// ShaderCso record. Until then this function is still a frontend reader on the
|
|
// apply thread - a BARRIERED one, which is legal (§4.4 / ruling 4) and is why vi
|
|
// does its half now rather than waiting: the VAO half is what this package owns and
|
|
// leaving it would keep GetBoundVertexArray alive for a reason that is not the
|
|
// program's.
|
|
const MG_Pipe::MGPipeVertexElementsRecord* elementsRecord = nullptr;
|
|
MG_Pipe::MGPipeHandle elementsHandle = MG_Pipe::kMGPipeNullHandle;
|
|
Uint64 elementsSerial = 0;
|
|
const auto& st = MG_Pipe::MGPipeApplier();
|
|
elementsHandle = st.BoundVertexElements;
|
|
if (!MG_Pipe::MGPipeHandleIsNull(elementsHandle) && elementsHandle.Slot < st.VertexElementsCsos.size()) {
|
|
const auto& record = st.VertexElementsCsos[elementsHandle.Slot];
|
|
if (record.Live && record.Gen == elementsHandle.Gen) {
|
|
elementsRecord = &record;
|
|
elementsSerial = record.ContentSerial;
|
|
}
|
|
}
|
|
// No record is "the configuration was never described": there is nothing to
|
|
// decide which locations lack an array from, and guessing would either
|
|
// re-issue every current value every draw or silently skip the ones that need
|
|
// it. The next create_vertex_elements re-opens the memo by serial.
|
|
if (elementsRecord == nullptr) return;
|
|
|
|
const SharedPtr<MG_State::GLState::VertexArrayObject> noVao;
|
|
const auto& vao = noVao;
|
|
|
|
const Uint32 activeAttribMask = program->GetActiveAttributeLocationMask();
|
|
if (activeAttribMask == 0) return;
|
|
|
|
// Which of the program's attributes lack an enabled array cannot change without
|
|
// the VAO's config version moving (Enable/DisableAttribute bump it) or the
|
|
// program's active mask changing, so the per-draw Enabled probes reduce to two
|
|
// compares. The memo lives on the twin (1:1 with the VAO, no identity key
|
|
// needed): a function-static single entry missed on every draw once the app
|
|
// cycled section VAOs, re-reading the cold attribute slots each time; here a
|
|
// cycle re-hits every VAO's own entry. The rebuild visits only ACTIVE locations.
|
|
auto& memo = vaoTwin->GetPendingAttribValueMaskMemo();
|
|
// The re-keyed memo: {elementsHandle, ContentSerial, activeMask}. Every
|
|
// Enable/DisableVertexAttribArray moves the frontend configuration version, the
|
|
// client re-emits create_vertex_elements on the same handle for it, and the
|
|
// applier ++s ContentSerial - so this key opens exactly when the old one did
|
|
// and never wraps (Managers.h states the strictly-stronger argument).
|
|
if (!memo.valid || !(memo.elementsHandle == elementsHandle) || memo.elementsSerial != elementsSerial ||
|
|
activeAttribMask != memo.activeMask) {
|
|
Uint32 pending = 0;
|
|
for (Uint32 remaining = activeAttribMask; remaining != 0; remaining &= remaining - 1) {
|
|
const Uint32 location = static_cast<Uint32>(std::countr_zero(remaining));
|
|
// rec->Attributes[] IS what was last pushed for this configuration; the
|
|
// record carries all 32 slots and zeroes the tail
|
|
// (MGPipeApplyCreateVertexElements), so a location past AttributeCount
|
|
// reads Enabled = 0, which is the same answer the frontend's cold
|
|
// attribute slot gave.
|
|
if (location >= MG_Pipe::kMGPipeMaxVertexAttribs || !elementsRecord->Attributes[location].Enabled) {
|
|
pending |= (1u << location);
|
|
}
|
|
}
|
|
memo.elementsHandle = elementsHandle;
|
|
memo.elementsSerial = elementsSerial;
|
|
memo.activeMask = activeAttribMask;
|
|
memo.pendingMask = pending;
|
|
memo.valid = true;
|
|
}
|
|
// Plus the enabled arrays the walk had to disable (Managers.h, GetDisabledEnabledAttribMask).
|
|
const Uint32 pendingMask = memo.pendingMask | (vaoTwin->GetDisabledEnabledAttribMask() & memo.activeMask);
|
|
if (pendingMask == 0) return;
|
|
|
|
for (Uint32 remaining = pendingMask; remaining != 0; remaining &= remaining - 1) {
|
|
const Uint32 location = static_cast<Uint32>(std::countr_zero(remaining));
|
|
|
|
const auto& currentValue = MG_Pipe::gPipeInputs->GetCurrentVertexAttribute(location);
|
|
const auto typeInfo = MG_State::GLState::ClassifyVertexAttribType(program->GetAttribType(location));
|
|
switch (typeInfo.baseType) {
|
|
case MG_State::GLState::VertexAttribBaseType::Float:
|
|
g_GLESFuncs.glVertexAttrib4fv(location, currentValue.floatValue.data());
|
|
break;
|
|
case MG_State::GLState::VertexAttribBaseType::Int:
|
|
g_GLESFuncs.glVertexAttribI4iv(location, currentValue.intValue.data());
|
|
break;
|
|
case MG_State::GLState::VertexAttribBaseType::Uint:
|
|
g_GLESFuncs.glVertexAttribI4uiv(location, currentValue.uintValue.data());
|
|
break;
|
|
case MG_State::GLState::VertexAttribBaseType::Unsupported:
|
|
MGLOG_E_ONCE(
|
|
"SyncCurrentVertexAttributeValues: program=%u location=%u has no enabled array and its "
|
|
"shader input type 0x%x is not supported as a current generic vertex attribute",
|
|
program->GetExternalIndex(), location, program->GetAttribType(location));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// P5e (pa), CONTRACT-P5E §5.5 / §5.8 (ruling ID-81): THE SAME SYNC, OFF THE RECORD.
|
|
//
|
|
// The overload above is the MONOLITH GLUE and keeps its text token for token; this one is
|
|
// what the handle arm calls, and it answers the note vi left inside it - "the two program
|
|
// reads below (GetActiveAttributeLocationMask, GetAttribType) ... stay until pg carries
|
|
// the attribute mask and types in the ShaderCso record". pg's archive carries both:
|
|
// `attribs` and `attribTypes` are members of LinkArtifacts, the record ADOPTS the whole of
|
|
// it at create_shader_state, and neither is one of the three post-link mutable fields the
|
|
// bindings tails exist for (glUniformBlockBinding / glUniform1i on a sampler /
|
|
// glShaderStorageBlockBinding move those, and nothing moves these without a relink, which
|
|
// re-issues the create). So this arm adds NOTHING to the wire and changes no record - it
|
|
// reads rows that already arrived.
|
|
//
|
|
// WHY A SECOND FUNCTION rather than an `#if` inside the body above: §5.8's idiom, and G1 -
|
|
// the frontend overload's preprocessed text does not move, so the pull build gains no
|
|
// symbol and no byte. The price is that vi's memo arm is COPIED rather than shared; a
|
|
// shared tail would have rewritten the pull build's one, which G1 measures at 0/0/0/0.
|
|
void SyncCurrentVertexAttributeValues(BackendVertexArrayObject* vaoTwin, MG_Pipe::MGPipeHandle cso) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// THE SAME THREE-PART TEST SyncCurrentProgramByHandle MAKES, from the descriptor, and
|
|
// it is this arm's spelling of `if (!program) return;` above rather than a weaker one.
|
|
// LinkStatus is a FIELD and not 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 that did not link, or whose SPIR-V never arrived, is one the
|
|
// same Prepare has just bound program 0 for - there is no shader to feed a current
|
|
// generic attribute to, which is what the frontend arm gets from an unlinked
|
|
// program's empty `attribs`.
|
|
const MG_Pipe::MGPipeShaderCsoRecord* const record = PipeShaderCsoRecordForHandle(cso);
|
|
if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) {
|
|
return;
|
|
}
|
|
// A NULL ARCHIVE ON THIS ARM IS A NAMED REFUSAL, NEVER A FALL-BACK TO THE FRONTEND -
|
|
// the shape of RefuseNullFrontendTextureOffTheHandleArm / MGB_TEXTURE_RECORD_ARM_-
|
|
// SELECTED (Managers.cpp, ID-110). This function is reached from ProgramHandleArm()
|
|
// only, i.e. Transport != Monolith AND the program subsystem bit, and under a
|
|
// transport pg fills Archive at every create_shader_state; so a LIVE record with a
|
|
// LINKED descriptor and no archive is a seam defect and not a state a draw may run
|
|
// in. Reaching back for GetProgramForDraw() here would answer the two rows out of the
|
|
// client's own live LinkArtifacts - the memory Link() replaces in place, i.e. exactly
|
|
// the read this package retires - and would do it behind a picture that still looks
|
|
// right, which is what the subsystem A/B exists to expose.
|
|
if (!record->Archive) {
|
|
MGLOG_F("MGPipe: Fatal{RoleViolation, \"program-handle-arm\"} - "
|
|
"SyncCurrentVertexAttributeValues needs the active-attribute mask and the "
|
|
"attribute types of ShaderCso {%u, %u} and that record carries no "
|
|
"server-owned archive. The arm is selected by Transport != Monolith AND "
|
|
"the program subsystem bit (CONTRACT-P5E §5.8, ID-81), so this record's "
|
|
"create_shader_state was applied under a transport and must have adopted "
|
|
"one; the frontend ProgramObject is not a fall-back here, it IS the client "
|
|
"memory this row retires",
|
|
cso.Slot, cso.Gen);
|
|
std::abort();
|
|
}
|
|
const MG_State::GLState::LinkArtifacts& link = record->Archive->Link;
|
|
if (!vaoTwin) return;
|
|
|
|
// vi's half, unchanged in meaning and copied rather than shared (see above). The two
|
|
// family bits are independent A/Bs, so a program on the handle arm says nothing about
|
|
// whether the vertex-input family is - this arm has to carry both of vi's.
|
|
const MG_Pipe::MGPipeVertexElementsRecord* elementsRecord = nullptr;
|
|
MG_Pipe::MGPipeHandle elementsHandle = MG_Pipe::kMGPipeNullHandle;
|
|
Uint64 elementsSerial = 0;
|
|
const auto& st = MG_Pipe::MGPipeApplier();
|
|
elementsHandle = st.BoundVertexElements;
|
|
if (!MG_Pipe::MGPipeHandleIsNull(elementsHandle) && elementsHandle.Slot < st.VertexElementsCsos.size()) {
|
|
const auto& elements = st.VertexElementsCsos[elementsHandle.Slot];
|
|
if (elements.Live && elements.Gen == elementsHandle.Gen) {
|
|
elementsRecord = &elements;
|
|
elementsSerial = elements.ContentSerial;
|
|
}
|
|
}
|
|
if (elementsRecord == nullptr) return;
|
|
|
|
const SharedPtr<MG_State::GLState::VertexArrayObject> noVao;
|
|
const auto& vao = noVao;
|
|
|
|
// ProgramObject::GetActiveAttributeLocationMask, over the archive's own `attribs`:
|
|
// the same 32-location bound and the same "an empty name is not an active attribute"
|
|
// rule, computed where the names live instead of through an accessor MG_Backend
|
|
// cannot reach without a frontend object.
|
|
Uint32 activeAttribMask = 0;
|
|
{
|
|
const SizeT attribCount = std::min<SizeT>(link.attribs.size(), 32);
|
|
for (SizeT index = 0; index < attribCount; ++index) {
|
|
if (!link.attribs[index].empty()) activeAttribMask |= (1u << index);
|
|
}
|
|
}
|
|
if (activeAttribMask == 0) return;
|
|
|
|
auto& memo = vaoTwin->GetPendingAttribValueMaskMemo();
|
|
if (!memo.valid || !(memo.elementsHandle == elementsHandle) || memo.elementsSerial != elementsSerial ||
|
|
activeAttribMask != memo.activeMask) {
|
|
Uint32 pending = 0;
|
|
for (Uint32 remaining = activeAttribMask; remaining != 0; remaining &= remaining - 1) {
|
|
const Uint32 location = static_cast<Uint32>(std::countr_zero(remaining));
|
|
if (location >= MG_Pipe::kMGPipeMaxVertexAttribs || !elementsRecord->Attributes[location].Enabled) {
|
|
pending |= (1u << location);
|
|
}
|
|
}
|
|
memo.elementsHandle = elementsHandle;
|
|
memo.elementsSerial = elementsSerial;
|
|
memo.activeMask = activeAttribMask;
|
|
memo.pendingMask = pending;
|
|
memo.valid = true;
|
|
}
|
|
// Plus the enabled arrays the walk had to disable (Managers.h, GetDisabledEnabledAttribMask).
|
|
const Uint32 pendingMask = memo.pendingMask | (vaoTwin->GetDisabledEnabledAttribMask() & memo.activeMask);
|
|
if (pendingMask == 0) return;
|
|
|
|
for (Uint32 remaining = pendingMask; remaining != 0; remaining &= remaining - 1) {
|
|
const Uint32 location = static_cast<Uint32>(std::countr_zero(remaining));
|
|
|
|
// BOUNDS-CHECKED, exactly as ProgramObject::GetAttribType is not: the frontend's
|
|
// form indexes attribTypes raw because the mask it walked came off the same
|
|
// artifacts instance, and here the two vectors arrived over a wire. A short tail
|
|
// is an archive whose halves disagree; 0 classifies as Unsupported and says so
|
|
// below rather than reading past the end.
|
|
const GLenum attribType = location < link.attribTypes.size() ? link.attribTypes[location] : 0;
|
|
const auto& currentValue = MG_Pipe::gPipeInputs->GetCurrentVertexAttribute(location);
|
|
const auto typeInfo = MG_State::GLState::ClassifyVertexAttribType(attribType);
|
|
switch (typeInfo.baseType) {
|
|
case MG_State::GLState::VertexAttribBaseType::Float:
|
|
g_GLESFuncs.glVertexAttrib4fv(location, currentValue.floatValue.data());
|
|
break;
|
|
case MG_State::GLState::VertexAttribBaseType::Int:
|
|
g_GLESFuncs.glVertexAttribI4iv(location, currentValue.intValue.data());
|
|
break;
|
|
case MG_State::GLState::VertexAttribBaseType::Uint:
|
|
g_GLESFuncs.glVertexAttribI4uiv(location, currentValue.uintValue.data());
|
|
break;
|
|
case MG_State::GLState::VertexAttribBaseType::Unsupported:
|
|
// THE HANDLE, NOT A GL NAME: the server does not know the client's program
|
|
// names and must not learn them (ProgramArchiveSource::Identity states the
|
|
// same rule for the twin's log lines).
|
|
MGLOG_E_ONCE("SyncCurrentVertexAttributeValues: ShaderCso {%u, %u} location=%u has no "
|
|
"enabled array and its shader input type 0x%x is not supported as a current "
|
|
"generic vertex attribute",
|
|
cso.Slot, cso.Gen, location, attribType);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} // namespace VertexArrayImpl
|
|
|
|
namespace TextureImpl {
|
|
static Uint g_imageSweepBackendContextGeneration = 0;
|
|
static Bool g_imageSweepValid = false;
|
|
// P5e (fb, ruling 7 / ID-86): the record arm's three applier serials. Held in their own
|
|
// fields beside the frontend pair rather than reusing them, for the reason the two
|
|
// framebuffer list keys are held apart - one field carrying two key shapes is how a
|
|
// stale half gets compared against a live one.
|
|
static Uint64 g_imageSweepShaderImagesSerial = 0;
|
|
static Uint64 g_imageSweepTextureShutterSerial = 0;
|
|
static Uint64 g_imageSweepContextSerial = 0;
|
|
|
|
// The sweep is a glBindImageTexture per unit, so it must not run per draw: the gate is the
|
|
// frontend's sampling-resolution generation, which TextureObjectBase::BumpShapeVersion
|
|
// moves on exactly the shape and format changes that can force the re-mint. Deliberately
|
|
// NOT the backend-side re-mint counter (g_attachmentBackendIdGeneration's sibling would be
|
|
// the obvious choice): a texture that is bound ONLY to an image unit is re-minted inside
|
|
// this very sweep, so a backend-side trigger would be bumped after the gate had already
|
|
// declined to run it.
|
|
//
|
|
// P4a e3 (D-G4) leaves BOTH properties of this gate exactly as they are, and says so
|
|
// because they are the kind of thing a later optimization deletes:
|
|
//
|
|
// 1. the g_imageUnitHighWaterMark == 0 early-out is what makes every Minecraft draw
|
|
// pay one integer test, and it is asked BEFORE anything reads a record;
|
|
// 2. the gate key stays the FRONTEND sampling-resolution generation and is
|
|
// deliberately not re-keyed onto a server-owned serial, for the reason above: a
|
|
// texture bound only to an image unit is re-minted INSIDE this sweep, so an epoch
|
|
// the server bumps would move after the gate had already declined to run it. The
|
|
// client's own NewShaderImages shutter mixes the same three frontend counters
|
|
// (texture content, texture params, the program's image-unit version), so the
|
|
// property is preserved on both sides by construction rather than by agreement.
|
|
//
|
|
// P5e (fb, ruling 7 / ID-86): PROPERTY 1 IS UNTOUCHED. PROPERTY 2 IS RE-KEYED, and the
|
|
// argument behind it is kept rather than dropped - it is what decides WHICH four values
|
|
// the new key may be made of.
|
|
//
|
|
// The frontend sampling-resolution generation cannot be read on an apply thread, so the
|
|
// key becomes (ShaderImagesSerial, TextureShutterSerial, ContextSerial,
|
|
// g_backendContextGeneration). Three of those are APPLIER-DERIVED - they move when the
|
|
// client's own shutters send a new set of images, new texture content or new texture
|
|
// parameters, i.e. at exactly the moments the frontend counter moved - and the fourth
|
|
// is the backend's ES-context generation, which the old key already carried.
|
|
//
|
|
// NONE OF THEM IS A BACKEND RE-MINT COUNTER, and that is the whole of the ruling: the
|
|
// hazard the old comment names is a texture bound ONLY to an image unit being re-minted
|
|
// INSIDE this sweep, which would bump a backend-side epoch after the gate had already
|
|
// declined to run. An applier serial moves when the CLIENT said something, strictly
|
|
// before the sweep the saying provoked, so the property survives the re-key intact.
|
|
// g_backendContextGeneration is not a counter of that kind either - it moves when the
|
|
// ES context is rebuilt, which no sweep does.
|
|
//
|
|
// THE FOUR SERIALS SAY NOTHING ABOUT CONTENT, so the key carries each image texture's
|
|
// content serial as well. A glTexSubImage* into a texture that is bound ONLY to an image
|
|
// unit moves that texture's record Serial and none of the four: the upload stays in the
|
|
// record's pending set, and nothing else flushes it before the draw - the sampled-unit
|
|
// walk and the attachment lists never visit an image-only texture. The draw then
|
|
// imageStore()s into the driver texture, and the stale upload lands on top of those
|
|
// stores at the next sync that does visit the texture (a readback), which is the
|
|
// ordering the application asked for in reverse (KHR-GL42.shader_image_load_store.
|
|
// early-fragment-tests, which zeroes its image before every draw).
|
|
//
|
|
// Per unit, keyed on the bound HANDLE and its record's Serial: the handle carries the
|
|
// slot's generation, so a recycled slot cannot answer with its predecessor's serial, and
|
|
// the record Serial is the server's own monotone content version. The walk is bounded by
|
|
// the high-water mark, so it costs nothing on the draws the first early-out already
|
|
// returns from.
|
|
struct ImageSweepUnitContent {
|
|
MG_Pipe::MGPipeHandle Res = MG_Pipe::kMGPipeNullHandle;
|
|
Uint64 Serial = 0;
|
|
};
|
|
static Array<ImageSweepUnitContent, MG_Pipe::kMGPipeMaxImageUnits> g_imageSweepUnitContent{};
|
|
static Uint32 g_imageSweepUnitContentCount = 0;
|
|
|
|
static Uint32 ImageSweepUnitContentCount(const MG_Pipe::MGPipeApplierState& st) {
|
|
return std::min<Uint32>(static_cast<Uint32>(g_imageUnitHighWaterMark),
|
|
static_cast<Uint32>(st.BoundShaderImages.size()));
|
|
}
|
|
|
|
static Uint64 ImageUnitContentSerial(MG_Pipe::MGPipeHandle res) {
|
|
if (MG_Pipe::MGPipeHandleIsNull(res)) return 0;
|
|
const auto* record = PipeTextureRecordForHandle(res);
|
|
return record != nullptr ? record->Serial : 0;
|
|
}
|
|
|
|
static Bool ImageSweepContentUnchanged(const MG_Pipe::MGPipeApplierState& st) {
|
|
const Uint32 count = ImageSweepUnitContentCount(st);
|
|
if (count != g_imageSweepUnitContentCount) return false;
|
|
for (Uint32 unit = 0; unit < count; ++unit) {
|
|
const MG_Pipe::MGPipeHandle res = st.BoundShaderImages[unit].Res;
|
|
const auto& memo = g_imageSweepUnitContent[unit];
|
|
if (!(memo.Res == res) || memo.Serial != ImageUnitContentSerial(res)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void StampImageSweepContent(const MG_Pipe::MGPipeApplierState& st) {
|
|
const Uint32 count = ImageSweepUnitContentCount(st);
|
|
for (Uint32 unit = 0; unit < count; ++unit) {
|
|
const MG_Pipe::MGPipeHandle res = st.BoundShaderImages[unit].Res;
|
|
g_imageSweepUnitContent[unit] = {res, ImageUnitContentSerial(res)};
|
|
}
|
|
g_imageSweepUnitContentCount = count;
|
|
}
|
|
|
|
void SyncImageTextureBindingsForDraw(const DrawTextureSyncKeys& keys) {
|
|
if (g_imageUnitHighWaterMark == 0) return;
|
|
const auto& st = MG_Pipe::MGPipeApplier();
|
|
if (g_imageSweepValid && g_imageSweepShaderImagesSerial == st.ShaderImagesSerial &&
|
|
g_imageSweepTextureShutterSerial == st.TextureShutterSerial &&
|
|
g_imageSweepContextSerial == st.ContextSerial &&
|
|
g_imageSweepBackendContextGeneration == g_backendContextGeneration && ImageSweepContentUnchanged(st)) {
|
|
return;
|
|
}
|
|
SyncImageTextureBindings();
|
|
// The sweep may just have landed a re-upload into a texture the draw reaches through
|
|
// an image unit (the content clause above is one of the ways it gets here).
|
|
if (const GLbitfield barriers = TakeInPlaceTextureUploadBarrierBits();
|
|
barriers != 0 && g_GLESFuncs.glMemoryBarrier) {
|
|
g_GLESFuncs.glMemoryBarrier(barriers);
|
|
}
|
|
g_imageSweepShaderImagesSerial = st.ShaderImagesSerial;
|
|
g_imageSweepTextureShutterSerial = st.TextureShutterSerial;
|
|
g_imageSweepContextSerial = st.ContextSerial;
|
|
g_imageSweepBackendContextGeneration = g_backendContextGeneration;
|
|
// After the sweep: it is the sync that consumed the pending set, and it may raise the
|
|
// high-water mark the walk is bounded by.
|
|
StampImageSweepContent(st);
|
|
g_imageSweepValid = true;
|
|
return;
|
|
}
|
|
} // namespace TextureImpl
|
|
|
|
void BindCurrentFBO(FramebufferTarget target) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// The handle arm reads the record instead of the binding slot, which is one of the
|
|
// five GetFramebufferBindingSlotChecked call sites this phase retires. Everything the
|
|
// pre-handle arm below is careful about is careful here for the same reasons and in
|
|
// the same order: NO FAST PATH on any version (the skip is BindFramebufferId's job and
|
|
// its shadow is where the cost actually is), and the DEFAULT framebuffer is bound
|
|
// THROUGH THE SHADOW, never raw. What changes is only where the two questions - "which
|
|
// framebuffer" and "is it the default one" - are answered: the record's Fbo and its
|
|
// IsDefault byte, rather than the bound object's address and a comparison against
|
|
// pDefaultFramebufferInfo->defaultFBO.
|
|
if (FramebufferImpl::g_fboRecordsTrusted) {
|
|
const MG_Pipe::MGPFramebufferState* const recordPtr = BoundFramebufferRecord(target);
|
|
// The record's own handle is the "has this binding ever been described" test, not
|
|
// FramebufferSerial - which MGPipeApplierReset advances whether or not anything
|
|
// was ever emitted (see SyncCurrentFBOByRecord). The trust latch above is the
|
|
// other half: it says the sync that ran a moment ago found these two records
|
|
// describing these two bindings.
|
|
// P4a decline-site F5: FLIPPED AT THE VERIFICATION ROUND to an assertion, same
|
|
// argument and same shape as F7 above - the trust latch implies a record with a
|
|
// non-null handle on both targets, and a silent decline here would look exactly
|
|
// like a legitimate one.
|
|
MOBILEGL_ASSERT(recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo));
|
|
if (recordPtr != nullptr && !MG_Pipe::MGPipeHandleIsNull(recordPtr->Fbo)) {
|
|
const MG_Pipe::MGPFramebufferState& record = *recordPtr;
|
|
if (record.IsDefault == 0) {
|
|
auto* twinEntry = FramebufferImpl::g_backendFramebufferObjects.FindByHandle(record.Fbo);
|
|
// P4a decline-site F6: S - already loud, and it KEPT this shape at the
|
|
// verification round: binding nothing is exactly what the pre-handle arm
|
|
// does in the same situation, so the two arms stay at parity.
|
|
if (twinEntry && *twinEntry) {
|
|
(*twinEntry)->Bind(target);
|
|
} else {
|
|
MGLOG_E_ONCE("No backend FBO found (maybe not synced) for the current %s FBO record, "
|
|
"cannot bind FBO.",
|
|
(target == FramebufferTarget::Read ? "READ" : "DRAW"));
|
|
}
|
|
} else {
|
|
MGLOG_D("Binding default framebuffer as %s FBO",
|
|
(target == FramebufferTarget::Read ? "READ" : "DRAW"));
|
|
if (FramebufferImpl::CurrentFramebufferBinding(target) != 0) FlushAheadOfWindowBufferUse();
|
|
// Through the shadow: a raw bind here would leave the shadow claiming
|
|
// the previous user FBO, false-skipping its next re-bind.
|
|
FramebufferImpl::BindFramebufferId(
|
|
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
// P5e (fb, §5.4): the pre-handle arm below reads the binding slot, so under a transport
|
|
// reaching it is the same refusal SyncCurrentFBO takes. Reaching it means either the
|
|
// sync's trust latch is clear - and the sync aborted before it could be - or this bind
|
|
// ran with no sync in front of it, which the latch exists to catch.
|
|
RefuseFramebufferBindingSlotRead();
|
|
}
|
|
|
|
void PrepareForDraw(DrawSyncFlags syncBit) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// One twin resolve serves the whole draw: the buffer sync (which hosts the
|
|
// resolved-buffers memo on the twin), the VAO sync and the draw-time bind
|
|
// below. Nothing in between can invalidate it — the bound VAO is pinned by
|
|
// the context, and no step here erases or replaces a live VAO's twin.
|
|
//
|
|
// P5e (vi), CONTRACT-P5E §4.2 / §5.1: under a transport the twin is resolved from
|
|
// st.BoundVertexElements - THE SAME HANDLE the frontend probe used to mint, since
|
|
// EmitVertexElements acquires it off the VAO's own lifetime id - and neither the bound
|
|
// VAO row nor the configuration version is read at all. `currentVAO` stays null on that
|
|
// arm, which is what the two callees' record arms expect; the null twin case (no
|
|
// vertex-elements CSO bound) reaches the same BindBackendVAOId(0) below that a null
|
|
// frontend VAO always did.
|
|
const SharedPtr<MG_State::GLState::VertexArrayObject> noFrontendVao;
|
|
const auto& currentVAO = noFrontendVao;
|
|
VertexArrayImpl::BackendVertexArrayObject* vaoTwin = nullptr;
|
|
Uint32 vaoConfigVersion = 0;
|
|
vaoTwin = VertexArrayImpl::ResolveVaoTwin(MG_Pipe::MGPipeApplier().BoundVertexElements);
|
|
if (vaoTwin != nullptr) vaoTwin->AdoptUnchangedBufferSets(MG_Pipe::MGPipeApplier());
|
|
// One program resolve and one texture-key capture serve the whole draw, for
|
|
// the same reason the twin resolve does: only frontend GL entry points move
|
|
// either, and none can run inside this preparation. GetProgramForDraw is a
|
|
// cross-TU call with a guarded static inside - repeating it per stage showed
|
|
// up in draw-loop profiles.
|
|
//
|
|
// P5e (pa), kimi row 91 / S4 1.1: AND ON THE RECORD ARM IT IS NOT MADE AT ALL. The row
|
|
// this package retires is this single call - it is what MGP_INPUT_CHECK trips on, once
|
|
// per draw, and it is 61 of the strict lane's 109 red entries. The arm is
|
|
// DrawProgramFromRecords(), which names every consumer of the value rather than this
|
|
// family alone; `currentProgram` stays null on it and the four callees below take their
|
|
// own handle arms, none of which consults it. The null twin case (no program bound)
|
|
// reaches the same "nothing to bind" answers a null frontend program always did.
|
|
const SharedPtr<MG_State::GLState::ProgramObject> noFrontendProgram;
|
|
const auto& currentProgram = noFrontendProgram;
|
|
if (MG_Util::PipeStats::Enabled()) {
|
|
// THE per-draw denominator for Espryt, plus this function's own two accessor
|
|
// calls (the VAO and the draw program). Everything the callees below read is
|
|
// counted by the callees that are instrumented; the rest is not counted (see
|
|
// the inventory in PipeStats.cpp).
|
|
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1);
|
|
// ZERO on the record arm, one per row still pulled otherwise: P5e (vi) retired the
|
|
// bound-VAO accessor call and P5e (pa) the draw-program one, and the ledger has to
|
|
// say so or the phase's own "accessor calls per draw" number would keep counting
|
|
// reads that no longer happen. Two independent bits, so it is a sum and not a
|
|
// three-way pick.
|
|
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, (0) + (0));
|
|
}
|
|
const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys();
|
|
|
|
BufferImpl::SyncNeccessaryBuffers(currentVAO, vaoTwin, vaoConfigVersion, syncBit & DrawSyncBit::IndexBuffer,
|
|
syncBit & DrawSyncBit::IndirectBuffer);
|
|
// The draw's integer arrays and the program's narrow-integer seeds (NarrowIntegerSeeds.h).
|
|
IntegerAttributePlan integerPlan = ResolveIntegerAttributePlanForDraw(MG_Pipe::MGPipeApplier().DrawProgram);
|
|
VertexArrayImpl::SyncCurrentVAOFromRecords(vaoTwin, integerPlan);
|
|
TextureImpl::SyncNeccessaryTextures(textureKeys);
|
|
// A draw reads and writes through its image units too, so the unit bindings have to be
|
|
// as current as the sampled ones. Gated (see the sweep): a program with no image binding
|
|
// pays one integer test, and one with images re-issues them only when a texture shape
|
|
// moved under them.
|
|
TextureImpl::SyncImageTextureBindingsForDraw(textureKeys);
|
|
// A draw writes through its image units too - the conformance case that found this
|
|
// stores into a buffer texture from the FRAGMENT stage, not from a dispatch.
|
|
TextureImpl::MarkWritableImageBufferTexturesGpuWritten();
|
|
FramebufferImpl::SyncCurrentFBO();
|
|
// P5e (pg), kimi row 91 / S4 1.1: the SharedPtr pull above is the row this family
|
|
// retires. On the handle arm the program is MGPipeApplier().DrawProgram - a handle the
|
|
// set_draw_program record carried - and `currentProgram` is not consulted for the sync
|
|
// at all; the remaining uses of it below are the two callees' own, and both have a
|
|
// handle arm of their own. P5e (pa) finished it: the pull itself is gone on that arm and
|
|
// `currentProgram` is a null SharedPtr there, which this branch never reaches for.
|
|
PrgramImpl::SyncCurrentProgramByHandle(MG_Pipe::MGPipeApplier().DrawProgram, true, &integerPlan);
|
|
if (DropRefusedNarrowIntegerSeeds(integerPlan, MG_Pipe::MGPipeApplier().DrawProgram))
|
|
VertexArrayImpl::SyncCurrentVAOFromRecords(vaoTwin, integerPlan);
|
|
RenderStateImpl::SyncRenderState();
|
|
|
|
BindCurrentFBO(FramebufferTarget::Draw);
|
|
|
|
{
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC("BindCurrentVAO", TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
if (vaoTwin) {
|
|
vaoTwin->Bind();
|
|
} else {
|
|
VertexArrayImpl::BindBackendVAOId(0);
|
|
}
|
|
}
|
|
|
|
// P5e (pa): the third consumer, on ITS OWN arm and not on the hoist's. The hoist above is
|
|
// a conjunction over four callees; this one is served by the record whenever the PROGRAM
|
|
// family is on the handle arm, which includes the mixed A/B where the texture bits are
|
|
// off and `currentProgram` above is therefore still a real object.
|
|
VertexArrayImpl::SyncCurrentVertexAttributeValues(vaoTwin, MG_Pipe::MGPipeApplier().DrawProgram);
|
|
|
|
BindCurrentTextures(textureKeys, currentProgram);
|
|
BindCurrentProgramWithResources(currentProgram, textureKeys, MG_Pipe::MGPipeApplier().DrawProgram);
|
|
PushNarrowIntegerWidthsForDraw(integerPlan, MG_Pipe::MGPipeApplier().DrawProgram);
|
|
|
|
// Last: opening the capture span needs the program current and the capture
|
|
// buffers bound, and ES rejects most binding changes once it is open.
|
|
XfbImpl::StartPendingTransformFeedback();
|
|
}
|
|
|
|
// Resolves every frontend texture unit's textures onto the backend context. Returns
|
|
// false when the resolution could not be completed from the state it read - a bound
|
|
// texture that has no backend object yet is skipped, and a later draw would bind it
|
|
// without any of the memo keys below moving - so the caller must not memoise it.
|
|
//
|
|
// P5e (pa): `currentProgram` IS THE LEGACY WALK'S PARAMETER and is null under
|
|
// TextureImpl::UnitTexturesByHandle(), where the by-handle pass below returns before the
|
|
// only reader of it (sampledTargetForUnit) is even declared. The caller drops the object
|
|
// only once that same test holds, so this is the arm speaking and not a pointer test.
|
|
Bool ResolveAndBindUnitTextures(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
|
Int maxTouchedUnit) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC("ResolveAndBindUnitTextures", TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
Bool fullyResolved = true;
|
|
// P5e (tx2), CONTRACT-P5E §5.2. ONE PASS OVER THE SAMPLER-VIEW WINDOW, AND THEN AN
|
|
// UNBIND OF EVERY TARGET THE WINDOW DID NOT CLAIM.
|
|
//
|
|
// The three-pass slot walk below exists to arbitrate: desktop 1D/1D-array targets alias
|
|
// ES 2D/2D-array, a unit can hold a real texture on one of an aliased pair and a default
|
|
// object on the other, and two REAL textures can want one native target - which the
|
|
// program's sampler types decide. THE CLIENT HAS ALREADY DECIDED ALL OF IT
|
|
// (SamplerEmit.h's per-unit resolution is keyed on exactly those sampler types), so the
|
|
// arbiter dies here rather than being re-implemented over records: one texture per unit,
|
|
// named by the record, with its frontend target in the view CSO.
|
|
//
|
|
// The unbind half keeps its shape and changes its driver: instead of walking the frontend
|
|
// unit's binding slots it walks the fixed TextureTarget enum against g_boundTexturesCache,
|
|
// which is SERVER memory and is the shadow every redundant-bind filter in this backend
|
|
// already trusts. That is what makes "the window did not claim it" mean "unbind it" - the
|
|
// same statement the pre-handle pass makes with `!boundBackendTargets[...]`.
|
|
const auto& st = MG_Pipe::MGPipeApplier();
|
|
const Uint32 windowEnd = st.SamplerViewStart + st.SamplerViewCount;
|
|
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
|
|
const Uint32 index = static_cast<Uint32>(unit);
|
|
Array<Bool, (SizeT)TextureTarget::TextureTargetCount> boundBackendTargets{};
|
|
if (index >= st.SamplerViewStart && index < windowEnd && index < st.BoundSamplerViews.size()) {
|
|
const auto& view = st.BoundSamplerViews[index];
|
|
if (!MG_Pipe::MGPipeHandleIsNull(view.Texture)) {
|
|
// The TARGET is the view CSO's, which is the frontend target the client
|
|
// resolved this unit against - not a second derivation from the texture's
|
|
// descriptor, which would be a second authority for one value.
|
|
const auto* viewRecord = PipeSamplerViewRecordForHandle(view.View);
|
|
const auto target = viewRecord != nullptr ? static_cast<TextureTarget>(viewRecord->View.Target)
|
|
: TextureTarget::Unknown;
|
|
if (viewRecord == nullptr) {
|
|
MGLOG_E_ONCE("MGPipe: unit %d's sampler view {%u, %u} has no applier record, "
|
|
"so the target to bind texture {%u, %u} at is unknown; the unit "
|
|
"keeps what it holds",
|
|
static_cast<int>(unit), view.View.Slot, view.View.Gen, view.Texture.Slot,
|
|
view.Texture.Gen);
|
|
fullyResolved = false;
|
|
} else if (!TextureImpl::IsSupportedTextureTarget(target)) {
|
|
MGLOG_D(" Texture target %s is not supported, skipping.",
|
|
MG_Util::ConvertTextureTargetToString(target).c_str());
|
|
} else if (auto* twin = TextureImpl::ResolveTextureTwin(view.Texture)) {
|
|
const GLenum targetGL = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
|
|
twin->Bind(targetGL, unit);
|
|
boundBackendTargets[static_cast<SizeT>(TextureImpl::MapToBackendTextureTarget(target))] = true;
|
|
} else {
|
|
// No twin yet: the sync pass that would have built it declined and
|
|
// named why. Not memoisable - a later draw would bind it without any
|
|
// key moving.
|
|
fullyResolved = false;
|
|
}
|
|
}
|
|
}
|
|
// The backend half of glBindTexture(..., 0) for every native target this unit no
|
|
// longer claims, driven by the server's own binding shadow.
|
|
Array<Bool, (SizeT)TextureTarget::TextureTargetCount> visitedBackendTargets{};
|
|
for (SizeT t = 0; t < (SizeT)TextureTarget::TextureTargetCount; ++t) {
|
|
const auto target = static_cast<TextureTarget>(t);
|
|
if (!TextureImpl::IsSupportedTextureTarget(target)) continue;
|
|
const auto backendTargetIndex = static_cast<SizeT>(TextureImpl::MapToBackendTextureTarget(target));
|
|
if (visitedBackendTargets[backendTargetIndex]) continue;
|
|
visitedBackendTargets[backendTargetIndex] = true;
|
|
if (boundBackendTargets[backendTargetIndex]) continue;
|
|
if (TextureImpl::g_boundTexturesCache[static_cast<SizeT>(unit)][backendTargetIndex] == nullptr) {
|
|
continue;
|
|
}
|
|
TextureImpl::UnbindTexture(unit, TextureImpl::ConvertTextureTargetToBackendGLEnum(target));
|
|
}
|
|
}
|
|
return fullyResolved;
|
|
}
|
|
|
|
// Per-unit memo of the sampler-registry lookup, NOT of the resulting unit binding
|
|
// (BackendSamplerObject::Bind already dedups against g_boundSamplersCache). The registry
|
|
// pairs a live frontend sampler with one backend object for the frontend's whole
|
|
// lifetime: GetOrCreate replaces a pairing only when the frontend expired, entries are
|
|
// erased only when the frontend expired, and a unit's sampler cannot expire while the
|
|
// unit holds it - so owner-equality of the unit's current sampler against the snapshot
|
|
// proves the cached pointer is exactly what Find would return. A miss result is never
|
|
// cached: the backend object may not exist yet when the unit pass runs (the program
|
|
// pass creates it later in the same draw), and a cached miss would keep skipping the
|
|
// bind after it appears.
|
|
struct UnitSamplerLookupMemo {
|
|
// The {slot, gen} of the frontend sampler this row was resolved for. It replaces the
|
|
// weak_ptr and its owner compare: a stale row cannot match, because the successor of a
|
|
// freed sampler is handed the same slot only with a higher Gen.
|
|
MG_Pipe::MGPipeHandle frontendHandle = MG_Pipe::kMGPipeNullHandle;
|
|
SamplerImpl::BackendSamplerObject* backend = nullptr;
|
|
};
|
|
static Array<UnitSamplerLookupMemo, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
|
g_unitSamplerLookupMemos;
|
|
|
|
// Puts each touched unit's frontend sampler object on the backend unit. Not part
|
|
// of the texture memo below (BindCurrentProgramWithResources may rewrite sampled
|
|
// units' samplers right after this runs - raw-depth-fetch substitution,
|
|
// per-program sampler objects); instead the walk carries its own memo:
|
|
//
|
|
// The walk is a pure function of WHICH sampler object each touched unit holds -
|
|
// covered by (context id, unit-bindings epoch, high-water mark; the epoch
|
|
// snapshot includes each unit's sampler object and bumps when the mark moves) -
|
|
// and replaying it as "do nothing" additionally requires the sampler bindings it
|
|
// left to still be on the driver, which the row compare against
|
|
// g_boundSamplersCache (the shadow every sampler bind routes through) proves.
|
|
// Any later writer - the program pass substituting a raw-depth sampler, a
|
|
// frontend sampler bind/unbind (epoch), the ES-context generation moving - lands
|
|
// in one of those inputs. A unit whose sampler had no backend object yet leaves
|
|
// its row untouched; the object is only created by the program pass, whose Bind
|
|
// moves the row and thereby re-opens this memo, so the miss cannot be latched.
|
|
Uint64 g_unitSamplerWalkContextId = 0;
|
|
Uint64 g_unitSamplerWalkEpoch = 0;
|
|
Int g_unitSamplerWalkMaxUnit = -1;
|
|
Uint g_unitSamplerWalkContextGeneration = 0;
|
|
Bool g_unitSamplerWalkValid = false;
|
|
static decltype(SamplerImpl::g_boundSamplersCache) g_unitSamplerWalkRows{};
|
|
|
|
void BindCurrentUnitSamplers(const TextureImpl::DrawTextureSyncKeys& keys) {
|
|
const Int maxTouchedUnit = keys.maxTouchedUnit;
|
|
const SizeT rowBytes = static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(SamplerImpl::g_boundSamplersCache[0]);
|
|
if (g_unitSamplerWalkValid && g_unitSamplerWalkContextId == keys.contextId &&
|
|
g_unitSamplerWalkEpoch == keys.unitBindingsEpoch && g_unitSamplerWalkMaxUnit == maxTouchedUnit &&
|
|
g_unitSamplerWalkContextGeneration == g_backendContextGeneration &&
|
|
std::memcmp(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes) == 0) {
|
|
return;
|
|
}
|
|
g_unitSamplerWalkValid = false;
|
|
Bool walkedFromRecords = false;
|
|
// P4a e4 (D-G2). THE WALK ITSELF READS THE APPLIER'S PER-UNIT HANDLE ARRAY.
|
|
//
|
|
// bind_sampler_states is the resolved answer to exactly the question this loop asks -
|
|
// which sampler CSO does each touched unit carry - so the handle IS the lookup: no
|
|
// GetTextureUnitObject per unit, no lifetime-id probe, and no UnitSamplerLookupMemo
|
|
// row to keep, because FindByHandle is already one array index. The memo below stays
|
|
// for the arm that still resolves through a frontend object.
|
|
//
|
|
// A NULL HANDLE MEANS "this unit has no sampler object", which is the record's way of
|
|
// saying what the frontend's null SharedPtr says, and it unbinds - the texture's own
|
|
// built-in sampler then applies, exactly as today. A live handle resolves to THE CSO's
|
|
// OWN TWIN (P4a fable seam F-4, SamplerImpl::ResolveSamplerCsoTwin): the sentence that
|
|
// stood here - "a handle whose twin does not exist yet is left alone ... the program
|
|
// pass creates the twin later in the same draw" - described a lookup that could never
|
|
// hit, because the handle is content-addressed and the registry's twins were minted off
|
|
// lifetime ids, so this arm bound nothing on every draw and only the pre-handle program
|
|
// pass ever put a glBindSampler'd object on the driver. The program pass now binds the
|
|
// same CSO twin for the units it samples, which is what stops the two arms ping-ponging
|
|
// a unit between two driver samplers.
|
|
//
|
|
// Declines - falls through to the frontend walk - until the set has arrived, and the
|
|
// COUNT says so rather than the serial (MGPipeApplierReset advances serials whether or
|
|
// not anything was emitted).
|
|
// P4a decline-site S1: M - the mask says this family is not switched on, and it IS
|
|
// D's SamplerSubsystemEnabled() now. Silent, confirmed at the verification round.
|
|
const auto& st = MG_Pipe::MGPipeApplier();
|
|
// P4a decline-site S2: S - FLIPPED AT THE VERIFICATION ROUND to loud-once, then
|
|
// the frontend walk. Bit 11 is on (the enclosing SamplerSubsystemEnabled()) and
|
|
// package C has landed, so a draw that touches texture units with no
|
|
// bind_sampler_states ever applied is a seam defect. The frontend walk still
|
|
// runs: it answers the same question correctly, and what this line exists to stop
|
|
// is the SILENT permanent fallback, not the fallback.
|
|
// NARROWED BY THE FIRST REAL-PATH RUN, exactly as T2 was and for the same
|
|
// reason: maxTouchedUnit < 0 is a draw that touches no texture unit, for which an
|
|
// empty window is the correct emission. All 161 hits of the unnarrowed line were
|
|
// that shape - the log said `units 0..-1` on every one of them.
|
|
if (st.SamplerStateCount == 0 && maxTouchedUnit >= 0) {
|
|
// P5e (tx2), CONTRACT-P5E §5.3: the same refusal CurrentUnitBindingsEpoch makes -
|
|
// the frontend walk below reads GetTextureUnitObject per unit, which an active
|
|
// transport may not do for a record whose client has moved on.
|
|
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - a draw "
|
|
"touches units 0..%d and no bind_sampler_states has ever been applied "
|
|
"while the sampler subsystem bit is set; the pre-handle sampler walk "
|
|
"reads client memory and is refused under an active transport "
|
|
"(CONTRACT-P5E.md §5.3)",
|
|
static_cast<int>(maxTouchedUnit));
|
|
std::abort();
|
|
MGLOG_E_ONCE("A sampler record does not describe the binding it names: a draw touches "
|
|
"units 0..%d and no bind_sampler_states has ever been applied while the "
|
|
"sampler subsystem bit is set; running the pre-handle sampler walk.",
|
|
static_cast<int>(maxTouchedUnit));
|
|
}
|
|
if (st.SamplerStateCount != 0) {
|
|
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
|
|
const Uint32 index = static_cast<Uint32>(unit);
|
|
// P4a decline-site S3: the OUT-OF-WINDOW UNBIND (MAJOR-2, fixed here); no
|
|
// flip remains at the verification round, only the A8 measurement below.
|
|
//
|
|
// A UNIT THE RECORD DOES NOT DESCRIBE IS UNBOUND, NOT SKIPPED. The var-tail
|
|
// window IS the bound for RECORD RETENTION - entries outside it are not
|
|
// cleared in the applier - but this loop is not reading records, it is
|
|
// writing DRIVER state, and for a driver binding "no record" means "no
|
|
// sampler object", which is exactly what the pre-handle arm's
|
|
// `else { UnbindSampler(unit); }` below says and why it was written: a
|
|
// sampler object left on the unit by an earlier draw keeps being applied,
|
|
// and on a multisample texture - which takes no sampler object at all - the
|
|
// draw is rejected outright. `continue` here reintroduced that defect on
|
|
// the new arm for every unit below SamplerStateStart or above the window,
|
|
// which is the same smaller-window direction DEV-3 refused.
|
|
//
|
|
// A8 again: nothing in the contract pins the window to cover
|
|
// [0, maxTouchedUnit]. If package C documents that it does, this branch
|
|
// becomes unreachable and can be asserted rather than executed; until then
|
|
// it is load-bearing, and the verification round measures
|
|
// (SamplerStateStart, SamplerStateCount) against maxTouchedUnit to say so.
|
|
if (index < st.SamplerStateStart || index - st.SamplerStateStart >= st.SamplerStateCount) {
|
|
SamplerImpl::UnbindSampler(unit);
|
|
continue;
|
|
}
|
|
const MG_Pipe::MGPipeHandle sampler = st.BoundSamplerStates[index];
|
|
if (MG_Pipe::MGPipeHandleIsNull(sampler)) {
|
|
SamplerImpl::UnbindSampler(unit);
|
|
continue;
|
|
}
|
|
// P4a decline-site S4, RETIRED at the fable seam round (F-4): what stood here
|
|
// was `g_backendSamplerObjects.FindByHandle(sampler)` - an identity-keyed
|
|
// table asked for a content-addressed handle, a miss by construction on
|
|
// every draw - with a comment that read the miss as a within-draw ordering
|
|
// fact. The twin is the CSO's own now; a null answer has already named its
|
|
// reason (no record, or a slot that cannot be adopted) and leaves the unit
|
|
// as it is, which is the one decline this arm still has.
|
|
if (auto* twin = SamplerImpl::ResolveSamplerCsoTwin(sampler)) {
|
|
twin->Bind(unit);
|
|
}
|
|
}
|
|
walkedFromRecords = true;
|
|
}
|
|
if (!walkedFromRecords) {
|
|
// P13: no bind_sampler_states window applied, i.e. no unit holds a sampler object -
|
|
// the record arm emits one for every unit that does. Unbind, so a sampler object
|
|
// left on a unit by an earlier draw is not applied to this one.
|
|
for (Int unit = 0; unit <= maxTouchedUnit; ++unit)
|
|
SamplerImpl::UnbindSampler(unit);
|
|
}
|
|
g_unitSamplerWalkContextId = keys.contextId;
|
|
g_unitSamplerWalkEpoch = keys.unitBindingsEpoch;
|
|
g_unitSamplerWalkMaxUnit = maxTouchedUnit;
|
|
g_unitSamplerWalkContextGeneration = g_backendContextGeneration;
|
|
std::memcpy(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes);
|
|
g_unitSamplerWalkValid = true;
|
|
}
|
|
// Small per-PROGRAM memo set, not one global: the program is part of the key
|
|
// (it arbitrates aliased native targets), so a frame that cycles a handful of
|
|
// programs over UNCHANGED unit bindings - Sodium issues 62 glUseProgram per
|
|
// frame - invalidated a single memo on every switch and re-ran the full
|
|
// three-pass slot walk for an answer that had not moved. Entries are selected
|
|
// by raw program pointer; everything a recycled address could confuse is
|
|
// caught by the full key compare (lifetime id) exactly as before. Round-robin
|
|
// eviction; 4 entries cover the shader set a Minecraft-shaped frame cycles.
|
|
Array<ResolvedTextureBindingMemo, 4> g_resolvedTextureBindingMemos;
|
|
Uint32 g_resolvedTextureBindingMemoCursor = 0;
|
|
|
|
// Rebinds every frontend texture unit's textures (and sampler objects) on the
|
|
// backend context. Needed before draws AND compute dispatches: content syncs
|
|
// (SyncTextureObjectToBackend) bind scratch textures on the active unit as a
|
|
// side effect, so unit bindings must be re-established afterwards or shaders
|
|
// sample whatever texture the last sync left behind (e.g. Flywheel's depth
|
|
// pyramid downsample reading a stale unit-0 binding instead of the depth
|
|
// attachment).
|
|
//
|
|
// P5e (pa): `currentProgram` IS NULL ON THE HANDLE ARM, and that is a statement about the
|
|
// caller's arm rather than something this body may test for. Every read of it below sits on
|
|
// the `byHandle == false` side of a branch whose condition is TextureImpl::UnitTexturesByHandle()
|
|
// - the memo's entry selection, the four frontend key rows, and ResolveAndBindUnitTextures'
|
|
// sampledTargetForUnit lambda, which its own handle arm returns before reaching. The hoist in
|
|
// PrepareForDraw only drops the object once THAT test holds too (DrawProgramFromRecords), so
|
|
// this parameter is null exactly where nothing consults it.
|
|
void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys,
|
|
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC("BindCurrentTextures", TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// Units past the frontend's high-water mark have provably-empty slots.
|
|
const Int maxTouchedUnit = keys.maxTouchedUnit;
|
|
|
|
// P5e (tx2) / ruling 6: on the handle arm the key is the applier's rows and nothing
|
|
// frontend - INCLUDING the entry selection, which used the frontend program's address.
|
|
// maxTouchedUnit stays in it because the unbind sweep this memo replays is bounded by it.
|
|
MG_Pipe::MGPipeHandle drawProgram = MG_Pipe::kMGPipeNullHandle;
|
|
Uint64 shaderCsoSerial = 0;
|
|
Uint64 bindingsSerial = 0;
|
|
Uint64 samplerViewsSerial = 0;
|
|
Uint64 contextSerial = 0;
|
|
const auto& st = MG_Pipe::MGPipeApplier();
|
|
drawProgram = st.DrawProgram;
|
|
samplerViewsSerial = st.SamplerViewsSerial;
|
|
contextSerial = st.ContextSerial;
|
|
if (const auto* cso = PipeShaderCsoRecordForHandle(drawProgram)) {
|
|
shaderCsoSerial = cso->Serial;
|
|
bindingsSerial = cso->BindingsSerial;
|
|
}
|
|
// Entry selection by program pointer; a missing program takes the round-robin
|
|
// victim. WHICH entry is used is only a performance choice - correctness sits
|
|
// entirely in the full key + shadow compare below, unchanged from the single
|
|
// memo this set replaces.
|
|
//
|
|
// P5e (pa): NOT EVEN THE POINTER, on the handle arm. The by-handle branch below selects
|
|
// and keys on {DrawProgram, ShaderCso.Serial, BindingsSerial} (ruling ID-86) and never
|
|
// looks at this value, but it was still being loaded out of the caller's SharedPtr every
|
|
// draw - and once the caller stops holding one there is nothing there to load.
|
|
const void* programKey = nullptr;
|
|
ResolvedTextureBindingMemo* memoSlot = nullptr;
|
|
for (auto& candidate : g_resolvedTextureBindingMemos) {
|
|
if (candidate.valid && candidate.drawProgram.Slot == drawProgram.Slot &&
|
|
candidate.drawProgram.Gen == drawProgram.Gen) {
|
|
memoSlot = &candidate;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
if (!memoSlot) {
|
|
g_resolvedTextureBindingMemoCursor =
|
|
(g_resolvedTextureBindingMemoCursor + 1) % g_resolvedTextureBindingMemos.size();
|
|
memoSlot = &g_resolvedTextureBindingMemos[g_resolvedTextureBindingMemoCursor];
|
|
}
|
|
auto& memo = *memoSlot;
|
|
const SizeT shadowBytes = static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(TextureImpl::g_boundTexturesCache[0]);
|
|
const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch;
|
|
// P5e (ID-124): the legacy key is a LAZY LAMBDA, not an eagerly-computed Bool. It was
|
|
// the else-arm of a ternary whose declaration sat inside `#if MOBILEGL_BUILD_DISAGGREGATED`
|
|
// while the arm itself and every use of `keysMatch` sat outside it, so neither
|
|
// non-disaggregated flavour compiled. Hoisting it to an eager Bool would have been
|
|
// WORSE than the build break: it reads `currentProgram`, which is null on the handle
|
|
// arm, so the laziness the ternary gave for free is load-bearing (ID-81 / ID-110 -
|
|
// the arm decides, and the other arm's reads must not happen at all).
|
|
const auto legacyKeysMatch = [&]() -> Bool {
|
|
return (memo.valid && memo.glContextId == keys.contextId && memo.maxTouchedUnit == maxTouchedUnit &&
|
|
memo.unitBindingsEpoch == unitBindingsEpoch &&
|
|
memo.samplingResolutionGeneration == keys.samplingGeneration && memo.program == programKey &&
|
|
memo.programLifetimeId == (currentProgram ? currentProgram->GetLifetimeId() : 0) &&
|
|
memo.programBackendStateVersion ==
|
|
(currentProgram ? currentProgram->GetBackendStateVersion() : 0) &&
|
|
memo.programLinked == (currentProgram && currentProgram->GetLinkStatus()) &&
|
|
memo.contextGeneration == g_backendContextGeneration);
|
|
};
|
|
const Bool keysMatch =
|
|
(memo.valid && memo.maxTouchedUnit == maxTouchedUnit && memo.contextSerial == contextSerial &&
|
|
memo.samplerViewsSerial == samplerViewsSerial && memo.drawProgram.Slot == drawProgram.Slot &&
|
|
memo.drawProgram.Gen == drawProgram.Gen && memo.shaderCsoSerial == shaderCsoSerial &&
|
|
memo.bindingsSerial == bindingsSerial && memo.contextGeneration == g_backendContextGeneration);
|
|
// Short-circuited: the shadow compare is only meaningful once the key (and with it the
|
|
// snapshotted row count) matches.
|
|
if (!keysMatch ||
|
|
std::memcmp(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), shadowBytes) != 0) {
|
|
memo.valid = false;
|
|
if (ResolveAndBindUnitTextures(currentProgram, maxTouchedUnit)) {
|
|
memo.glContextId = keys.contextId;
|
|
memo.maxTouchedUnit = maxTouchedUnit;
|
|
memo.unitBindingsEpoch = unitBindingsEpoch;
|
|
memo.samplingResolutionGeneration = keys.samplingGeneration;
|
|
memo.program = programKey;
|
|
// P5e (pa): the four frontend program rows belong to the LEGACY key and are not
|
|
// written on the handle arm - decided by `byHandle`, which is the arm, and not by
|
|
// the object happening to be null, which is only a consequence of it. They are
|
|
// zeroed rather than skipped so an entry cannot carry a row nobody wrote.
|
|
memo.programLifetimeId = 0;
|
|
memo.programBackendStateVersion = 0;
|
|
memo.programLinked = false;
|
|
memo.contextGeneration = g_backendContextGeneration;
|
|
memo.drawProgram = drawProgram;
|
|
memo.shaderCsoSerial = shaderCsoSerial;
|
|
memo.bindingsSerial = bindingsSerial;
|
|
memo.samplerViewsSerial = samplerViewsSerial;
|
|
memo.contextSerial = contextSerial;
|
|
std::memcpy(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), shadowBytes);
|
|
memo.valid = true;
|
|
}
|
|
}
|
|
|
|
BindCurrentUnitSamplers(keys);
|
|
}
|
|
|
|
void BindCurrentTextures() {
|
|
// P5e (pa): the exported no-argument entry (DirectGLES.h) takes the SAME arm the hoist in
|
|
// PrepareForDraw does, and for the same reason: the pull is what trips the input check, so
|
|
// an entry point that keeps it would keep the row alive for whichever caller reaches this
|
|
// overload next. Today that is SanityTest's unit cases, which run under monolith
|
|
// transport and therefore take the frontend arm below unchanged.
|
|
const SharedPtr<MG_State::GLState::ProgramObject> noFrontendProgram;
|
|
BindCurrentTextures(TextureImpl::CaptureDrawTextureSyncKeys(), noFrontendProgram);
|
|
}
|
|
|
|
// Binds the current program's backend object and re-establishes its per-program
|
|
// resources: global UBO contents, uniform-block bindings, and sampler uniform
|
|
// units (layout(binding=N) qualifiers are stripped from transpiled ESSL, so the
|
|
// association must be rebuilt through the API). Compute dispatches depend on
|
|
// this as much as draws do — e.g. Flywheel's cull shader reads the
|
|
// _FlwFrameUniforms block and the _flw_depthPyramid sampler.
|
|
//
|
|
// P5e (pa): `currentProgram` IS NULL WHENEVER `handleArm` HOLDS. Every read of it in the
|
|
// body below is on the `:` side of a `handleArm ? <record> :` pick or inside the `else` of
|
|
// `if (handleArm)`, with one pair that is guarded instead - the two `globalConstants ? ... :`
|
|
// reads, which the [[noreturn]] refusal a few lines above them makes unreachable on that arm
|
|
// (its note says so). The parameter stays for the monolith arm, which is ruling ID-81: that
|
|
// build keeps this function's frontend text token for token.
|
|
void BindCurrentProgramWithResources(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
|
const TextureImpl::DrawTextureSyncKeys& keys,
|
|
MG_Pipe::MGPipeHandle programCso) {
|
|
// P5e (pg): THE HANDLE ARM'S GATE, from the record rather than from the object. Hoisted
|
|
// out of the `if` below because the whole body's entry condition is "the program linked
|
|
// and has SPIR-V", and on this arm both answers are fields of the descriptor
|
|
// (Desc.LinkStatus is ID-88's, Desc.SpirvStatus P4a's).
|
|
const MG_Pipe::MGPipeShaderCsoRecord* programRecord = nullptr;
|
|
programRecord = PrgramImpl::FindShaderCsoRecord(programCso);
|
|
if (programRecord != nullptr && (programRecord->Desc.LinkStatus == 0 || programRecord->Desc.SpirvStatus == 0)) {
|
|
programRecord = nullptr;
|
|
}
|
|
const Bool programUsable = programRecord != nullptr;
|
|
if (programUsable) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
PrgramImpl::BackendProgramObjectImpl* twin = nullptr;
|
|
// THE STASH, KEYED ON THE HANDLE (CONTRACT-P5E §5.5). SyncCurrentProgramByHandle
|
|
// wrote it a few lines ago in the same Prepare; the fallback is the by-handle
|
|
// resolver, which is record-first and touches no frontend identity - so unlike
|
|
// the frontend fallback below it is not a registry probe at all.
|
|
twin = PrgramImpl::g_currentDrawProgramHandle == programCso ? PrgramImpl::g_currentDrawBackendProgram
|
|
: nullptr;
|
|
if (!twin) twin = PrgramImpl::ResolveProgramTwin(programCso);
|
|
if (twin) {
|
|
auto& backendProgram = *twin;
|
|
backendProgram.Use();
|
|
|
|
// Global UBO: block index and binding-point assignment are cached at
|
|
// link time (CacheResourceLocations); re-upload only when the CPU shadow
|
|
// actually changed since the last upload for this program.
|
|
// P5e (pg): the block's SIZE is Desc.GlobalUboSize on the handle arm. It is the
|
|
// same number GetUBOSize() answers - the emitter takes it from there - but it is
|
|
// the record's copy, which is the point.
|
|
const Uint globalUboSize = programRecord->Desc.GlobalUboSize;
|
|
if (globalUboSize > 0 && backendProgram.HasGlobalUboBlock()) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC("UpdateGlobalUBO", TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// P4a e4 (D-H6): set_global_constants' counterpart. The version and the
|
|
// block image come off the ShaderCso record when it carries them; the UBO
|
|
// RING BELOW IS UNTOUCHED - it is on the Espryt do-not-touch list, keeps
|
|
// its {contentVersion, ringGeneration, frameSerial, offset} allocation
|
|
// key, keeps its glBufferSubData fallback and keeps accounting to
|
|
// ByteClass::StageUboGlobal. What changes is where the version and the
|
|
// bytes come from, and nothing else.
|
|
//
|
|
// The record covers the DEFAULT UNIFORM BLOCK ONLY. Named UBOs are the
|
|
// block right after this one and they are P4b's (dirty bits 15-17), which
|
|
// is why nothing here touches them.
|
|
const MG_Pipe::MGPipeShaderCsoRecord* const globalConstants =
|
|
PrgramImpl::ResolveGlobalConstantsRecordForHandle(programCso, globalUboSize);
|
|
// P5e (pg), gap G-C: `MapUBO()` HAS NO RUN-AHEAD ANSWER. It returns
|
|
// ProgramObject::globalUboScratch - the live CPU array every glUniform*
|
|
// writes into - so under a transport it is a torn read of client memory by
|
|
// construction, not a read that happens to race. Every reason
|
|
// ResolveGlobalConstantsRecord* can decline is a SEAM DEFECT here (bit 12
|
|
// off, the mis-keyed descriptor, the never-uploaded sentinel, a short block
|
|
// image), and each of them has already said so by name one level down; what
|
|
// this adds is that on the handle arm there is nothing legal to fall back
|
|
// to. CONTRACT-P5E §5.5: Fatal{UnmigratedVerb, "set_global_constants"}, not
|
|
// a torn upload.
|
|
//
|
|
// P5e (pa): AND IT IS ALSO WHAT KEEPS THE TWO `globalConstants ? ... :
|
|
// currentProgram->...` READS BELOW OFF THE FRONTEND. std::abort() is
|
|
// [[noreturn]], so past this statement `handleArm` implies
|
|
// `globalConstants != nullptr` in one step and in this function - not by a
|
|
// chain through other files, which is the distinction ID-110 draws. It
|
|
// matters now: pa's caller passes a NULL ProgramObject on that arm, so the
|
|
// `:` sides are unreachable rather than merely unused.
|
|
if (globalConstants == nullptr) {
|
|
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"set_global_constants\"} - the "
|
|
"ShaderCso record {%u, %u} does not answer the default uniform "
|
|
"block for a draw of a program that declares %u bytes of it, and "
|
|
"MapUBO() is the client's live scratch array",
|
|
programCso.Slot, programCso.Gen, globalUboSize);
|
|
std::abort();
|
|
}
|
|
const Uint32 uboContentVersion = globalConstants ? globalConstants->GlobalConstantsVersion
|
|
: currentProgram->GetUBOContentVersion();
|
|
const SizeT uboSize = static_cast<SizeT>(globalUboSize);
|
|
// Substituted at the two upload sites rather than hoisted into a local:
|
|
// MapUBO() is called there today and only there, and hoisting it would
|
|
// call it on paths that skip the upload entirely.
|
|
#define MGB_UBO_BYTES \
|
|
(globalConstants ? static_cast<const void*>(globalConstants->GlobalConstants.data()) \
|
|
: static_cast<const void*>(currentProgram->MapUBO()))
|
|
// Preferred path: write changed contents into a fresh slot of the
|
|
// shared persistent-mapped ring and bind it as a range. The GPU
|
|
// never reads bytes the CPU is writing, so the driver has no
|
|
// write-after-read hazard to resolve — the in-place glBufferSubData
|
|
// below forced Adreno into a ghost/stall on every uniform-dirtying
|
|
// draw (MC dirties uniforms every draw), which dominated frame time.
|
|
Bool ringBound = false;
|
|
{
|
|
const SizeT bindSize =
|
|
std::max(uboSize, static_cast<SizeT>(backendProgram.GetGlobalUboBackendBlockSize()));
|
|
auto& ringSlot = backendProgram.GetGlobalUboRingAllocation();
|
|
void* write = nullptr;
|
|
Uint ringId = 0;
|
|
const auto state = BufferImpl::UboRingPrepareSlot(
|
|
ringSlot, uboContentVersion, CurrentFrameSerial(), bindSize, write, ringId);
|
|
if (state == BufferImpl::UboRingSlotState::Allocated) {
|
|
std::memcpy(write, MGB_UBO_BYTES, uboSize);
|
|
if (MG_Util::PipeStats::Enabled()) {
|
|
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal,
|
|
static_cast<Uint64>(uboSize));
|
|
}
|
|
}
|
|
if (state == BufferImpl::UboRingSlotState::Allocated ||
|
|
state == BufferImpl::UboRingSlotState::Reused) {
|
|
BufferImpl::BindBufferRangeCached(GL_UNIFORM_BUFFER, 0, ringId,
|
|
static_cast<GLintptr>(ringSlot.offset),
|
|
static_cast<GLsizeiptr>(bindSize));
|
|
ringBound = true;
|
|
}
|
|
}
|
|
if (!ringBound) {
|
|
// Fallback (no EXT_buffer_storage / fences, or ring creation
|
|
// failed): the original in-place upload.
|
|
if (backendProgram.GetLastUploadedGlobalUboVersion() != uboContentVersion) {
|
|
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, backendProgram.GetBackendGlobalUBOId());
|
|
g_GLESFuncs.glBufferSubData(GL_UNIFORM_BUFFER, 0, globalUboSize, MGB_UBO_BYTES);
|
|
if (MG_Util::PipeStats::Enabled()) {
|
|
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal,
|
|
static_cast<Uint64>(globalUboSize));
|
|
}
|
|
g_GLESFuncs.glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
|
backendProgram.SetLastUploadedGlobalUboVersion(uboContentVersion);
|
|
}
|
|
BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, 0, backendProgram.GetBackendGlobalUBOId());
|
|
}
|
|
#undef MGB_UBO_BYTES
|
|
}
|
|
|
|
{
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC("UpdateUBO", TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// Normal UBOs: backend block indices and glUniformBlockBinding
|
|
// assignments are cached at link time; per draw only the buffer
|
|
// bindings are re-established (they follow frontend binding points).
|
|
const auto& blockBackendIndices = backendProgram.GetUniformBlockBackendIndices();
|
|
const auto uboCount = static_cast<Int>(blockBackendIndices.size());
|
|
// Pre-loop epoch read; the per-resource drawCleanEpoch stamps below
|
|
// follow the same stamp-the-pre-read-value rule as the VAO memo
|
|
// (Managers.h documents the contract and the mutation-site list).
|
|
const Uint64 bufferEpoch = uboCount > 0 ? BufferImpl::CurrentBufferMutationEpoch() : 0;
|
|
Uint lastUBOBinding = 0; // binding 0 is reserved for the global UBO
|
|
for (Int i = 0; i < uboCount; ++i) {
|
|
++lastUBOBinding;
|
|
if (blockBackendIndices[i] < 0) {
|
|
continue;
|
|
}
|
|
|
|
// Connect buffer to backend binding point.
|
|
//
|
|
// P5e (pg): glUniformBlockBinding MOVES THIS AFTER THE LINK, so it is
|
|
// one of the three post-link mutable reflection fields the archive alone
|
|
// cannot answer - set_program_bindings' first tail carries it, dense in
|
|
// this very index space. The BINDING POINT it names is sb's row (the
|
|
// GetBufferBindingPoint below); this line is the boundary S4 R5 named.
|
|
auto binding = PrgramImpl::ProgramBlockBindingFromRecord(*programRecord, i);
|
|
// P5e (sb, §5.6): THE POINT HALF, by record. `binding` above is the
|
|
// PROGRAM's block binding and stays exactly where it is (it is package
|
|
// pg's row); what moves is the point it indexes. The index space is the
|
|
// same one - Start is 0 by contract - so `binding` addresses the
|
|
// applier's window directly, and a binding at or above Count means
|
|
// "nothing bound", which is what the frontend array's default said too.
|
|
const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
|
|
const Uint32 windowStart = st.ShaderBufferStart[MG_Pipe::kMGPipeShaderBufferClassUniform];
|
|
const SizeT windowEnd = static_cast<SizeT>(windowStart) +
|
|
st.ShaderBufferCount[MG_Pipe::kMGPipeShaderBufferClassUniform];
|
|
if (static_cast<SizeT>(binding) < windowStart || static_cast<SizeT>(binding) >= windowEnd) {
|
|
continue; // nothing bound there - the frontend arm's `if (bufferObj)`
|
|
}
|
|
const MG_Pipe::MGPBufferRange& entry =
|
|
st.BoundShaderBuffers[MG_Pipe::kMGPipeShaderBufferClassUniform]
|
|
[static_cast<SizeT>(binding)];
|
|
if (MG_Pipe::MGPipeHandleIsNull(entry.Res)) continue;
|
|
// The clean probe is the SAME five questions IsBufferDrawClean asks
|
|
// the frontend object, asked by handle: the twin's identity, the
|
|
// resource record's Serial against the twin's synced one, and the
|
|
// pending-work sets - all server-owned (D-A4). The drawCleanEpoch
|
|
// short-circuit is unchanged; it was never the point read.
|
|
auto* backendResource = BufferImpl::FindBufferResourceForHandle(entry.Res);
|
|
if (!backendResource || backendResource->drawCleanEpoch != bufferEpoch) {
|
|
if (backendResource &&
|
|
BufferImpl::IsBufferDrawCleanByHandle(entry.Res, backendResource, nullptr)) {
|
|
backendResource->drawCleanEpoch = bufferEpoch;
|
|
} else {
|
|
backendResource = BufferImpl::EnsureBufferResourceForHandle(nullptr, entry.Res);
|
|
}
|
|
}
|
|
if (backendResource && backendResource->id != 0) {
|
|
// WHOLE-VS-RANGE IS THE RECORD's TEST, not `range.end == 0`: a
|
|
// base binding travels as kMGPipeWholeBuffer precisely so the
|
|
// extent is re-resolved HERE, against the storage this server
|
|
// holds, rather than frozen at the client's emission.
|
|
if (entry.Offset == 0 && entry.Size == MG_Pipe::kMGPipeWholeBuffer) {
|
|
BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, lastUBOBinding,
|
|
backendResource->id);
|
|
} else {
|
|
const SizeT storage = backendResource->storageSize;
|
|
const SizeT rangeStart = std::min<SizeT>(static_cast<SizeT>(entry.Offset), storage);
|
|
const SizeT rangeEnd =
|
|
std::min<SizeT>(static_cast<SizeT>(entry.Offset + entry.Size), storage);
|
|
BufferImpl::BindBufferRangeCached(
|
|
GL_UNIFORM_BUFFER, lastUBOBinding, backendResource->id,
|
|
static_cast<GLintptr>(rangeStart), static_cast<GLsizeiptr>(rangeEnd - rangeStart));
|
|
}
|
|
} else {
|
|
MGLOG_E_ONCE("No backend buffer found for UBO binding, cannot bind UBO.");
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Atomic counter buffers. Bound here rather than beside the storage-buffer sync
|
|
// in SyncNeccessaryBuffers because the reserved slot the transpiled ESSL reads
|
|
// them at is PROGRAM state: it is `top - GL binding` for the counter blocks THIS
|
|
// program declares, and no other program's blocks live there. Both the draw and
|
|
// the dispatch path reach this, which is what a compute-shader counter needs.
|
|
if (!backendProgram.GetAtomicCounterBindings().empty()) {
|
|
BufferImpl::SyncAtomicCounterBuffers(backendProgram.GetAtomicCounterBindings(),
|
|
backendProgram.GetAtomicCounterEsslBindingTop());
|
|
}
|
|
|
|
{
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedNC("BindSamplerUnit", TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
// Sampler unit binding: backend locations are cached at link time;
|
|
// glUniform1i is program state, so it is only re-issued when the
|
|
// frontend-assigned unit differs from what this program last saw.
|
|
//
|
|
// The whole pass sits behind the per-twin SamplerPassMemo (see its
|
|
// declaration for the invalidation enumeration): while the keys hold
|
|
// and every previously touched unit's sampler-shadow row is exactly
|
|
// what this pass last left there, re-running it is a provable no-op.
|
|
auto& samplerPassMemo = backendProgram.GetSamplerPassMemo();
|
|
// P5e (pg), CONTRACT-P5E §5.5: THE SAMPLER PASS MEMO KEYS ON BindingsSerial
|
|
// on the handle arm. m_backendStateVersion is a frontend counter read on
|
|
// every draw (kimi row 105); what it is standing in for here is "has a
|
|
// sampler uniform's unit moved", and that is exactly what an applied
|
|
// set_program_bindings means. Truncated to the memo's Uint32 field, which is
|
|
// safe for a memo key - it is a change detector, not an ordering - and the
|
|
// record's serial would need four billion binding records to wrap.
|
|
const Uint32 programBackendStateVersion = static_cast<Uint32>(programRecord->BindingsSerial);
|
|
Bool samplerPassClean = samplerPassMemo.valid && samplerPassMemo.contextId == keys.contextId &&
|
|
samplerPassMemo.unitBindingsEpoch == keys.unitBindingsEpoch &&
|
|
samplerPassMemo.samplingGeneration == keys.samplingGeneration &&
|
|
samplerPassMemo.backendStateVersion == programBackendStateVersion &&
|
|
samplerPassMemo.textureContextGeneration == g_backendContextGeneration;
|
|
if (samplerPassClean) {
|
|
for (Uint i = 0; i < samplerPassMemo.count; ++i) {
|
|
if (SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]] !=
|
|
samplerPassMemo.rows[i]) {
|
|
samplerPassClean = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!samplerPassClean) {
|
|
constexpr SizeT kMaxMemoEntries =
|
|
PrgramImpl::BackendProgramObjectImpl::SamplerPassMemo::kMaxEntries;
|
|
samplerPassMemo.valid = false;
|
|
samplerPassMemo.count = 0;
|
|
Bool memoisable = true;
|
|
for (auto& samplerBinding : backendProgram.GetSamplerUniformBindings()) {
|
|
// P5e (pg): the unit glUniform1i put on this sampler - the second
|
|
// of the three post-link mutable fields, carried sparse and ascending
|
|
// by location in set_program_bindings' second tail.
|
|
const auto unit = PrgramImpl::ProgramSamplerUnitFromRecord(*programRecord,
|
|
samplerBinding.frontendLocation);
|
|
if (unit == -1) continue;
|
|
// Record the touched unit for the memo; a pass touching more
|
|
// units than the memo can carry (or an out-of-range unit)
|
|
// simply never memoises.
|
|
if (memoisable && samplerPassMemo.count < kMaxMemoEntries && unit >= 0 &&
|
|
unit < static_cast<Int>(SamplerImpl::g_boundSamplersCache.size())) {
|
|
samplerPassMemo.units[samplerPassMemo.count++] = static_cast<Uint8>(unit);
|
|
} else {
|
|
memoisable = false;
|
|
}
|
|
if (samplerBinding.lastAssignedUnit != unit) {
|
|
g_GLESFuncs.glUniform1i(samplerBinding.backendLocation, unit);
|
|
samplerBinding.lastAssignedUnit = unit;
|
|
}
|
|
|
|
// P5e (tx2), CONTRACT-P5E §5.3. THE SAMPLER PASS, FROM RECORDS.
|
|
//
|
|
// Four frontend reads used to live in this loop body and every one of
|
|
// them has a carrier: the unit's sampler object is
|
|
// st.BoundSamplerStates[u] (null = the texture's built-in sampler, which
|
|
// is TextureResources[view.Texture].Params.BuiltinSampler); its fifteen
|
|
// values - lodBias among them - are the CSO record's
|
|
// (MGPipeValueTypes.h); and the sampled texture's FORMAT, which decides
|
|
// the raw-depth-fetch substitution, is
|
|
// SamplerViewCsos[View].View.InternalFormat.
|
|
//
|
|
// THE SUBSTITUTION ITSELF STAYS ON THE SERVER (ARCHITECTURE.md §5.5
|
|
// assigns it there by name); what moves is where its two inputs come
|
|
// from.
|
|
const auto& st = MG_Pipe::MGPipeApplier();
|
|
const auto index = static_cast<Uint32>(unit);
|
|
const Bool inViewWindow = index >= st.SamplerViewStart &&
|
|
index - st.SamplerViewStart < st.SamplerViewCount &&
|
|
index < st.BoundSamplerViews.size();
|
|
const MG_Pipe::MGPBoundView view =
|
|
inViewWindow ? st.BoundSamplerViews[index] : MG_Pipe::MGPBoundView{};
|
|
const Bool inStateWindow = index >= st.SamplerStateStart &&
|
|
index - st.SamplerStateStart < st.SamplerStateCount &&
|
|
index < st.BoundSamplerStates.size();
|
|
const MG_Pipe::MGPipeHandle unitSampler =
|
|
inStateWindow ? st.BoundSamplerStates[index] : MG_Pipe::kMGPipeNullHandle;
|
|
|
|
// The EFFECTIVE sampler of the unit: the bound CSO if there is one,
|
|
// else the sampled texture's built-in sampler CSO - exactly the
|
|
// override order GL states and the pre-handle body spells with
|
|
// `samplerObject ? ... : texture->GetSamplerObject()`.
|
|
MG_Pipe::MGPipeHandle effectiveCso = unitSampler;
|
|
if (MG_Pipe::MGPipeHandleIsNull(effectiveCso) &&
|
|
!MG_Pipe::MGPipeHandleIsNull(view.Texture)) {
|
|
if (const auto* texRecord = PipeTextureRecordForHandle(view.Texture)) {
|
|
effectiveCso = texRecord->Params.BuiltinSampler;
|
|
}
|
|
}
|
|
const MG_Pipe::MGPipeSamplerCsoRecord* effectiveCsoRecord =
|
|
PipeSamplerCsoRecordForHandle(effectiveCso);
|
|
|
|
if (samplerBinding.lodBiasLocation >= 0) {
|
|
const Float lodBias =
|
|
effectiveCsoRecord != nullptr ? effectiveCsoRecord->Params.lodBias : 0.0f;
|
|
if (lodBias != samplerBinding.lastAssignedLodBias) {
|
|
g_GLESFuncs.glUniform1f(samplerBinding.lodBiasLocation, lodBias);
|
|
samplerBinding.lastAssignedLodBias = lodBias;
|
|
}
|
|
}
|
|
|
|
// The last sample a usampler2DMS fetch may name, from the texture
|
|
// actually bound: a stencil texture's real count is not an integer
|
|
// color texture's. Keyed like the bias above - a texture's storage or
|
|
// a unit's binding moving is what invalidates this pass's memo.
|
|
if (samplerBinding.msSampleMaxLocation >= 0) {
|
|
const Int sampleMax =
|
|
std::max(PipeTextureBackendSampleCountForHandle(view.Texture), 1) - 1;
|
|
if (sampleMax != samplerBinding.lastAssignedMsSampleMax) {
|
|
g_GLESFuncs.glUniform1i(samplerBinding.msSampleMaxLocation, sampleMax);
|
|
samplerBinding.lastAssignedMsSampleMax = sampleMax;
|
|
}
|
|
}
|
|
|
|
const auto* viewRecord = PipeSamplerViewRecordForHandle(view.View);
|
|
const Bool sampledIsTexture2D =
|
|
viewRecord != nullptr &&
|
|
static_cast<TextureTarget>(viewRecord->View.Target) == TextureTarget::Texture2D;
|
|
if (samplerBinding.uniformType == GL_SAMPLER_2D && sampledIsTexture2D &&
|
|
effectiveCsoRecord != nullptr &&
|
|
NeedsRawDepthFetchSampler(
|
|
effectiveCsoRecord->Params,
|
|
static_cast<TextureInternalFormat>(viewRecord->View.InternalFormat))) {
|
|
GetRawDepthFetchSampler()->Bind(unit);
|
|
MGLOG_D("Using raw depth fetch sampler on unit %d.", unit);
|
|
} else if (!MG_Pipe::MGPipeHandleIsNull(unitSampler)) {
|
|
// THE MINT IS GONE (§4.2): the unit's sampler is the CSO's own
|
|
// twin, the same one BindCurrentUnitSamplers binds, so the two
|
|
// arms cannot hand a unit back and forth between two driver
|
|
// samplers - and there is no arm left that creates a twin from a
|
|
// frontend SamplerObject.
|
|
if (auto* csoTwin = SamplerImpl::ResolveSamplerCsoTwin(unitSampler)) {
|
|
csoTwin->Bind(unit);
|
|
}
|
|
} else {
|
|
SamplerImpl::UnbindSampler(unit);
|
|
}
|
|
continue;
|
|
}
|
|
if (memoisable) {
|
|
// Snapshot the rows AFTER the pass: they are exactly what a
|
|
// clean replay must find untouched.
|
|
for (Uint i = 0; i < samplerPassMemo.count; ++i) {
|
|
samplerPassMemo.rows[i] = SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]];
|
|
}
|
|
samplerPassMemo.contextId = keys.contextId;
|
|
samplerPassMemo.unitBindingsEpoch = keys.unitBindingsEpoch;
|
|
samplerPassMemo.samplingGeneration = keys.samplingGeneration;
|
|
samplerPassMemo.backendStateVersion = programBackendStateVersion;
|
|
samplerPassMemo.textureContextGeneration = g_backendContextGeneration;
|
|
samplerPassMemo.valid = true;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
g_GLESFuncs.glUseProgram(0);
|
|
PrgramImpl::g_lastUsedBackendProgramId = 0;
|
|
MGLOG_E_ONCE("No backend program found (maybe not synced) for current program, cannot use program.");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Raw pointer: only ever used inside one GL entry point after PrepareForDraw/
|
|
// PrepareForCompute, where the current program (and therefore its registry twin)
|
|
// is pinned for the duration. Prefers the per-draw stash those preparations wrote.
|
|
PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() {
|
|
// P5e (pg), kimi row 107: THE PER-SUB-DRAW ROW. This is reached once per indirect
|
|
// sub-draw for the gl_DrawID / gl_BaseVertex / gl_BaseInstance uniforms, and before P5e
|
|
// every one of those pulled GetProgramForDraw() and, on a stash miss, probed the client
|
|
// allocator. On the handle arm the stash key is the applier's own DrawProgram and the
|
|
// miss path is the by-handle resolver, which is record-first and names no frontend
|
|
// identity - so the whole entry point leaves the frontend.
|
|
const MG_Pipe::MGPipeHandle cso = MG_Pipe::MGPipeApplier().DrawProgram;
|
|
if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr;
|
|
const auto* const record = PrgramImpl::FindShaderCsoRecord(cso);
|
|
if (record == nullptr || record->Desc.LinkStatus == 0 || record->Desc.SpirvStatus == 0) {
|
|
return nullptr;
|
|
}
|
|
if (PrgramImpl::g_currentDrawProgramHandle == cso) {
|
|
return PrgramImpl::g_currentDrawBackendProgram;
|
|
}
|
|
return PrgramImpl::ResolveProgramTwin(cso);
|
|
}
|
|
|
|
void SetCurrentBaseInstance(Uint32 baseInstance) {
|
|
if (const auto program = GetCurrentBackendProgram()) {
|
|
program->SetBaseInstance(baseInstance);
|
|
}
|
|
}
|
|
|
|
void SetCurrentDrawID(Uint32 drawId) {
|
|
if (const auto program = GetCurrentBackendProgram()) {
|
|
program->SetDrawID(drawId);
|
|
}
|
|
}
|
|
|
|
void SetCurrentBaseVertex(Int32 baseVertex) {
|
|
if (const auto program = GetCurrentBackendProgram()) {
|
|
program->SetBaseVertex(baseVertex);
|
|
}
|
|
}
|
|
|
|
Bool CurrentProgramReadsDrawID() {
|
|
const auto program = GetCurrentBackendProgram();
|
|
return program != nullptr && program->ReadsDrawID();
|
|
}
|
|
|
|
Bool CurrentProgramReadsBaseVertex() {
|
|
const auto program = GetCurrentBackendProgram();
|
|
return program != nullptr && program->ReadsBaseVertex();
|
|
}
|
|
|
|
// The two questions above, asked from BEFORE PrepareForDraw - where neither can be
|
|
// answered honestly. GetCurrentBackendProgram only sees a twin that a previous draw
|
|
// already synced, and a twin from before a relink still carries the previous link's
|
|
// uniform locations, so "no" there means "not known yet" at least as often as it
|
|
// means no. The multi-draw compute tier has to decide whether to flatten a batch
|
|
// before PrepareForDraw runs (its dispatch cannot come after the draw state), and
|
|
// flattening a batch that turns out to need per-sub-draw values is unrecoverable -
|
|
// so an unanswerable program counts as needing them.
|
|
Bool CurrentProgramMayNeedPerSubDrawBuiltins(Bool batchCarriesBaseVertices) {
|
|
const auto program = GetCurrentBackendProgram();
|
|
if (program == nullptr) return true;
|
|
// P5e (pg), kimi row 108: the same "is this twin current" question, from the record's
|
|
// serial instead of the frontend's link version. The ANSWER TO AN UNANSWERABLE PROGRAM
|
|
// IS STILL `true` - a flattened batch that turns out to need per-sub-draw values is
|
|
// unrecoverable, so "not known yet" has to count as "may need them", and a missing
|
|
// record is the most not-known-yet a program can be.
|
|
const auto* const record = PrgramImpl::FindShaderCsoRecord(MG_Pipe::MGPipeApplier().DrawProgram);
|
|
if (record == nullptr || program->GetSyncedShaderCsoSerial() != record->Serial) {
|
|
return true;
|
|
}
|
|
return program->ReadsDrawID() || (batchCarriesBaseVertices && program->ReadsBaseVertex());
|
|
}
|
|
} // namespace MobileGL::MG_Backend::DirectGLES
|