diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 6c312f5a..29380647 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -17,6 +17,10 @@ #include #include #include +#if MOBILEGL_PIPE_PUSH +// P3a: the applier's vertex-input records the re-keyed draw-buffer memo is validated against. +#include +#endif #include #include #include @@ -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& 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(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(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([&] { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index e245373b..d4a88b23 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2242,6 +2242,8 @@ namespace MobileGL::MG_Backend::DirectGLES { bufferObject->GetLifetimeId()); } + SizeT ResourceWidthForHandle(MG_Pipe::MGPipeHandle res) { return ResourceWidthOf(res); } + void MarkBufferGpuWritten(const SharedPtr& 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(baseInstance) * static_cast(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(GL_BGRA) : static_cast(attrib.Size); + g_GLESFuncs.glVertexAttribFormat(attribIndex, glSize, + MG_Util::ConvertDataTypeToGLEnum(static_cast(attrib.Type)), + attrib.Normalized ? GL_TRUE : GL_FALSE, 0); + } else { + g_GLESFuncs.glVertexAttribIFormat(attribIndex, static_cast(attrib.Size), + MG_Util::ConvertDataTypeToGLEnum(static_cast(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(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& 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(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(DataType::Float64)) { + if (attrib.Enabled && attrib.Type == static_cast(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(attrib.Offset) + + BaseInstanceByteShiftWire(attrib.Stride, divisor, fetchBaseInstance); + const GLenum glType = MG_Util::ConvertDataTypeToGLEnum(static_cast(attrib.Type)); + + if (!attrib.IsInteger) { + const GLint glSize = attrib.IsBgra ? static_cast(GL_BGRA) : static_cast(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(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(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& 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(attrib.Size); + const SizeT sourceElementSize = componentCount * sizeof(Double); + const SizeT available = sourceSize - static_cast(attrib.Offset); + if (available < sourceElementSize) { + return false; + } + + const Bool neverAdvances = attrib.Stride <= 0; + const SizeT sourceStride = neverAdvances ? sourceElementSize : static_cast(attrib.Stride); + const SizeT elementCount = neverAdvances ? 1 : ((available - sourceElementSize) / sourceStride) + 1; + + const SizeT firstElement = (fetchBaseInstance != 0 && binding.Divisor != 0 && !neverAdvances) + ? static_cast(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 converted; + NarrowDoubleStreamToFloat32(sourceBase + attrib.Offset, sourceStride, componentCount, elementCount, + converted); + BufferImpl::BindBufferId(GL_ARRAY_BUFFER, convertedBufferId); + g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast(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(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(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(attrib.Size), GL_FLOAT, GL_FALSE, + static_cast(convertedElementSize), + (const void*)(firstElement * convertedElementSize)); + return true; + } +#endif // MOBILEGL_PIPE_PUSH + TwinRegistry g_backendVertexArrayObjects; } // namespace VertexArrayImpl diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 5e473dca..d118ea7c 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -17,6 +17,10 @@ #include #include #include "SlotTables.h" +#if MOBILEGL_PIPE_PUSH +// P3a: the vertex-input payload views the handle arm of the VAO twin consumes. +#include +#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& 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 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 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 {