From 62bfe4615d62a506744b4be0800d235d2f19bb47 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 20 Sep 2026 20:47:50 -0400 Subject: [PATCH] [MG_Impl, MG_Backend] (Disaggregated): the monolith arm fetches the client arrays a draw names The wire arm snapshots the elements a draw fetches out of the application's own vertex/index memory (beba0256's MGPipeOwnedDrawInputs). The monolith arm's upload answered only the non-indexed family's contiguous (first, count) run, so an INDEXED or INDIRECT draw over a client-memory array staged nothing: the enabled array kept whatever the ES context last held for it - normally disabled, i.e. every vertex reading the generic current value - and the draw painted nothing with no GL error anywhere. ClientFetchPlan.h is that question factored out once (the fetches, the referenced vertices, the per-divisor element sets) so both arms answer it from one place; OwnedDrawInputs now calls it and keeps its behaviour byte for byte. DirectGLES: SyncClientSideAttributesForDraw stages the sparse snapshot for a draw whose elements the (first, count) run does not describe, driven by the plan; the indexed entry points and both indirect executors ask for it, and the arrays path learns the baseInstance shift a divisor'd client array needs. W3: DirectGLES' six monolith failures are the four indexed/indirect shapes. --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 171 +++++++++++++ MobileGL/MG_Backend/DirectGLES/Managers.cpp | 138 +++++++++- MobileGL/MG_Backend/DirectGLES/Managers.h | 22 +- MobileGL/MG_Impl/Pipe/ClientFetchPlan.h | 240 ++++++++++++++++++ MobileGL/MG_Impl/Pipe/OwnedDrawInputs.h | 167 +++++------- 5 files changed, 625 insertions(+), 113 deletions(-) create mode 100644 MobileGL/MG_Impl/Pipe/ClientFetchPlan.h diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 5bc1bfa1..7c594fe6 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -2422,6 +2422,153 @@ namespace MobileGL::MG_Backend::DirectGLES { // `static` on purpose (G1): the pull build must not gain an exported name for a // function that only re-homes two identical inline blocks the two DrawArrays entry // points used to carry. +#if MOBILEGL_BUILD_DISAGGREGATED + // ---- W3: THE MONOLITH ARM'S CLIENT-MEMORY SNAPSHOT, PER DRAW ------------------------- + // + // A client-memory attribute is uploaded HERE because its bytes have no store on this side + // to bind. SyncClientSideVertexArraysForDrawArrays below does that for the non-indexed + // family, whose fetched elements are exactly the contiguous (first, count) run. Every + // OTHER draw shape fetches by index and by instance, so its elements have to come from + // MGPipeClientFetchPlan - the same answer the wire arm's owned snapshot is built from + // (MG_Impl/Pipe/OwnedDrawInputs.h, which is why the two arms agree by construction). + // + // WHAT WAS MISSING BEFORE THIS: an indexed draw over a client array uploaded nothing at + // all. The ES context kept whatever the last VBO-backed attribute had left for that + // index - normally the array DISABLED, i.e. every vertex reading the generic current + // value - so the draw painted nothing, with no GL error anywhere to say why. + struct ClientSnapshotSources { + const SharedPtr* ElementBuffer = nullptr; + }; + + Bool ReadClientSnapshotBytes(void* user, MG_Pipe::MGPipeClientBufferKind kind, Uint64 offset, SizeT size, + void* destination) { + if (kind != MG_Pipe::MGPipeClientBufferKind::Element) return false; + const auto& buffer = *static_cast(user)->ElementBuffer; + if (!buffer || offset > buffer->GetSize() || size > buffer->GetSize() - offset) return false; + // A shader-written index stream is exactly the case this snapshot exists for, so the + // object's shadow is reconciled before it is read (SyncGpuWrites is a no-op otherwise). + buffer->SyncGpuWrites(); + buffer->DownloadSubData(destination, static_cast(offset), size); + return true; + } + + // The draw's own fetch, spelled as the range record spells it: `indexSize` 0 is arrays, + // `elementStart` is the first element (an element offset into the bound element buffer, or + // 0 for the application's own index array), and `clientIndices` names that array when + // there is one. FALSE means the caller must SKIP the draw: the elements it would fetch are + // not in the store the driver is about to read, and issuing it anyway is a wrong picture + // rather than an error. + Bool SyncClientSideVertexArraysForFetch(Uint8 indexSize, Uint32 elementStart, GLsizei count, + GLsizei instanceCount, GLint baseVertex, Uint32 baseInstance, + const void* clientIndices, Uint64 clientIndexBytes) { + if (count <= 0 || instanceCount <= 0) return true; + const auto& currentVAO = MGB_CTX->GetBoundVertexArray(); + if (!currentVAO) return true; + + // A draw whose attributes all come from buffers has nothing to snapshot, and asking + // the plan would read the index stream to answer a question nobody asked. + Bool clientArrays = false; + Bool clientVertexRate = false; + const auto& attributes = currentVAO->GetAllAttributes(); + for (SizeT i = 0; i < attributes.size(); ++i) { + if (!attributes[i].Enabled || attributes[i].Buffer) continue; + clientArrays = true; + clientVertexRate |= attributes[i].Divisor == 0; + } + if (!clientArrays) return true; + + auto* twin = ResolveVaoTwin(currentVAO); + if (twin == nullptr) return false; + + const SharedPtr elementBuffer = + currentVAO->GetIndexBufferBindingSlot().GetBoundObject(); + ClientSnapshotSources sources{&elementBuffer}; + + MG_Pipe::MGPDrawRange range{elementStart, static_cast(count), baseVertex}; + MG_Pipe::MGPipeClientDrawInputs inputs{}; + inputs.IndexSize = indexSize; + inputs.Ranges = ⦥ + inputs.RangeCount = 1; + inputs.InstanceCount = static_cast(instanceCount); + inputs.BaseInstance = baseInstance; + inputs.ClientIndices = clientIndices; + inputs.ClientIndexBytes = clientIndexBytes; + inputs.WantVertices = clientVertexRate; + // The driver does its own restart handling; the plan has to know only that the restart + // value is not a vertex, or the snapshot would stage an element no primitive fetches. + inputs.PrimitiveRestart = MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) || + MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex); + inputs.RestartIndex = MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex) + ? (indexSize == 1 ? 0xffu : indexSize == 2 ? 0xffffu : 0xffffffffu) + : MGB_CTX->GetPrimitiveRestartIndex(); + + MG_Pipe::MGPipeClientFetchPlan plan; + if (!plan.Build(inputs, &ReadClientSnapshotBytes, &sources)) return false; + return twin->SyncClientSideAttributesForDraw(currentVAO, plan, baseInstance); + } + + // The indirect executors' form: the command's own words ARE the draw's fetch, and a + // command block a shader wrote this frame is exactly what these shapes exist for - so the + // command buffer's shadow is reconciled before it is read, and `commandBytes` (the + // caller's possibly stale copy) is only the fallback for a client-authored block. + Bool SyncClientSideVertexArraysForIndirectFetch(const Uint8* commandBytes, SizeT commandOffset, + GLsizei stride, GLsizei index, Uint8 indexSize, + const SharedPtr& commandBuffer) { + const Uint8* source = commandBytes; + if (commandBuffer) { + commandBuffer->SyncGpuWrites(); + const Uint8* mapped = commandBuffer->MappedData(); + if (mapped == nullptr) return false; + source = mapped + commandOffset; + } + const SizeT at = static_cast(index) * static_cast(stride); + if (indexSize == 0) { + DrawArraysIndirectCommand cmd{}; + std::memcpy(&cmd, source + at, sizeof(cmd)); + return SyncClientSideVertexArraysForFetch(0, cmd.first, static_cast(cmd.count), + static_cast(cmd.instanceCount), 0, cmd.baseInstance, + nullptr, 0); + } + DrawElementsIndirectCommand cmd{}; + std::memcpy(&cmd, source + at, sizeof(cmd)); + return SyncClientSideVertexArraysForFetch(indexSize, cmd.firstIndex, static_cast(cmd.count), + static_cast(cmd.instanceCount), cmd.baseVertex, + static_cast(cmd.baseInstance), nullptr, 0); + } + + // The indexed entry points' form. `indices` is a byte offset into the bound element buffer + // when one is bound and the application's own array otherwise, which is the same + // distinction every one of them already makes when it issues the draw. + Bool SyncClientSideVertexArraysForIndexedFetch(GLenum type, GLsizei count, const void* indices, + GLsizei instanceCount, GLint baseVertex, GLuint baseInstance) { + if (count <= 0 || instanceCount <= 0) return true; + const auto& currentVAO = MGB_CTX->GetBoundVertexArray(); + if (!currentVAO) return true; + const Uint8 indexSize = static_cast(MG_Util::GetGLTypeSize(type)); + if (indexSize == 0) return true; + + const Bool clientIndices = !currentVAO->GetIndexBufferBindingSlot().GetBoundObject(); + Uint32 elementStart = 0; + Uint64 clientIndexBytes = 0; + if (clientIndices) { + if (indices == nullptr) return false; + clientIndexBytes = static_cast(count) * indexSize; + } else { + const Uint64 byteOffset = reinterpret_cast(indices); + if (byteOffset % indexSize != 0 || + byteOffset / indexSize > std::numeric_limits::max()) { + MGLOG_E_ONCE("An indexed draw over a client-memory vertex array names byte offset %llu, " + "which is not a whole number of %u-byte indices; the draw is skipped", + static_cast(byteOffset), static_cast(indexSize)); + return false; + } + elementStart = static_cast(byteOffset / indexSize); + } + return SyncClientSideVertexArraysForFetch(indexSize, elementStart, count, instanceCount, baseVertex, + baseInstance, clientIndices ? indices : nullptr, + clientIndexBytes); + } +#endif static void SyncClientSideVertexArraysForDrawArrays(GLint first, GLsizei count) { #if MOBILEGL_BUILD_DISAGGREGATED if (BufferImpl::VertexInputReadsRecords()) { @@ -8269,6 +8416,9 @@ namespace MobileGL::MG_Backend::DirectGLES { SetCurrentBaseVertex(cmd.baseVertex); } ForEachViewportRoutingPass([&] { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch( + commandBytes, commandOffset, stride, i, static_cast(indexSize), drawIndirectBuffer)) + return; g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast(cmdByteOffset)); }); } @@ -8284,6 +8434,9 @@ namespace MobileGL::MG_Backend::DirectGLES { SetCurrentBaseVertex(cmd.baseVertex); const auto indexByteOffset = static_cast(cmd.firstIndex) * indexSize; ForEachViewportRoutingPass([&] { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch( + commandBytes, commandOffset, stride, i, static_cast(indexSize), drawIndirectBuffer)) + continue; g_GLESFuncs.glDrawElementsInstancedBaseVertex( mode, static_cast(cmd.count), type, reinterpret_cast(indexByteOffset), static_cast(cmd.instanceCount), cmd.baseVertex); @@ -8347,6 +8500,9 @@ namespace MobileGL::MG_Backend::DirectGLES { SetCurrentBaseInstance(cmd.baseInstance); } ForEachViewportRoutingPass([&] { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch( + commandBytes, commandOffset, stride, i, 0, drawIndirectBuffer)) + return; g_GLESFuncs.glDrawArraysIndirect(mode, reinterpret_cast(cmdByteOffset)); }); } @@ -8360,6 +8516,9 @@ namespace MobileGL::MG_Backend::DirectGLES { SetCurrentDrawID(static_cast(i)); SetCurrentBaseInstance(cmd.baseInstance); ForEachViewportRoutingPass([&] { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch( + commandBytes, commandOffset, stride, i, 0, drawIndirectBuffer)) + continue; g_GLESFuncs.glDrawArraysInstanced(mode, static_cast(cmd.first), static_cast(cmd.count), static_cast(cmd.instanceCount)); @@ -9045,6 +9204,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, 0, 0)) return; #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER DebugImpl::OpenGLScopeMarker marker(__func__); #endif @@ -9070,6 +9230,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices, GLint basevertex) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, basevertex, 0)) return; #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER DebugImpl::OpenGLScopeMarker marker(__func__); #endif @@ -9441,6 +9602,7 @@ namespace MobileGL::MG_Backend::DirectGLES { void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices, GLint basevertex) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, basevertex, 0)) return; DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; PrepareForDraw(syncBit); const ScopedRestartIndexSubstitution restart(type, count, indices); @@ -9454,6 +9616,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, 1, 0, 0)) return; DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer; PrepareForDraw(syncBit); const ScopedRestartIndexSubstitution restart(type, count, indices); @@ -9491,6 +9654,8 @@ namespace MobileGL::MG_Backend::DirectGLES { void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, basevertex, + baseinstance)) return; DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; MGL_SCOPED_FETCH_BASE_INSTANCE(fetchScope, EmulatedFetchBaseInstance(baseinstance)); PrepareForDraw(syncBit); @@ -9514,6 +9679,8 @@ namespace MobileGL::MG_Backend::DirectGLES { void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLint basevertex) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, basevertex, + 0)) return; DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; PrepareForDraw(syncBit); const ScopedRestartIndexSubstitution restart(type, count, indices); @@ -9528,6 +9695,8 @@ namespace MobileGL::MG_Backend::DirectGLES { void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount, GLuint baseinstance) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, 0, + baseinstance)) return; DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; MGL_SCOPED_FETCH_BASE_INSTANCE(fetchScope, EmulatedFetchBaseInstance(baseinstance)); PrepareForDraw(syncBit); @@ -9546,6 +9715,8 @@ namespace MobileGL::MG_Backend::DirectGLES { } void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instancecount) { + if (!VertexArrayImpl::SyncClientSideVertexArraysForIndexedFetch(type, count, indices, instancecount, 0, 0)) + return; DrawSyncFlags syncBit = DrawSyncBit::IndexBuffer | DrawSyncBit::Instancing; PrepareForDraw(syncBit); const ScopedRestartIndexSubstitution restart(type, count, indices); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 697a8140..885fac1c 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -5545,8 +5545,119 @@ namespace MobileGL::MG_Backend::DirectGLES { } #endif // MOBILEGL_PIPE_PUSH + + +#if MOBILEGL_BUILD_DISAGGREGATED + // THE CLIENT-ARRAY UPLOAD FOR A DRAW WHOSE FETCHED ELEMENTS (first, count) DOES NOT + // DESCRIBE. SyncClientSideAttributesForDrawArrays below answers the non-indexed family + // exactly and nothing else: an indexed draw reads the elements its INDICES name, an + // instanced draw reads one element per instance (shifted by baseInstance and divided by + // divisor), and the indirect forms read a count and a first element that live in GPU + // memory. Until this function existed those draws took NO upload at all - the enabled + // client attribute kept whatever the ES context last held for it (normally: disabled, so + // every vertex read the generic current value), and an indexed draw over a client array + // painted nothing. + // + // `plan` is MGPipeClientFetchPlan's answer for this very draw, which is the same + // arithmetic the wire arm snapshots from (MG_Impl/Pipe/OwnedDrawInputs.h) - the two arms + // fetch the same elements by construction rather than by inspection. + // + // THE UPLOAD IS SPARSE: only the fetched elements are dereferenced, and the gaps between + // them are zero-filled. Reading the intervening application memory would be legal here - + // one thread, one address space - but it is not needed, and a draw naming element 0 and + // element 2^20 must not turn into an 8 MiB read of somebody else's heap. + // + // False means the draw must be SKIPPED: the elements it would fetch are not in the store + // the driver is about to read, and issuing it anyway is a wrong picture, not an error. + Bool BackendVertexArrayObject::SyncClientSideAttributesForDraw( + const SharedPtr& stateVAOObject, + const MG_Pipe::MGPipeClientFetchPlan& plan, Uint32 fetchBaseInstance) { + if (!stateVAOObject) return true; + + Bind(); + + const auto& allAttributes = stateVAOObject->GetAllAttributes(); + Vector elements; + for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) { + const auto& attrib = allAttributes[attribIndex]; + if (!attrib.Enabled || attrib.Buffer) continue; + + // The same outcome SyncToBackend reaches for a 64-bit client array it cannot + // narrow: the enabled array is DROPPED (the attribute reads its generic current + // value) rather than left enabled with no pointer behind it, which is the Adreno + // null-deref. The narrowing stream itself stays the arrays path's answer. + if (attrib.IsLong || attrib.Type == DataType::Float64) { + g_GLESFuncs.glDisableVertexAttribArray(attribIndex); + continue; + } + + if (!plan.Elements(static_cast(attrib.Divisor), elements)) return false; + if (elements.empty()) continue; + + const auto* source = reinterpret_cast(attrib.Offset); + const SizeT elementSize = GetAttributeByteSize(attrib.Type, attrib.Size, attrib.IsBgra); + if (source == nullptr || elementSize == 0 || attrib.Size <= 0) { + MGLOG_E_ONCE("Client attribute %u has no readable element size (%zu); the draw is skipped", + attribIndex, elementSize); + return false; + } + const SizeT stride = attrib.Stride > 0 ? static_cast(attrib.Stride) : elementSize; + const Uint64 last = elements.back(); + if (stride != 0 && last > (std::numeric_limits::max() - elementSize) / stride) return false; + const SizeT byteCount = static_cast(last * stride + elementSize); + + auto& bufferId = m_clientAttributeBufferIds[attribIndex]; + if (bufferId == 0) { + g_GLESFuncs.glGenBuffers(1, &bufferId); + if (bufferId == 0) { + MGLOG_E_ONCE("Failed to create client-side vertex attribute upload buffer."); + return false; + } + } + + Vector snapshot(byteCount, 0); + for (const Uint64 element : elements) { + const SizeT offset = static_cast(element * stride); + std::memcpy(snapshot.data() + offset, source + offset, elementSize); + } + + BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId); + g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast(snapshot.size()), + snapshot.data(), GL_STREAM_DRAW); + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient, + static_cast(snapshot.size())); + } + + // Element 0 of a client array sits at its buffer's byte 0, so a divisor'd array's + // baseInstance shift is BaseInstanceByteShift - the same arithmetic SyncToBackend + // applies to a VBO-backed array - and it rides in the pointer argument. + const void* const shift = + reinterpret_cast(BaseInstanceByteShift(attrib, fetchBaseInstance)); + if (!attrib.IsInteger) { + const GLint glSize = attrib.IsBgra ? static_cast(GL_BGRA) : attrib.Size; + g_GLESFuncs.glVertexAttribPointer(attribIndex, glSize, + MG_Util::ConvertDataTypeToGLEnum(attrib.Type), + attrib.Normalized ? GL_TRUE : GL_FALSE, + static_cast(stride), shift); + } else { + g_GLESFuncs.glVertexAttribIPointer(attribIndex, attrib.Size, + MG_Util::ConvertDataTypeToGLEnum(attrib.Type), + static_cast(stride), shift); + } + // The divisor is NOT optional here. SyncToBackend's own glVertexAttribDivisor sits + // behind BindAttributeBuffer, which a client-memory attribute never passes, so + // until this line the ES context's divisor for this index is whatever the last + // VBO-backed attribute left there. + g_GLESFuncs.glVertexAttribDivisor(attribIndex, attrib.Divisor); + g_GLESFuncs.glEnableVertexAttribArray(attribIndex); + } + return true; + } +#endif void BackendVertexArrayObject::SyncClientSideAttributesForDrawArrays( - const SharedPtr& stateVAOObject, GLint first, GLsizei count) { + const SharedPtr& stateVAOObject, GLint first, GLsizei count, + Uint32 fetchBaseInstance) { if (!stateVAOObject || count <= 0 || first < 0) { return; } @@ -5606,7 +5717,18 @@ namespace MobileGL::MG_Backend::DirectGLES { // forwarded here either. g_GLESFuncs.glVertexAttribPointer(attribIndex, attrib.Size, GL_FLOAT, GL_FALSE, static_cast(componentCount * sizeof(Float)), - nullptr); + // The converted stream is tightly packed, so a divisor'd array's + // baseInstance shift is a whole number of CONVERTED elements - + // not BaseInstanceByteShift's source-stride arithmetic. + reinterpret_cast( + attrib.Divisor == 0 ? 0u : static_cast(fetchBaseInstance) * + componentCount * + sizeof(Float))); + // glVertexAttribPointer's final argument IS that shift. A client array + // declared through the pointer API keeps element 0 at the buffer's byte 0, so + // the shift rides in the pointer argument the call above passes. + // The float64 narrowing itself is shared with SyncToBackend's conversion + // branch; only the fetch RANGE differs, and the draw's is the accurate one. g_GLESFuncs.glEnableVertexAttribArray(attribIndex); continue; } @@ -5641,11 +5763,19 @@ namespace MobileGL::MG_Backend::DirectGLES { const GLint glSize = attrib.IsBgra ? static_cast(GL_BGRA) : attrib.Size; g_GLESFuncs.glVertexAttribPointer( attribIndex, glSize, MG_Util::ConvertDataTypeToGLEnum(attrib.Type), - attrib.Normalized ? GL_TRUE : GL_FALSE, static_cast(stride), nullptr); + attrib.Normalized ? GL_TRUE : GL_FALSE, static_cast(stride), + // A client array's element 0 sits at its buffer's byte 0, so the + // divisor'd arrays' baseInstance shift is exactly BaseInstanceByteShift - + // the same arithmetic SyncToBackend applies to a VBO-backed array. + reinterpret_cast(BaseInstanceByteShift(attrib, fetchBaseInstance))); } else { g_GLESFuncs.glVertexAttribIPointer(attribIndex, attrib.Size, MG_Util::ConvertDataTypeToGLEnum(attrib.Type), - static_cast(stride), nullptr); + static_cast(stride), + // See the pointer call above: the divisor'd + // arrays' baseInstance shift is the pointer. + reinterpret_cast( + BaseInstanceByteShift(attrib, fetchBaseInstance))); } } diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index cf75f73b..1ab27256 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -20,6 +20,10 @@ #if MOBILEGL_PIPE_PUSH // P3a: the vertex-input payload views the handle arm of the VAO twin consumes. #include +// The client-memory fetch plan: which elements of an application-owned array a draw reads. +// The monolith arm answers it here (SyncClientSideAttributesForDraw) and the wire arm asks +// the same question of the same header (MG_Impl/Pipe/OwnedDrawInputs.h). +#include // P4a: the RECORDS the five re-keyed twins read instead of the frontend object. The readers // below hand back pointers to them, and MGPipeResourceRecord::PendingUpload is a nested type, // so a forward declaration would not do. Push-only, like everything else P4a adds to this @@ -1339,7 +1343,23 @@ namespace MobileGL::MG_Backend::DirectGLES { void SyncToBackendFromApplier(); #endif void SyncClientSideAttributesForDrawArrays( - const SharedPtr& stateVAOObject, GLint first, GLsizei count); + const SharedPtr& stateVAOObject, GLint first, GLsizei count, + Uint32 fetchBaseInstance = 0); +#if MOBILEGL_BUILD_DISAGGREGATED + // THE SAME UPLOAD FOR A DRAW WHOSE FETCHED ELEMENTS THE (first, count) RANGE DOES NOT + // DESCRIBE: an indexed draw reads the elements its indices name, an instanced one one + // element per instance, and the indirect forms a range that lives in GPU memory. + // `plan` is MGPipeClientFetchPlan's answer for this very draw, so this arm and the + // wire arm's owned snapshot fetch the same elements by construction. + // + // False when a client array could not be snapshotted (an index or command block this + // side cannot read, a fetch range that cannot be represented). The caller must then + // SKIP the draw: issuing it would have the shader read whatever the ES context last + // held for that attribute, which is a wrong picture rather than an error. + Bool SyncClientSideAttributesForDraw( + const SharedPtr& stateVAOObject, + const MG_Pipe::MGPipeClientFetchPlan& plan, Uint32 fetchBaseInstance); +#endif Uint GetBackendVertexArrayId() const { return m_backendVAOId; } void Bind() const; diff --git a/MobileGL/MG_Impl/Pipe/ClientFetchPlan.h b/MobileGL/MG_Impl/Pipe/ClientFetchPlan.h new file mode 100644 index 00000000..062cf31b --- /dev/null +++ b/MobileGL/MG_Impl/Pipe/ClientFetchPlan.h @@ -0,0 +1,240 @@ +// MobileGL - MobileGL/MG_Impl/Pipe/ClientFetchPlan.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0 +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// WHICH ELEMENTS OF A CLIENT-MEMORY ARRAY A DRAW ACTUALLY FETCHES. +// +// A client-memory vertex array has no extent the object could report, so "how much of it +// has to exist before this draw can run" is a question only the DRAW can answer: the +// index bytes it will read, the instance count and base instance it shifts by, and - for +// the indirect forms - the command block a shader may have written moments earlier. +// +// Two roles ask it and for the same reason. The wire arm asks it to snapshot the fetched +// elements into owned buffers before the draw crosses (MGPipeOwnedDrawInputs), and the +// monolith arm asks it to size the per-draw upload the backend has always done from the +// application's own pointer (DirectGLES' SyncClientSideAttributesForDraw*, Magma's +// UploadAndBindVertexBuffers). The question is identical, so the answer lives here once: +// two copies of this arithmetic would be two chances to disagree about which vertices a +// draw reads, and disagreeing is a wrong picture rather than an error. +// +// WHAT IT DELIBERATELY DOES NOT DO: dereference application memory. The plan names element +// INDICES; reading the bytes behind them is the caller's, because only the caller knows +// whether the elements are read from the client's own array or out of a store the server +// owns. +#pragma once + +#if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH +#include +#include +#include +#include +#include + +namespace MobileGL::MG_Pipe { + + // The indirect block as a role that reads GL state can state it: the byte geometry plus + // whether the GL_PARAMETER_BUFFER of the *IndirectCount forms is bound. MGPDrawIndirect's + // two handle fields are the wire's identity and are meaningless to the monolith arm, which + // resolves the very same bytes out of the frontend binding slots. + struct MGPipeClientIndirect { + Uint64 Offset = 0; + Uint64 ParameterOffset = 0; + Uint32 Stride = 0; + Uint32 DrawCount = 0; + Bool HasParameterBuffer = false; + }; + + // The draw's own shape, spelled the way the range record spells it: arrays carry Start = + // first and no index size, indexed carries Start = the element offset into the element + // buffer and IndexBias = base vertex. + struct MGPipeClientDrawInputs { + Uint8 IndexSize = 0; + const MGPDrawRange* Ranges = nullptr; + Uint32 RangeCount = 0; + Uint32 InstanceCount = 1; + Uint32 BaseInstance = 0; + // True when primitive restart is live and RestartIndex is the value the index stream + // restarts on. A restart index is a marker, not a vertex: it is what the mono arm's + // restart substitution rewrites around, and counting it as a fetched element would + // copy an element no primitive ever reads. + Bool PrimitiveRestart = false; + Uint32 RestartIndex = 0; + // The application's own index array, when no element buffer is bound. Null with + // IndexSize != 0 means "the indices come from the bound element buffer", i.e. through + // the reader. + const void* ClientIndices = nullptr; + Uint64 ClientIndexBytes = 0; + const MGPipeClientIndirect* Indirect = nullptr; + // Whether a vertex-rate (divisor == 0) client array is present. A draw whose client + // arrays are all instance-rate never has to read the index stream to answer "which + // elements", so the element buffer is left alone (and the reader is never called with + // BufferKind::Element). + Bool WantVertices = false; + }; + + // Where a byte request is aimed. Both kinds are buffers a GL context names; they are + // distinguished because the two roles resolve them from different places and a reader + // that answered one for the other would size an upload off the wrong bytes. + enum class MGPipeClientBufferKind : Uint8 { + Element, // the bound GL_ELEMENT_ARRAY_BUFFER, for the index stream + IndirectCommand,// the bound GL_DRAW_INDIRECT_BUFFER, for the command block + IndirectCount, // the bound GL_PARAMETER_BUFFER, for an *IndirectCount form's count + }; + + // Reads `size` bytes at `offset` of the named buffer on the CPU. False when the bytes do + // not exist on this side, which is a refusal rather than a guess. + using MGPipeClientBufferReader = Bool (*)(void* user, MGPipeClientBufferKind kind, Uint64 offset, SizeT size, + void* destination); + + // One sub-draw's fetch: the element range it reads, how many instances it runs, and the + // raw base instance it shifts by. + struct MGPipeClientFetch { + MGPDrawRange Range; + Uint32 Instances = 1; + Uint32 BaseInstance = 0; + }; + + // The reader for a buffer whose bytes this side owns - the wire client's staged shadow and + // the monolith arm's frontend object are both read this way. Reconciles a pending GPU write + // first, because a shader-written index or command block is exactly the case this plan + // exists to handle. + inline Bool MGPipeReadBufferShadow(const SharedPtr& buffer, Uint64 offset, + SizeT size, void* destination) { + if (!buffer || offset > buffer->GetSize() || size > buffer->GetSize() - offset) return false; + buffer->SyncGpuWrites(); + buffer->DownloadSubData(destination, static_cast(offset), size); + return true; + } + + class MGPipeClientFetchPlan { + public: + // False when the draw's fetched elements cannot be determined - an index or command + // block this side cannot read, an index stream that leaves the client array, an index + // that runs below the base vertex. A caller must then refuse the draw rather than + // snapshot a guess: the elements it would leave out are the ones the draw reads. + Bool Build(const MGPipeClientDrawInputs& inputs, MGPipeClientBufferReader reader, void* user) { + m_inputs = inputs; + m_fetches.clear(); + m_vertices.clear(); + if (inputs.Indirect != nullptr) { + if (!ReadIndirectFetches(inputs, reader, user)) return false; + } else { + m_fetches.reserve(inputs.RangeCount); + for (Uint32 i = 0; i < inputs.RangeCount; ++i) { + m_fetches.push_back({inputs.Ranges[i], inputs.InstanceCount, inputs.BaseInstance}); + } + } + if (!inputs.WantVertices) return true; + return ReadVertices(inputs, reader, user); + } + + // The vertex elements the draw fetches through a vertex-rate array, ascending and + // unique. Only meaningful after a Build that wanted them. + const Vector& Vertices() const { return m_vertices; } + + // The elements an attribute of `divisor` fetches: the vertex-rate set for divisor 0, + // and one element per INSTANCE (baseInstance + instance / divisor) otherwise. Sorted + // and unique, and `out` is left empty for a draw that fetches nothing. + Bool Elements(Uint32 divisor, Vector& out) const { + out.clear(); + if (divisor == 0) { + // Vertex rate: every instance reads the same vertices, and the collection above + // already deduplicated them across the draw's sub-ranges. + out = m_vertices; + return true; + } + for (const auto& fetch : m_fetches) { + if (!fetch.Range.Count || !fetch.Instances) continue; + const Uint64 first = fetch.BaseInstance; + const Uint64 last = first + (fetch.Instances - 1u) / divisor; + for (Uint64 at = first; at <= last; ++at) out.push_back(at); + } + std::sort(out.begin(), out.end()); + out.erase(std::unique(out.begin(), out.end()), out.end()); + return true; + } + + const Vector& Fetches() const { return m_fetches; } + + private: + Bool ReadIndirectFetches(const MGPipeClientDrawInputs& inputs, MGPipeClientBufferReader reader, void* user) { + const MGPipeClientIndirect& indirect = *inputs.Indirect; + Uint32 count = indirect.DrawCount; + if (indirect.HasParameterBuffer) { + Uint32 actual = 0; + if (reader == nullptr || + !reader(user, MGPipeClientBufferKind::IndirectCount, indirect.ParameterOffset, sizeof(actual), + &actual)) { + return false; + } + count = std::min(count, actual); + } + const Uint64 commandBytes = inputs.IndexSize ? 5 * sizeof(Uint32) : 4 * sizeof(Uint32); + const Uint64 stride = indirect.Stride ? indirect.Stride : commandBytes; + m_fetches.reserve(count); + for (Uint32 i = 0; i < count; ++i) { + const Uint64 displacement = static_cast(i) * stride; + if (displacement > std::numeric_limits::max() - indirect.Offset) return false; + Uint32 words[5]{}; + if (reader == nullptr || + !reader(user, MGPipeClientBufferKind::IndirectCommand, indirect.Offset + displacement, + commandBytes, words)) { + return false; + } + Int32 bias = 0; + if (inputs.IndexSize) std::memcpy(&bias, &words[3], sizeof(bias)); + m_fetches.push_back({{words[2], words[0], bias}, words[1], words[inputs.IndexSize ? 4 : 3]}); + } + return true; + } + + // The index stream's own answer. Arrays are contiguous by construction; an indexed + // draw has to read the indices to learn which vertices it names, which is the whole + // reason a GPU-written element buffer makes this a readback rather than arithmetic. + Bool ReadVertices(const MGPipeClientDrawInputs& inputs, MGPipeClientBufferReader reader, void* user) { + for (const auto& fetch : m_fetches) { + if (!fetch.Range.Count || !fetch.Instances) continue; + if (!inputs.IndexSize) { + for (Uint64 n = 0; n < fetch.Range.Count; ++n) + m_vertices.push_back(static_cast(fetch.Range.Start) + n); + continue; + } + const Uint64 offset = static_cast(fetch.Range.Start) * inputs.IndexSize; + const Uint64 size = static_cast(fetch.Range.Count) * inputs.IndexSize; + if (size > std::numeric_limits::max()) return false; + Vector indexBytes; + const Uint8* data = nullptr; + if (inputs.ClientIndices != nullptr) { + if (offset > inputs.ClientIndexBytes || size > inputs.ClientIndexBytes - offset) return false; + data = static_cast(inputs.ClientIndices) + static_cast(offset); + } else { + indexBytes.resize(static_cast(size)); + if (reader == nullptr || + !reader(user, MGPipeClientBufferKind::Element, offset, indexBytes.size(), + indexBytes.data())) { + return false; + } + data = indexBytes.data(); + } + for (Uint32 n = 0; n < fetch.Range.Count; ++n) { + Uint32 index = 0; + std::memcpy(&index, data + static_cast(n) * inputs.IndexSize, inputs.IndexSize); + if (inputs.PrimitiveRestart && index == inputs.RestartIndex) continue; + const Int64 vertex = static_cast(index) + fetch.Range.IndexBias; + if (vertex < 0) return false; + m_vertices.push_back(static_cast(vertex)); + } + } + std::sort(m_vertices.begin(), m_vertices.end()); + m_vertices.erase(std::unique(m_vertices.begin(), m_vertices.end()), m_vertices.end()); + return true; + } + + MGPipeClientDrawInputs m_inputs{}; + Vector m_fetches; + Vector m_vertices; + }; +} // namespace MobileGL::MG_Pipe +#endif // MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH diff --git a/MobileGL/MG_Impl/Pipe/OwnedDrawInputs.h b/MobileGL/MG_Impl/Pipe/OwnedDrawInputs.h index bffa9080..8fc2982e 100644 --- a/MobileGL/MG_Impl/Pipe/OwnedDrawInputs.h +++ b/MobileGL/MG_Impl/Pipe/OwnedDrawInputs.h @@ -10,6 +10,7 @@ #pragma once #if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH +#include #include #include #include @@ -61,43 +62,56 @@ namespace MobileGL::MG_Pipe { for (const auto& attribute : attributes) needsVertexIndices |= attribute.Enabled && !attribute.Buffer && attribute.Divisor == 0; - Vector fetches; + // The element, command and parameter buffers are read through one reader for + // WHICH elements this draw fetches, and that question lives in + // ClientFetchPlan.h - the monolith arm asks the same one of the same bytes. + const SharedPtr elementBuffer = vao->GetIndexBufferBindingSlot().GetBoundObject(); + const SharedPtr commandBuffer = + m_context.GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); + const SharedPtr parameterBuffer = + m_context.GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject(); + ByteSources sources{&elementBuffer, &commandBuffer, ¶meterBuffer}; + + MGPipeClientDrawInputs inputs{}; + inputs.IndexSize = info.IndexSize; + inputs.Ranges = ranges; + inputs.RangeCount = rangeCount; + inputs.InstanceCount = info.InstanceCount; + inputs.BaseInstance = info.StartInstance; + inputs.PrimitiveRestart = (info.Flags & kDrawPrimitiveRestart) != 0; + const Bool fixedRestart = m_context.IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex); + inputs.RestartIndex = fixedRestart + ? (info.IndexSize == 1 ? 0xffu : info.IndexSize == 2 ? 0xffffu : 0xffffffffu) + : info.RestartIndex; + inputs.ClientIndices = clientIndices; + inputs.ClientIndexBytes = clientIndexBytes; + inputs.WantVertices = needsVertexIndices; + MGPipeClientIndirect indirectInputs{}; if (indirect != nullptr) { - if (!ReadIndirectFetches(info.IndexSize, *indirect, fetches)) return false; - } else { - fetches.reserve(rangeCount); - for (Uint32 i = 0; i < rangeCount; ++i) - fetches.push_back({ranges[i], info.InstanceCount, info.StartInstance}); + indirectInputs.Offset = indirect->Offset; + indirectInputs.ParameterOffset = indirect->ParameterOffset; + indirectInputs.Stride = indirect->Stride; + indirectInputs.DrawCount = indirect->DrawCount; + indirectInputs.HasParameterBuffer = !MGPipeHandleIsNull(indirect->ParameterBuffer); + inputs.Indirect = &indirectInputs; } + if (!m_plan.Build(inputs, &ReadBytes, &sources)) return false; - Vector vertices; - if (needsVertexIndices && !ReadVertexIndices(info, fetches, clientIndices, clientIndexBytes, vertices)) - return false; Array handles{}; + Vector referenced; for (SizeT location = 0; location < attributes.size(); ++location) { const auto& attribute = attributes[location]; if (!attribute.Enabled || attribute.Buffer) continue; const SizeT elementBytes = AttributeBytes(attribute); if (!elementBytes || attribute.Stride < 0) return false; - Vector instances; - const Vector* referenced = &vertices; - if (attribute.Divisor != 0) { - for (const auto& fetch : fetches) { - if (!fetch.Range.Count || !fetch.Instances) continue; - const Uint64 first = fetch.BaseInstance; - const Uint64 last = first + (fetch.Instances - 1u) / attribute.Divisor; - for (Uint64 at = first; at <= last; ++at) instances.push_back(at); - } - SortUnique(instances); - referenced = &instances; - } + if (!m_plan.Elements(static_cast(attribute.Divisor), referenced)) return false; const Uint64 stride = static_cast(attribute.Stride); - const Uint64 last = referenced->empty() ? 0 : referenced->back(); + const Uint64 last = referenced.empty() ? 0 : referenced.back(); if (stride != 0 && last > (std::numeric_limits::max() - elementBytes) / stride) return false; const SizeT byteCount = static_cast(last * stride + elementBytes); Vector bytes(byteCount, 0); - if (!referenced->empty()) { + if (!referenced.empty()) { if (attribute.Offset == 0 || byteCount > std::numeric_limits::max() - attribute.Offset) return false; const auto* source = reinterpret_cast(attribute.Offset); @@ -106,7 +120,7 @@ namespace MobileGL::MG_Pipe { } else { // Only dereference fetched elements: sparse indices and first>0 // do not grant permission to read intervening application memory. - for (const Uint64 vertex : *referenced) { + for (const Uint64 vertex : referenced) { const SizeT offset = static_cast(vertex * stride); std::memcpy(bytes.data() + offset, source + offset, elementBytes); } @@ -122,12 +136,28 @@ namespace MobileGL::MG_Pipe { } private: - struct Fetch { - MGPDrawRange Range; - Uint32 Instances; - Uint32 BaseInstance; + // The three buffers one draw can fetch bytes out of, and the reader that resolves + // them. `user` is a ByteSources owned by the caller's scope, so nothing here outlives + // the Prepare that built it. + struct ByteSources { + const SharedPtr* Elements = nullptr; + const SharedPtr* Commands = nullptr; + const SharedPtr* Parameters = nullptr; }; + static Bool ReadBytes(void* user, MGPipeClientBufferKind kind, Uint64 offset, SizeT size, void* destination) { + const auto& sources = *static_cast(user); + switch (kind) { + case MGPipeClientBufferKind::Element: + return MGPipeReadBufferShadow(*sources.Elements, offset, size, destination); + case MGPipeClientBufferKind::IndirectCommand: + return MGPipeReadBufferShadow(*sources.Commands, offset, size, destination); + case MGPipeClientBufferKind::IndirectCount: + return MGPipeReadBufferShadow(*sources.Parameters, offset, size, destination); + } + return false; + } + static SizeT AttributeBytes(const MG_State::GLState::VertexAttribute& attribute) { if (attribute.IsBgra || attribute.Type == DataType::Int2101010Rev || attribute.Type == DataType::Uint2101010Rev) return 4; @@ -143,11 +173,6 @@ namespace MobileGL::MG_Pipe { return attribute.Size > 0 && attribute.Size <= 4 ? component * attribute.Size : 0; } - static void SortUnique(Vector& values) { - std::sort(values.begin(), values.end()); - values.erase(std::unique(values.begin(), values.end()), values.end()); - } - static UniquePtr MakeOwnedBuffer(const void* bytes, SizeT size, BufferTarget target) { auto buffer = MakeUnique(0); const auto handle = MGPipeResourceTrackerInstance().Find(*buffer); @@ -157,82 +182,8 @@ namespace MobileGL::MG_Pipe { return buffer; } - static Bool ReadBuffer(const SharedPtr& buffer, Uint64 offset, SizeT size, void* destination) { - if (!buffer || offset > buffer->GetSize() || size > buffer->GetSize() - offset) return false; - // This waits for the actual server writeback when an earlier GPU draw, - // dispatch, or capture produced the index/command bytes. - buffer->SyncGpuWrites(); - buffer->DownloadSubData(destination, static_cast(offset), size); - return true; - } - - Bool ReadIndirectFetches(Uint8 indexSize, const MGPDrawIndirect& indirect, Vector& fetches) { - const auto& commands = m_context.GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject(); - Uint32 count = indirect.DrawCount; - if (!MGPipeHandleIsNull(indirect.ParameterBuffer)) { - const auto& parameter = m_context.GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject(); - Uint32 actual = 0; - if (!ReadBuffer(parameter, indirect.ParameterOffset, sizeof(actual), &actual)) return false; - count = std::min(count, actual); - } - const Uint64 commandBytes = indexSize ? 5 * sizeof(Uint32) : 4 * sizeof(Uint32); - const Uint64 stride = indirect.Stride ? indirect.Stride : commandBytes; - fetches.reserve(count); - for (Uint32 i = 0; i < count; ++i) { - const Uint64 displacement = static_cast(i) * stride; - if (displacement > std::numeric_limits::max() - indirect.Offset) return false; - Uint32 words[5]{}; - if (!ReadBuffer(commands, indirect.Offset + displacement, commandBytes, words)) return false; - Int32 bias = 0; - if (indexSize) std::memcpy(&bias, &words[3], sizeof(bias)); - fetches.push_back({{words[2], words[0], bias}, words[1], words[indexSize ? 4 : 3]}); - } - return true; - } - - Bool ReadVertexIndices(const MGPDrawInfo& info, const Vector& fetches, - const void* clientIndices, Uint64 clientIndexBytes, Vector& vertices) { - const auto& vao = m_context.GetBoundVertexArray(); - const auto& elementBuffer = vao->GetIndexBufferBindingSlot().GetBoundObject(); - const Bool restart = (info.Flags & kDrawPrimitiveRestart) != 0; - const Bool fixedRestart = m_context.IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex); - const Uint32 restartIndex = fixedRestart - ? (info.IndexSize == 1 ? 0xffu : info.IndexSize == 2 ? 0xffffu : 0xffffffffu) - : info.RestartIndex; - for (const auto& fetch : fetches) { - if (!fetch.Range.Count || !fetch.Instances) continue; - if (!info.IndexSize) { - for (Uint64 n = 0; n < fetch.Range.Count; ++n) - vertices.push_back(static_cast(fetch.Range.Start) + n); - continue; - } - const Uint64 offset = static_cast(fetch.Range.Start) * info.IndexSize; - const Uint64 size = static_cast(fetch.Range.Count) * info.IndexSize; - if (size > std::numeric_limits::max()) return false; - Vector indexBytes; - const Uint8* data = nullptr; - if (clientIndices != nullptr) { - if (offset > clientIndexBytes || size > clientIndexBytes - offset) return false; - data = static_cast(clientIndices) + static_cast(offset); - } else { - indexBytes.resize(static_cast(size)); - if (!ReadBuffer(elementBuffer, offset, indexBytes.size(), indexBytes.data())) return false; - data = indexBytes.data(); - } - for (Uint32 n = 0; n < fetch.Range.Count; ++n) { - Uint32 index = 0; - std::memcpy(&index, data + static_cast(n) * info.IndexSize, info.IndexSize); - if (restart && index == restartIndex) continue; - const Int64 vertex = static_cast(index) + fetch.Range.IndexBias; - if (vertex < 0) return false; - vertices.push_back(static_cast(vertex)); - } - } - SortUnique(vertices); - return true; - } - Context& m_context; + MGPipeClientFetchPlan m_plan; Array, kMGPipeMaxVertexAttribs> m_vertexBuffers{}; UniquePtr m_indexBuffer; Uint32 m_baseInstance = 0;