mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-10 11:33:24 +09:00
249 lines
13 KiB
C++
249 lines
13 KiB
C++
// 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.
|
|
//
|
|
// COMPILED IN EVERY FLAVOR, and the guard it used to carry (`MOBILEGL_BUILD_DISAGGREGATED &&
|
|
// MOBILEGL_PIPE_PUSH`) was the same mistake the monolith arm's callers carried: the
|
|
// MONOLITH arm is not a split-arm fallback, it is the arm the pull build runs. An indexed or
|
|
// indirect draw over a client-memory array staged nothing there either, so the question has
|
|
// to be answerable - and answered - in the transport-free library too. The plan itself is
|
|
// pure arithmetic over the draw's own words; what changes per flavor is only who reads the
|
|
// bytes (the caller's reader), never the answer.
|
|
#pragma once
|
|
|
|
#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));
|
|
}
|
|
}
|
|
// An array draw's single range arrives ascending already; sorting it again was most of
|
|
// the client's time on a client-array draw.
|
|
if (!std::is_sorted(m_vertices.begin(), m_vertices.end())) 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
|