[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.
This commit is contained in:
2026-09-20 20:47:50 -04:00
parent e4ae19643d
commit 62bfe4615d
5 changed files with 625 additions and 113 deletions
@@ -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<MG_State::GLState::BufferObject>* 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<const ClientSnapshotSources*>(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<SizeT>(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<MG_State::GLState::BufferObject> elementBuffer =
currentVAO->GetIndexBufferBindingSlot().GetBoundObject();
ClientSnapshotSources sources{&elementBuffer};
MG_Pipe::MGPDrawRange range{elementStart, static_cast<Uint32>(count), baseVertex};
MG_Pipe::MGPipeClientDrawInputs inputs{};
inputs.IndexSize = indexSize;
inputs.Ranges = &range;
inputs.RangeCount = 1;
inputs.InstanceCount = static_cast<Uint32>(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<MG_State::GLState::BufferObject>& 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<SizeT>(index) * static_cast<SizeT>(stride);
if (indexSize == 0) {
DrawArraysIndirectCommand cmd{};
std::memcpy(&cmd, source + at, sizeof(cmd));
return SyncClientSideVertexArraysForFetch(0, cmd.first, static_cast<GLsizei>(cmd.count),
static_cast<GLsizei>(cmd.instanceCount), 0, cmd.baseInstance,
nullptr, 0);
}
DrawElementsIndirectCommand cmd{};
std::memcpy(&cmd, source + at, sizeof(cmd));
return SyncClientSideVertexArraysForFetch(indexSize, cmd.firstIndex, static_cast<GLsizei>(cmd.count),
static_cast<GLsizei>(cmd.instanceCount), cmd.baseVertex,
static_cast<Uint32>(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<Uint8>(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<Uint64>(count) * indexSize;
} else {
const Uint64 byteOffset = reinterpret_cast<Uint64>(indices);
if (byteOffset % indexSize != 0 ||
byteOffset / indexSize > std::numeric_limits<Uint32>::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<unsigned long long>(byteOffset), static_cast<Uint>(indexSize));
return false;
}
elementStart = static_cast<Uint32>(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<Uint8>(indexSize), drawIndirectBuffer))
return;
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(cmdByteOffset));
});
}
@@ -8284,6 +8434,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
SetCurrentBaseVertex(cmd.baseVertex);
const auto indexByteOffset = static_cast<SizeT>(cmd.firstIndex) * indexSize;
ForEachViewportRoutingPass([&] {
if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch(
commandBytes, commandOffset, stride, i, static_cast<Uint8>(indexSize), drawIndirectBuffer))
continue;
g_GLESFuncs.glDrawElementsInstancedBaseVertex(
mode, static_cast<GLsizei>(cmd.count), type, reinterpret_cast<const GLvoid*>(indexByteOffset),
static_cast<GLsizei>(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<const void*>(cmdByteOffset));
});
}
@@ -8360,6 +8516,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
SetCurrentDrawID(static_cast<Uint32>(i));
SetCurrentBaseInstance(cmd.baseInstance);
ForEachViewportRoutingPass([&] {
if (!VertexArrayImpl::SyncClientSideVertexArraysForIndirectFetch(
commandBytes, commandOffset, stride, i, 0, drawIndirectBuffer))
continue;
g_GLESFuncs.glDrawArraysInstanced(mode, static_cast<GLint>(cmd.first),
static_cast<GLsizei>(cmd.count),
static_cast<GLsizei>(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);
+134 -4
View File
@@ -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<MG_State::GLState::VertexArrayObject>& stateVAOObject,
const MG_Pipe::MGPipeClientFetchPlan& plan, Uint32 fetchBaseInstance) {
if (!stateVAOObject) return true;
Bind();
const auto& allAttributes = stateVAOObject->GetAllAttributes();
Vector<Uint64> 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<Uint32>(attrib.Divisor), elements)) return false;
if (elements.empty()) continue;
const auto* source = reinterpret_cast<const Uint8*>(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<SizeT>(attrib.Stride) : elementSize;
const Uint64 last = elements.back();
if (stride != 0 && last > (std::numeric_limits<SizeT>::max() - elementSize) / stride) return false;
const SizeT byteCount = static_cast<SizeT>(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<Uint8> snapshot(byteCount, 0);
for (const Uint64 element : elements) {
const SizeT offset = static_cast<SizeT>(element * stride);
std::memcpy(snapshot.data() + offset, source + offset, elementSize);
}
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId);
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(snapshot.size()),
snapshot.data(), GL_STREAM_DRAW);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(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<const void*>(BaseInstanceByteShift(attrib, fetchBaseInstance));
if (!attrib.IsInteger) {
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : attrib.Size;
g_GLESFuncs.glVertexAttribPointer(attribIndex, glSize,
MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
attrib.Normalized ? GL_TRUE : GL_FALSE,
static_cast<GLsizei>(stride), shift);
} else {
g_GLESFuncs.glVertexAttribIPointer(attribIndex, attrib.Size,
MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
static_cast<GLsizei>(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<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count) {
const SharedPtr<MG_State::GLState::VertexArrayObject>& 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<GLsizei>(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<const void*>(
attrib.Divisor == 0 ? 0u : static_cast<SizeT>(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<GLint>(GL_BGRA) : attrib.Size;
g_GLESFuncs.glVertexAttribPointer(
attribIndex, glSize, MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
attrib.Normalized ? GL_TRUE : GL_FALSE, static_cast<GLsizei>(stride), nullptr);
attrib.Normalized ? GL_TRUE : GL_FALSE, static_cast<GLsizei>(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<const void*>(BaseInstanceByteShift(attrib, fetchBaseInstance)));
} else {
g_GLESFuncs.glVertexAttribIPointer(attribIndex, attrib.Size,
MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
static_cast<GLsizei>(stride), nullptr);
static_cast<GLsizei>(stride),
// See the pointer call above: the divisor'd
// arrays' baseInstance shift is the pointer.
reinterpret_cast<const void*>(
BaseInstanceByteShift(attrib, fetchBaseInstance)));
}
}
+21 -1
View File
@@ -20,6 +20,10 @@
#if MOBILEGL_PIPE_PUSH
// P3a: the vertex-input payload views the handle arm of the VAO twin consumes.
#include <MG_Pipe/MGPipeTypes.h>
// 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 <MG_Impl/Pipe/ClientFetchPlan.h>
// 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<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count);
const SharedPtr<MG_State::GLState::VertexArrayObject>& 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<MG_State::GLState::VertexArrayObject>& stateVAOObject,
const MG_Pipe::MGPipeClientFetchPlan& plan, Uint32 fetchBaseInstance);
#endif
Uint GetBackendVertexArrayId() const { return m_backendVAOId; }
void Bind() const;
+240
View File
@@ -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 <MG_Pipe/MGPipeTypes.h>
#include <MG_State/GLState/BufferState/BufferObject.h>
#include <algorithm>
#include <cstring>
#include <limits>
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<MG_State::GLState::BufferObject>& 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<SizeT>(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<Uint64>& 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<Uint64>& 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<MGPipeClientFetch>& 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<Uint64>(i) * stride;
if (displacement > std::numeric_limits<Uint64>::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<Uint64>(fetch.Range.Start) + n);
continue;
}
const Uint64 offset = static_cast<Uint64>(fetch.Range.Start) * inputs.IndexSize;
const Uint64 size = static_cast<Uint64>(fetch.Range.Count) * inputs.IndexSize;
if (size > std::numeric_limits<SizeT>::max()) return false;
Vector<Uint8> indexBytes;
const Uint8* data = nullptr;
if (inputs.ClientIndices != nullptr) {
if (offset > inputs.ClientIndexBytes || size > inputs.ClientIndexBytes - offset) return false;
data = static_cast<const Uint8*>(inputs.ClientIndices) + static_cast<SizeT>(offset);
} else {
indexBytes.resize(static_cast<SizeT>(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<SizeT>(n) * inputs.IndexSize, inputs.IndexSize);
if (inputs.PrimitiveRestart && index == inputs.RestartIndex) continue;
const Int64 vertex = static_cast<Int64>(index) + fetch.Range.IndexBias;
if (vertex < 0) return false;
m_vertices.push_back(static_cast<Uint64>(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<MGPipeClientFetch> m_fetches;
Vector<Uint64> m_vertices;
};
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH
+59 -108
View File
@@ -10,6 +10,7 @@
#pragma once
#if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/ClientFetchPlan.h>
#include <MG_Impl/Pipe/VertexInputEmit.h>
#include <MG_State/GLState/BufferState/BufferObject.h>
#include <algorithm>
@@ -61,43 +62,56 @@ namespace MobileGL::MG_Pipe {
for (const auto& attribute : attributes)
needsVertexIndices |= attribute.Enabled && !attribute.Buffer && attribute.Divisor == 0;
Vector<Fetch> 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<Buffer> elementBuffer = vao->GetIndexBufferBindingSlot().GetBoundObject();
const SharedPtr<Buffer> commandBuffer =
m_context.GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
const SharedPtr<Buffer> parameterBuffer =
m_context.GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
ByteSources sources{&elementBuffer, &commandBuffer, &parameterBuffer};
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<Uint64> vertices;
if (needsVertexIndices && !ReadVertexIndices(info, fetches, clientIndices, clientIndexBytes, vertices))
return false;
Array<MGPipeHandle, kMGPipeMaxVertexAttribs> handles{};
Vector<Uint64> 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<Uint64> instances;
const Vector<Uint64>* 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<Uint32>(attribute.Divisor), referenced)) return false;
const Uint64 stride = static_cast<Uint32>(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<Uint32>::max() - elementBytes) / stride)
return false;
const SizeT byteCount = static_cast<SizeT>(last * stride + elementBytes);
Vector<Uint8> bytes(byteCount, 0);
if (!referenced->empty()) {
if (!referenced.empty()) {
if (attribute.Offset == 0 || byteCount > std::numeric_limits<SizeT>::max() - attribute.Offset)
return false;
const auto* source = reinterpret_cast<const Uint8*>(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<SizeT>(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<Buffer>* Elements = nullptr;
const SharedPtr<Buffer>* Commands = nullptr;
const SharedPtr<Buffer>* Parameters = nullptr;
};
static Bool ReadBytes(void* user, MGPipeClientBufferKind kind, Uint64 offset, SizeT size, void* destination) {
const auto& sources = *static_cast<const ByteSources*>(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<Uint64>& values) {
std::sort(values.begin(), values.end());
values.erase(std::unique(values.begin(), values.end()), values.end());
}
static UniquePtr<Buffer> MakeOwnedBuffer(const void* bytes, SizeT size, BufferTarget target) {
auto buffer = MakeUnique<Buffer>(0);
const auto handle = MGPipeResourceTrackerInstance().Find(*buffer);
@@ -157,82 +182,8 @@ namespace MobileGL::MG_Pipe {
return buffer;
}
static Bool ReadBuffer(const SharedPtr<Buffer>& 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<SizeT>(offset), size);
return true;
}
Bool ReadIndirectFetches(Uint8 indexSize, const MGPDrawIndirect& indirect, Vector<Fetch>& 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<Uint64>(i) * stride;
if (displacement > std::numeric_limits<Uint64>::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<Fetch>& fetches,
const void* clientIndices, Uint64 clientIndexBytes, Vector<Uint64>& 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<Uint64>(fetch.Range.Start) + n);
continue;
}
const Uint64 offset = static_cast<Uint64>(fetch.Range.Start) * info.IndexSize;
const Uint64 size = static_cast<Uint64>(fetch.Range.Count) * info.IndexSize;
if (size > std::numeric_limits<SizeT>::max()) return false;
Vector<Uint8> indexBytes;
const Uint8* data = nullptr;
if (clientIndices != nullptr) {
if (offset > clientIndexBytes || size > clientIndexBytes - offset) return false;
data = static_cast<const Uint8*>(clientIndices) + static_cast<SizeT>(offset);
} else {
indexBytes.resize(static_cast<SizeT>(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<SizeT>(n) * info.IndexSize, info.IndexSize);
if (restart && index == restartIndex) continue;
const Int64 vertex = static_cast<Int64>(index) + fetch.Range.IndexBias;
if (vertex < 0) return false;
vertices.push_back(static_cast<Uint64>(vertex));
}
}
SortUnique(vertices);
return true;
}
Context& m_context;
MGPipeClientFetchPlan m_plan;
Array<UniquePtr<Buffer>, kMGPipeMaxVertexAttribs> m_vertexBuffers{};
UniquePtr<Buffer> m_indexBuffer;
Uint32 m_baseInstance = 0;