[Refactor] (Espryt): drive the driver VAO from the pushed vertex-elements record and retire the wrapping index-slot version and its identity patch

This commit is contained in:
2026-09-08 04:52:15 -04:00
parent ce24e2a734
commit cd05de504e
3 changed files with 640 additions and 4 deletions
+134 -3
View File
@@ -17,6 +17,10 @@
#include <MG_Util/Metrics/TextureMetrics.h>
#include <MG_State/GLState/Core.h>
#include <MG_Pipe/PipeInputsSwitch.h>
#if MOBILEGL_PIPE_PUSH
// P3a: the applier's vertex-input records the re-keyed draw-buffer memo is validated against.
#include <MG_Pipe/PipeApply.h>
#endif
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
@@ -574,6 +578,93 @@ namespace MobileGL::MG_Backend::DirectGLES {
BindBufferId(glTarget, backendResource->id);
}
#if MOBILEGL_PIPE_PUSH
// The handle arm of the resolved-draw-buffers memo (D-G4). Two substitutions and
// nothing else:
//
// validity the frontend VAO's wrapping configuration version is replaced by the
// bound vertex-elements CSO ({slot, gen} AND its server-owned content
// serial) plus the vertex-buffer set's own serial - so an emission the
// suppressor let through is what re-opens the memo, not a counter the
// backend reads out of the frontend;
// identity an entry names its resource by handle and the clean probe compares that,
// instead of a raw frontend address that a successor object can reproduce.
//
// The WALK itself still reads the frontend VAO's attributes, and deliberately: the
// buffers a draw needs ensured is a pull site P3a does not migrate (dirty bits 15-17
// are P4b's and the resolution move is P8's), and EnsureBufferResource still owes
// BufferObject::SyncPersistentMappedRange one call (D-N).
void SyncVaoAttributeBuffersByHandle(const SharedPtr<MG_State::GLState::VertexArrayObject>& currentVAOObject,
VertexArrayImpl::BackendVertexArrayObject::ResolvedDrawBuffers* memo,
Uint64 bufferEpoch) {
const auto& st = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPipeHandle elements = st.BoundVertexElements;
Uint64 elementsSerial = 0;
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;
}
const Uint64 buffersSerial = st.VertexBuffersSerial;
if (memo && memo->valid && 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];
if (IsBufferDrawCleanByHandle(entry.handle, entry.resource)) continue;
allClean = false;
entry.resource =
EnsureBufferResource(currentVAOObject->GetAttribute(entry.attribIndex).Buffer);
}
memo->vboCleanEpoch = allClean ? bufferEpoch : 0;
}
return;
}
// Full walk, once per distinct buffer, rebuilding the memo as it goes.
MG_State::GLState::BufferObject* syncedBuffers[MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS];
Uint syncedBufferCount = 0;
const auto& allAttributes = currentVAOObject->GetAllAttributes();
for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) {
const auto& attrib = allAttributes[attribIndex];
if (!attrib.Enabled) continue;
const auto& bufferObject = attrib.Buffer;
if (!bufferObject) continue;
auto* const bufferKey = bufferObject.get();
Bool alreadySynced = false;
for (Uint i = 0; i < syncedBufferCount; ++i) {
if (syncedBuffers[i] == bufferKey) {
alreadySynced = true;
break;
}
}
if (alreadySynced) continue;
auto* resource = EnsureBufferResource(bufferObject);
if (memo) {
auto& entry = memo->entries[syncedBufferCount];
entry.frontend = bufferKey;
entry.attribIndex = static_cast<Uint8>(attribIndex);
entry.resource = resource;
entry.handle = HandleOfBuffer(bufferKey);
}
syncedBuffers[syncedBufferCount++] = bufferKey;
}
if (memo) {
memo->count = syncedBufferCount;
memo->elementsHandle = elements;
memo->elementsSerial = elementsSerial;
memo->buffersSerial = buffersSerial;
memo->valid = true;
// Rebuilt via EnsureBufferResource, not probed clean: the next probe pass
// stamps the epoch.
memo->vboCleanEpoch = 0;
}
}
#endif // MOBILEGL_PIPE_PUSH
// `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
@@ -613,6 +704,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Uint64 bufferEpoch = CurrentBufferMutationEpoch();
auto* memo = vaoTwin ? &vaoTwin->GetResolvedDrawBuffersMemo() : nullptr;
const Uint32 configVersion = vaoConfigVersion;
#if MOBILEGL_PIPE_PUSH
if (VertexInputSubsystemEnabled()) {
SyncVaoAttributeBuffersByHandle(currentVAOObject, memo, bufferEpoch);
} else
#endif
if (memo && memo->valid && memo->configVersion == configVersion) {
if (memo->vboCleanEpoch != bufferEpoch) {
Bool allClean = true;
@@ -683,6 +779,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
// rebind another buffer with no epoch (and no config-version) move,
// so the identity compare always runs; only the clean PROBE is
// elided while the stamp holds.
#if MOBILEGL_PIPE_PUSH
if (ResourceSubsystemEnabled()) {
// Same three cases, with the identity re-keyed off the raw frontend
// address onto the resource's {slot, gen} (D-G4).
const MG_Pipe::MGPipeHandle iboHandle = HandleOfBuffer(possibleIBO.get());
if (memo && memo->iboHandle == iboHandle && memo->iboCleanEpoch == bufferEpoch) {
// probed fully clean at this epoch; nothing can have dirtied it
} else if (memo && memo->iboHandle == iboHandle &&
IsBufferDrawCleanByHandle(iboHandle, memo->iboResource)) {
memo->iboCleanEpoch = bufferEpoch;
} else {
auto* resource = EnsureBufferResource(possibleIBO);
if (memo) {
memo->iboHandle = iboHandle;
memo->iboFrontend = possibleIBO.get();
memo->iboResource = resource;
// Repaired, not probed clean: stamp on the next clean probe.
memo->iboCleanEpoch = 0;
}
}
} else
#endif
if (memo && memo->iboFrontend == possibleIBO.get() && memo->iboCleanEpoch == bufferEpoch) {
// probed fully clean at this epoch; nothing can have dirtied it
} else if (memo && memo->iboFrontend == possibleIBO.get() &&
@@ -5140,14 +5258,27 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The emulated shift has to be in place before PrepareForDraw, because that is what syncs the
// VAO; a zero here is what un-shifts the arrays for the next ordinary draw.
//
// P3a (D-H2): this is the LEGACY arm's carrier. On the handle arm the draw's raw base
// instance rides in MGPVertexBuffers::BaseInstance - a ContentHash input, so a base-instance
// change that moves no buffer is still emitted rather than suppressed - and the server
// decides whether to shift, because emulation ownership is the server's. The scopes below
// are therefore compiled only where the pre-handle arm is (a pull build always).
inline Uint32 EmulatedFetchBaseInstance(GLuint baseinstance) {
return UseNativeBaseInstance() ? 0u : static_cast<Uint32>(baseinstance);
}
#if MOBILEGL_PIPE_LEGACY_MEMOS
#define MGL_SCOPED_FETCH_BASE_INSTANCE(name, value) \
const VertexArrayImpl::ScopedFetchBaseInstance name(value)
#else
#define MGL_SCOPED_FETCH_BASE_INSTANCE(name, value) ((void)(value))
#endif
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex, GLuint baseinstance) {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
MGL_SCOPED_FETCH_BASE_INSTANCE(fetchScope, EmulatedFetchBaseInstance(baseinstance));
PrepareForDraw(syncBit);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
@@ -5184,7 +5315,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLuint baseinstance) {
DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing;
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
MGL_SCOPED_FETCH_BASE_INSTANCE(fetchScope, EmulatedFetchBaseInstance(baseinstance));
PrepareForDraw(syncBit);
const ScopedRestartIndexSubstitution restart(type, count, indices);
if (!restart.DrawIsValid()) return;
@@ -5236,7 +5367,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance) {
DrawSyncFlags syncBit = DrawSyncBit::Instancing;
const VertexArrayImpl::ScopedFetchBaseInstance fetchScope(EmulatedFetchBaseInstance(baseinstance));
MGL_SCOPED_FETCH_BASE_INSTANCE(fetchScope, EmulatedFetchBaseInstance(baseinstance));
PrepareForDraw(syncBit);
SetCurrentBaseInstance(baseinstance);
ForEachViewportRoutingPass([&] {
+417 -1
View File
@@ -2242,6 +2242,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
bufferObject->GetLifetimeId());
}
SizeT ResourceWidthForHandle(MG_Pipe::MGPipeHandle res) { return ResourceWidthOf(res); }
void MarkBufferGpuWritten(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject) {
if (!bufferObject) return;
if (!ResourceSubsystemEnabled()) {
@@ -3309,8 +3311,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_GLESFuncs.glVertexBindingDivisor != nullptr;
}
#if MOBILEGL_PIPE_LEGACY_MEMOS
// Draw state, not VAO state: set by the baseInstance draw entry points around
// PrepareForDraw and back to zero as soon as the draw is issued.
// PrepareForDraw and back to zero as soon as the draw is issued. The legacy arm's
// carrier; the handle arm's is MGPipeApplierState::VertexFetchBaseInstance (D-H2).
Uint32 g_pendingFetchBaseInstance = 0;
void SetPendingFetchBaseInstance(Uint32 baseInstance) {
@@ -3320,6 +3324,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint32 GetPendingFetchBaseInstance() {
return g_pendingFetchBaseInstance;
}
#endif
#if MOBILEGL_PIPE_PUSH
Bool BackendUsesNativeBaseInstance() { return g_GLESCapabilities.SupportsBaseInstance; }
#endif
// The "+ baseInstance" of GL's instanced-array element index, expressed as a byte shift
// of the array's own offset. Only divisor'd arrays step per instance, so only they move.
@@ -3372,11 +3381,123 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
#if MOBILEGL_PIPE_PUSH
// ---- the handle arm's small helpers -------------------------------------------
//
// Each one is its object-shaped counterpart above with the frontend reads replaced by
// the two wire views, and nothing else. Divisor is deliberately NOT on the attribute
// view: it is resolved per binding point and rides in MGPVertexBuffer::Divisor, which
// is where glVertexAttribDivisor reads it (D-G2).
// The entry of the applied set that feeds this attribute. The client emits one entry
// per enabled attribute with BindingIndex == the attribute index, so the positional
// slot is the answer in every real record; the scan behind it is what keeps a record
// that numbers its entries differently correct rather than silently misfed.
const MG_Pipe::MGPVertexBuffer* VertexBufferForBindingIndex(const MG_Pipe::MGPipeApplierState& st,
Uint32 bindingIndex) {
if (st.VertexBufferCount == 0) return nullptr;
const Uint32 begin = st.VertexBufferStart;
const Uint32 end = begin + st.VertexBufferCount;
if (bindingIndex >= begin && bindingIndex < end &&
bindingIndex < MG_Pipe::kMGPipeMaxVertexAttribs &&
st.VertexBuffers[bindingIndex].BindingIndex == bindingIndex) {
return &st.VertexBuffers[bindingIndex];
}
for (Uint32 i = begin; i < end && i < MG_Pipe::kMGPipeMaxVertexAttribs; ++i) {
if (st.VertexBuffers[i].BindingIndex == bindingIndex) return &st.VertexBuffers[i];
}
return nullptr;
}
// BaseInstanceByteShift, on the wire views. baseInstance is added to the ELEMENT index,
// so the divisor does not appear here; a resolved stride of zero never advances and the
// arithmetic already yields zero for it.
inline SizeT BaseInstanceByteShiftWire(Int32 stride, Uint32 divisor, Uint32 baseInstance) {
if (baseInstance == 0 || divisor == 0) return 0;
return static_cast<SizeT>(baseInstance) * static_cast<SizeT>(stride);
}
// BindAttributeBuffer, resolving the driver id from the slot table instead of from the
// attribute's SharedPtr. It does NOT ensure storage: on this arm the draw's buffers
// were ensured by SyncNeccessaryBuffers earlier in the same PrepareForDraw, which is
// the one place that still holds the frontend objects (a pull site P4b/P8 own).
inline Bool BindAttributeBufferByHandle(MG_Pipe::MGPipeHandle res) {
if (MG_Pipe::MGPipeHandleIsNull(res)) {
MGLOG_W_ONCE("Attribute has no bound buffer, skipping.");
return false;
}
auto* backendResource = BufferImpl::FindBufferResourceForHandle(res);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer found for attribute's buffer, cannot bind attribute.");
return false;
}
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, backendResource->id);
return true;
}
// SyncZeroStrideAttribute on the wire views: the one spelling that can carry a resolved
// stride of zero, which glVertexAttribPointer's zero means the opposite of.
inline Bool SyncZeroStrideAttributeByHandle(Uint attribIndex, const MGPVertexAttribWire& attrib,
const MG_Pipe::MGPVertexBuffer& binding) {
if (MG_Pipe::MGPipeHandleIsNull(binding.Res)) {
MGLOG_W_ONCE("Zero-stride attribute %u has no bound buffer, skipping.", attribIndex);
return false;
}
auto* backendResource = BufferImpl::FindBufferResourceForHandle(binding.Res);
if (!backendResource || backendResource->id == 0) {
MGLOG_E_ONCE("No backend buffer for zero-stride attribute %u, cannot bind it.", attribIndex);
return false;
}
if (!attrib.IsInteger) {
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : static_cast<GLint>(attrib.Size);
g_GLESFuncs.glVertexAttribFormat(attribIndex, glSize,
MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type)),
attrib.Normalized ? GL_TRUE : GL_FALSE, 0);
} else {
g_GLESFuncs.glVertexAttribIFormat(attribIndex, static_cast<GLint>(attrib.Size),
MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type)),
0);
}
g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex);
// The resolved offset goes on the binding point, not into a relative offset (the
// relative offset is capped by GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET, a buffer
// offset is not). BindBufferId is bypassed deliberately.
g_GLESFuncs.glBindVertexBuffer(attribIndex, backendResource->id,
static_cast<GLintptr>(attrib.Offset), 0);
return true;
}
// The record the applier holds for the bound vertex-elements CSO, or null.
const MG_Pipe::MGPipeVertexElementsRecord* BoundVertexElementsRecord(
const MG_Pipe::MGPipeApplierState& st) {
const MG_Pipe::MGPipeHandle cso = st.BoundVertexElements;
if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr;
if (cso.Slot >= st.VertexElementsCsos.size()) return nullptr;
const auto& record = st.VertexElementsCsos[cso.Slot];
if (!record.Live || record.Gen != cso.Gen) return nullptr;
return &record;
}
#endif // MOBILEGL_PIPE_PUSH
void BackendVertexArrayObject::SyncToBackend(
const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
#if MOBILEGL_PIPE_PUSH
if (BufferImpl::VertexInputSubsystemEnabled()) {
// Everything this arm needs is in the applier's records; the frontend VAO is
// not read at all, which is the whole point of the conversion.
SyncToBackendFromApplier();
return;
}
#endif
#if !MOBILEGL_PIPE_LEGACY_MEMOS
(void)stateVAOObject;
MGLOG_E_ONCE("MGPipe: the vertex-input subsystem bit is clear and MOBILEGL_PIPE_LEGACY_MEMOS=0 "
"removed the pre-handle VAO sync, so this configuration has no arm at all");
return;
#else
if (!stateVAOObject) {
MGLOG_E_ONCE("State VAO object is null, cannot sync to backend.");
return;
@@ -3602,8 +3723,205 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_syncedFetchBaseInstance = fetchBaseInstance;
}
m_syncedBufferIdGeneration = currentBufferIdGeneration;
#endif // MOBILEGL_PIPE_LEGACY_MEMOS
}
#if MOBILEGL_PIPE_PUSH
// The same function, driven by the applier's records (D-G4). Every branch of the walk
// survives - the enable/disable block, the fp64 narrowing with its Adreno disable, the
// zero-stride binding-API path, the attribute-buffer bind, the BGRA refusal probe, the
// pointer/IPointer at the shifted fetch offset and the divisor - and the gate is
// re-keyed onto three server-owned MGGen counters plus the CSO's {slot, gen}:
//
// m_syncedElementsHandle + m_syncedElementsSerial <- config version + the whole
// per-attribute version array
// m_syncedVertexBuffersSerial <- (new) the buffer set's own
// m_syncedIndexSerial <- wrapping Uint16 + identity patch
// m_syncedBufferIdGeneration <- UNCHANGED, server-local, and
// still the only thing that
// catches a driver-id re-mint no
// counter on either side moves
// m_hasConvertedFloat64Attribute <- UNCHANGED: a narrowed stream is
// derived from buffer CONTENT,
// which no serial covers
// m_syncedFetchBaseInstance <- no longer in the gate as a
// DIRTY input of its own (a base
// instance change is a
// ContentHash input and so moves
// VertexBuffersSerial), but kept
// as the record of what was last
// EMITTED, which is what the next
// sync has to correct
void BackendVertexArrayObject::SyncToBackendFromApplier() {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
const auto* rec = BoundVertexElementsRecord(st);
if (rec == nullptr) {
MGLOG_E_ONCE("MGPipe: no vertex-elements record is bound, so the driver VAO cannot be "
"configured - nothing emitted create_vertex_elements/bind_vertex_elements");
return;
}
const Uint64 currentBufferIdGeneration = BufferImpl::g_bufferBackendIdGeneration;
const Bool bufferIdsRemitted = m_syncedBufferIdGeneration != currentBufferIdGeneration;
const Bool attributesDirty = bufferIdsRemitted || !m_hasSyncedElements ||
!(m_syncedElementsHandle == st.BoundVertexElements) ||
m_syncedElementsSerial != rec->ContentSerial ||
m_syncedVertexBuffersSerial != st.VertexBuffersSerial;
const Bool indexBufferDirty = bufferIdsRemitted || m_syncedIndexSerial != st.IndexBufferSerial;
// Emulation is server-owned: the client sends the draw's RAW base instance and never
// learns the answer. Applied here as well as in the applier so the decision is the
// same whichever side resolved it first - it is idempotent.
const Uint32 fetchBaseInstance =
BackendUsesNativeBaseInstance() ? 0u : st.VertexFetchBaseInstance;
const Bool baseInstanceDirty = m_syncedFetchBaseInstance != fetchBaseInstance;
const Bool emitAttributes = attributesDirty || baseInstanceDirty || m_hasConvertedFloat64Attribute;
if (!emitAttributes && !indexBufferDirty) {
return;
}
m_hasConvertedFloat64Attribute = false;
Bind();
const Uint32 attributeCount =
std::min<Uint32>(rec->AttributeCount, MG_Pipe::kMGPipeMaxVertexAttribs);
for (Uint attribIndex = 0; attribIndex < attributeCount && emitAttributes; ++attribIndex) {
const MGPVertexAttribWire& attrib = rec->Attributes[attribIndex];
const MG_Pipe::MGPVertexBuffer* binding =
VertexBufferForBindingIndex(st, attrib.BindingIndex);
const Uint32 divisor = binding != nullptr ? binding->Divisor : 0u;
// The enable/disable block. On this arm there is no per-attribute version to
// compare: the applier's Attributes[] IS what was last pushed, so a moved
// ContentSerial means re-emit and an unchanged one means the early-out above
// already returned.
if (attrib.Enabled) {
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
} else {
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
}
// The fp64 narrowing, verbatim in behaviour including the Adreno workaround:
// when no float32 stream can be built the array is DISABLED rather than left
// enabled with no pointer, which is what the driver turns into a SIGSEGV inside
// the next draw (KHR-GL43.vertex_attrib_binding.basic-input-case4). IsLong is
// carried separately from Type == Float64 on the wire precisely so this test
// can still tell the two apart.
if (attrib.IsLong || attrib.Type == static_cast<Uint32>(DataType::Float64)) {
if (attrib.Enabled && attrib.Type == static_cast<Uint32>(DataType::Float64) &&
binding != nullptr &&
SyncFloat64AttributeAsFloat32ByHandle(attribIndex, attrib, *binding, fetchBaseInstance)) {
m_hasConvertedFloat64Attribute = true;
// Explicit, not redundant: an earlier walk that could not build the
// stream disabled this array.
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
g_GLESFuncs.glVertexAttribDivisor(attribIndex, divisor);
continue;
}
if (attrib.Enabled) {
MGLOG_W_ONCE("DirectGLES: vertex attribute %u is a 64-bit (GL_DOUBLE) array whose source "
"stream could not be narrowed to float32 - disabling the array",
attribIndex);
}
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
continue;
}
if (!attrib.Enabled || binding == nullptr) continue;
// A resolved stride of zero is the binding model's "never advance" and
// glVertexAttribPointer cannot say it - its zero means "tightly packed", i.e.
// the opposite. ES 3.1's binding-point API can.
if (attrib.Stride == 0 && HasVertexBindingApi()) {
if (!SyncZeroStrideAttributeByHandle(attribIndex, attrib, *binding)) {
continue;
}
// No shift here on purpose: a zero stride never advances.
g_GLESFuncs.glVertexBindingDivisor(attribIndex, divisor);
continue;
}
if (!BindAttributeBufferByHandle(binding->Res)) {
continue;
}
// GL_BGRA as a vertex SIZE is desktop-only and ES rejects it, which leaves the
// array ENABLED with no pointer - and the Adreno driver then dereferences null
// inside the next draw (KHR-GL43.vertex_attrib_binding.basic-input-case5). So
// the refusal is observed and the array disabled. Deliberately ONLY this
// format: the per-draw sync must not grow a glGetError round trip for the
// formats real applications use.
const Bool formatMayBeRefused = attrib.IsBgra != 0;
if (formatMayBeRefused) {
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
} // start from a clean slate so the check below is about THIS call
}
const SizeT fetchOffset = static_cast<SizeT>(attrib.Offset) +
BaseInstanceByteShiftWire(attrib.Stride, divisor, fetchBaseInstance);
const GLenum glType = MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type));
if (!attrib.IsInteger) {
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : static_cast<GLint>(attrib.Size);
g_GLESFuncs.glVertexAttribPointer(attribIndex, glSize, glType,
attrib.Normalized ? GL_TRUE : GL_FALSE, attrib.Stride,
(const void*)fetchOffset);
} else {
g_GLESFuncs.glVertexAttribIPointer(attribIndex, static_cast<GLint>(attrib.Size), glType,
attrib.Stride, (const void*)fetchOffset);
}
if (formatMayBeRefused && g_GLESFuncs.glGetError() != GL_NO_ERROR) {
MGLOG_W_ONCE("DirectGLES: the driver refused the vertex format of attribute %u "
"(size=%d bgra=%d type=%s) - disabling the array so the draw cannot "
"fetch through a pointer the driver never accepted",
attribIndex, static_cast<int>(attrib.Size), attrib.IsBgra ? 1 : 0,
MG_Util::ConvertGLEnumToString(glType).c_str());
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
continue;
}
g_GLESFuncs.glVertexAttribDivisor(attribIndex, divisor);
}
if (indexBufferDirty) {
Bool indexBufferSynced = false;
if (!MG_Pipe::MGPipeHandleIsNull(st.IndexBuffer.Res)) {
auto* backendResource = BufferImpl::FindBufferResourceForHandle(st.IndexBuffer.Res);
if (backendResource && backendResource->id != 0) {
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, backendResource->id);
indexBufferSynced = true;
} else {
MGLOG_W_ONCE("No backend buffer found for index buffer binding, cannot bind index buffer.");
}
} else {
g_GLESFuncs.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
indexBufferSynced = true;
}
if (indexBufferSynced) {
// The two element-array restore scopes (the restart substitution's and
// MultiDrawImpl's) put the DRIVER id back without touching this serial,
// which is correct: the serial records what the APPLIER last said, and
// those scopes restored exactly what the applier said.
m_syncedIndexSerial = st.IndexBufferSerial;
}
}
if (attributesDirty) {
m_syncedElementsHandle = st.BoundVertexElements;
m_syncedElementsSerial = rec->ContentSerial;
m_syncedVertexBuffersSerial = st.VertexBuffersSerial;
m_hasSyncedElements = true;
}
if (emitAttributes) {
m_syncedFetchBaseInstance = fetchBaseInstance;
}
m_syncedBufferIdGeneration = currentBufferIdGeneration;
}
#endif // MOBILEGL_PIPE_PUSH
void BackendVertexArrayObject::SyncClientSideAttributesForDrawArrays(
const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count) {
if (!stateVAOObject || count <= 0 || first < 0) {
@@ -3830,6 +4148,104 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
#if MOBILEGL_PIPE_PUSH
Bool BackendVertexArrayObject::SyncFloat64AttributeAsFloat32ByHandle(
Uint attribIndex, const MGPVertexAttribWire& attrib, const MG_Pipe::MGPVertexBuffer& binding,
Uint32 fetchBaseInstance) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (attribIndex >= m_convertedAttributeBufferIds.size() || attrib.Size < 1 || attrib.Size > 4) {
return false;
}
if (MG_Pipe::MGPipeHandleIsNull(binding.Res)) {
// A client-memory 64-bit array is narrowed on the draw path instead, which is
// the only place its fetch range is known.
return false;
}
auto* resource = BufferImpl::FindBufferResourceForHandle(binding.Res);
if (resource == nullptr) return false;
// WHAT IS NOT HERE, recorded rather than hidden: the legacy arm opens with
// bufferObject->SyncGpuWrites(), one of the eleven Espryt SyncPersistentMappedRange
// / SyncGpuWrites sites D-N keeps where they are for P3a. It cannot be made from a
// handle - the server has no inverse map to a frontend object, by design - so on
// this arm a 64-bit array whose SOURCE buffer was written by a shader and not yet
// pulled back narrows stale bytes. P8 is what closes it, by moving the pull to the
// client where the object lives; until then this is the one behavioural difference
// between the two arms and it is confined to fp64 vertex arrays fed by
// shader-written buffers.
const Uint8* const sourceBase = resource->hostBytes;
const SizeT sourceSize = BufferImpl::ResourceWidthForHandle(binding.Res);
if (sourceBase == nullptr || attrib.Offset >= sourceSize) {
return false;
}
const SizeT componentCount = static_cast<SizeT>(attrib.Size);
const SizeT sourceElementSize = componentCount * sizeof(Double);
const SizeT available = sourceSize - static_cast<SizeT>(attrib.Offset);
if (available < sourceElementSize) {
return false;
}
const Bool neverAdvances = attrib.Stride <= 0;
const SizeT sourceStride = neverAdvances ? sourceElementSize : static_cast<SizeT>(attrib.Stride);
const SizeT elementCount = neverAdvances ? 1 : ((available - sourceElementSize) / sourceStride) + 1;
const SizeT firstElement = (fetchBaseInstance != 0 && binding.Divisor != 0 && !neverAdvances)
? static_cast<SizeT>(fetchBaseInstance)
: 0;
if (firstElement >= elementCount) {
return false;
}
Uint& convertedBufferId = m_convertedAttributeBufferIds[attribIndex];
if (convertedBufferId == 0) {
g_GLESFuncs.glGenBuffers(1, &convertedBufferId);
if (convertedBufferId == 0) {
MGLOG_E_ONCE("Failed to create the float32 scratch buffer for the 64-bit vertex array at "
"attribute %u.",
attribIndex);
return false;
}
}
// NO MEMO YET on this arm: its key is the source buffer's identity plus its change
// serial, and re-keying ConvertedFloat64Stream onto the buffer's {slot, gen} is the
// next commit's whole subject. Until then every walk reconverts, which is correct
// and slower on a path that only 64-bit vertex arrays reach.
Vector<Float> converted;
NarrowDoubleStreamToFloat32(sourceBase + attrib.Offset, sourceStride, componentCount, elementCount,
converted);
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, convertedBufferId);
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(converted.size() * sizeof(Float)),
converted.data(), GL_STREAM_DRAW);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(converted.size() * sizeof(Float)));
}
const SizeT convertedElementSize = componentCount * sizeof(Float);
if (neverAdvances) {
// Only the binding-point API can say "stride 0".
if (!HasVertexBindingApi()) {
return false;
}
g_GLESFuncs.glVertexAttribFormat(attribIndex, static_cast<GLint>(attrib.Size), GL_FLOAT, GL_FALSE, 0);
g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex);
g_GLESFuncs.glBindVertexBuffer(attribIndex, convertedBufferId, 0, 0);
return true;
}
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, convertedBufferId);
// `normalized` is deliberately GL_FALSE rather than attrib.Normalized: GL ignores it
// for floating-point array types, and honouring it would scale the fetched values.
g_GLESFuncs.glVertexAttribPointer(attribIndex, static_cast<GLint>(attrib.Size), GL_FLOAT, GL_FALSE,
static_cast<GLsizei>(convertedElementSize),
(const void*)(firstElement * convertedElementSize));
return true;
}
#endif // MOBILEGL_PIPE_PUSH
TwinRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject, MG_Pipe::MGPipeKind::VertexElementsCso>
g_backendVertexArrayObjects;
} // namespace VertexArrayImpl
+89
View File
@@ -17,6 +17,10 @@
#include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#include "SlotTables.h"
#if MOBILEGL_PIPE_PUSH
// P3a: the vertex-input payload views the handle arm of the VAO twin consumes.
#include <MG_Pipe/MGPipeTypes.h>
#endif
namespace MobileGL::MG_Backend::DirectGLES {
String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType);
@@ -732,6 +736,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The legacy arm keeps calling BufferObject::MarkGpuWritten directly, and the pull
// build never sees this function at all (G1).
void MarkBufferGpuWritten(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
// The applier's stored extent for this resource, 0 when it has no record. The one
// thing outside BufferImpl that needs it is the fp64 narrowing, whose source extent
// used to be BufferObject::GetSize().
SizeT ResourceWidthForHandle(MG_Pipe::MGPipeHandle res);
#endif
// Registered as the frontend's BufferBackendOps at backend init and on
@@ -938,6 +947,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_State::GLState::BufferObject* frontend = nullptr;
BufferImpl::GLESBufferResource* resource = nullptr;
Uint8 attribIndex = 0;
#if MOBILEGL_PIPE_PUSH
// P3a re-key: the entry's identity on the handle arm. A {slot, gen} cannot
// be reproduced by a recycled heap address, so the clean probe compares
// this instead of the raw frontend pointer and never has to ask the
// allocator for it again mid-draw.
MG_Pipe::MGPipeHandle handle = MG_Pipe::kMGPipeNullHandle;
#endif
};
Bool valid = false;
Uint32 configVersion = 0;
@@ -945,6 +961,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
Array<Entry, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS> entries;
MG_State::GLState::BufferObject* iboFrontend = nullptr;
BufferImpl::GLESBufferResource* iboResource = nullptr;
#if MOBILEGL_PIPE_PUSH
// P3a re-key of the memo's validity key: on the handle arm the frontend VAO's
// wrapping configuration version is replaced by the bound vertex-elements CSO
// (identity AND its server-owned content serial) plus the vertex-buffer set's
// own serial - three monotone Uint64s and a {slot, gen}, no wrap and no
// identity patch. The IBO entry keeps its separate key for the same reason it
// always had one: the index slot is not part of the configuration (D5).
MG_Pipe::MGPipeHandle elementsHandle = MG_Pipe::kMGPipeNullHandle;
Uint64 elementsSerial = 0;
Uint64 buffersSerial = 0;
MG_Pipe::MGPipeHandle iboHandle = MG_Pipe::kMGPipeNullHandle;
#endif
// Buffer-mutation epoch (BufferImpl::CurrentBufferMutationEpoch) at which
// the LAST probe pass found every entry / the IBO clean; 0 = not stamped
// (epochs start at 1). While a stamp matches the pre-pass epoch read, the
@@ -983,6 +1011,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool SyncFloat64AttributeAsFloat32(Uint attribIndex, const MG_State::GLState::VertexAttribute& attrib,
Uint32 fetchBaseInstance);
#if MOBILEGL_PIPE_PUSH
// The handle arm of the whole vertex-elements half. Everything it needs arrives in
// the applier's records - the bound CSO's two views, the vertex-buffer set, the
// index buffer and the resolved fetch base instance - so it takes no argument at
// all and touches no frontend type. The legacy arm above it is unchanged and both
// compile in every push build (ARCHITECTURE.md 9.6).
void SyncToBackendFromApplier();
// Same narrowing, same memo, same Adreno disable; the source bytes are the shadow
// base the resource call carried and the memo key is the buffer's {slot, gen}.
Bool SyncFloat64AttributeAsFloat32ByHandle(Uint attribIndex, const MGPVertexAttribWire& attrib,
const MG_Pipe::MGPVertexBuffer& binding,
Uint32 fetchBaseInstance);
#endif
// What the converted float32 stream in m_convertedAttributeBufferIds[i] was built
// from. A hit skips the CPU conversion and the re-upload; the buffer's change serial
// is part of the key, so a glBufferSubData into the source invalidates it.
@@ -1011,6 +1053,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// version early-out in SyncToBackend must not be trusted while it is set.
Bool m_hasConvertedFloat64Attribute = false;
Bool m_isInitialized = false;
#if MOBILEGL_PIPE_LEGACY_MEMOS
// ---- the pre-handle memo set (ARCHITECTURE.md 9.6) -------------------------
// Retired by P3a on the handle arm and kept compiled here so the A/B is real: a
// cleared subsystem bit runs THESE, not a re-keyed twin wearing their names. A
// pull build forces MOBILEGL_PIPE_LEGACY_MEMOS ON, so sizeof(this) does not move
// and no symbol resizes (G1).
Uint16 m_syncedIndexBufferVersion = 0;
// Identity of the buffer the version above was stamped against. Raw and never
// dereferenced: the slot version is a wrapping Uint16 (see the ResolvedDrawBuffers
@@ -1026,6 +1074,28 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint32 m_syncedConfigVersion = 0;
Array<MG_State::GLState::VertexAttributeVersion, MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>
m_syncedAttributeVersions;
#endif // MOBILEGL_PIPE_LEGACY_MEMOS
#if MOBILEGL_PIPE_PUSH
// ---- what replaces them on the handle arm (D-G4) ---------------------------
// The bound vertex-elements CSO this twin last emitted, and the applier's
// server-owned content serial for it. Together they replace
// m_hasSyncedConfigVersion + m_syncedConfigVersion AND the whole per-attribute
// version array: the applier's stored Attributes[] IS what was last pushed, so a
// per-attribute compare has nothing left to prove and the walk re-emits.
MG_Pipe::MGPipeHandle m_syncedElementsHandle = MG_Pipe::kMGPipeNullHandle;
Uint64 m_syncedElementsSerial = 0;
Bool m_hasSyncedElements = false;
// The vertex-buffer set's own serial. Not in D-G4's table, and it has to be here:
// set_vertex_buffers is an independent call carrying the buffer identities, the
// offsets and the divisors this twin BAKES into the driver VAO, so a set that
// moved while the format did not must still re-emit them.
Uint64 m_syncedVertexBuffersSerial = 0;
// Replaces m_syncedIndexBufferVersion (a wrapping Uint16) AND
// m_syncedIndexBufferObject (the raw identity patch that closed its wrap hole):
// one monotone Uint64, no wrap, nothing to patch. This is the Track H re-key
// ARCHITECTURE.md 9.5 counts.
Uint64 m_syncedIndexSerial = 0;
#endif
// Byte shift currently baked into the instanced arrays' offsets by the baseInstance
// emulation (see SetPendingFetchBaseInstance). It is draw state, not VAO state, so it
// is deliberately NOT covered by the config version: the frontend never bumps for it.
@@ -1055,6 +1125,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
// instanced array at element "floor(instance / divisor) + baseInstance", and ES has no
// way to say the "+ baseInstance" part - so it is folded into the attribute's own byte
// offset (baseInstance * stride) for every divisor'd array, which is exactly equivalent.
//
// P3a RETIRES THE AMBIENT GLOBAL (D-H2): an ambient process global cannot cross a
// pushed boundary, so on the handle arm the draw's RAW base instance rides in
// MGPVertexBuffers::BaseInstance and the SERVER decides whether to shift - the answer
// lands in MGPipeApplierState::VertexFetchBaseInstance and the VAO sync reads it there.
// The three declarations below and the three scopes in DirectGLES.cpp are the legacy
// arm's, kept compiled because a cleared subsystem bit has to run a real pre-handle
// path and because removing them would delete two symbols from the PULL build (G1).
#if MOBILEGL_PIPE_LEGACY_MEMOS
// Must be set BEFORE PrepareForDraw so the VAO sync sees it, and cleared after the draw
// so the next one refetches from element 0; ScopedFetchBaseInstance does both.
void SetPendingFetchBaseInstance(Uint32 baseInstance);
@@ -1067,6 +1146,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
ScopedFetchBaseInstance(const ScopedFetchBaseInstance&) = delete;
ScopedFetchBaseInstance& operator=(const ScopedFetchBaseInstance&) = delete;
};
#endif
#if MOBILEGL_PIPE_PUSH
// The server-owned half of the same decision, and the reason the client never
// pre-shifts an offset: emulation ownership is the server's (ARCHITECTURE.md 5.7).
// True when the driver applies baseInstance to the vertex fetch itself, in which case
// the attribute-offset emulation must stay out of the way. Applied to whatever the
// applier stored, so the answer is the same whichever side resolved it first.
Bool BackendUsesNativeBaseInstance();
#endif
} // namespace VertexArrayImpl
namespace TextureImpl {