mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 10:03:23 +09:00
- Layout only: token streams match up to split literals, continuations and using order - The size totals fall, so the structure baseline is lowered
1124 lines
69 KiB
C++
1124 lines
69 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectGLES/ManagersVertexArrays.cpp
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
#include "ManagersInternal.h"
|
|
|
|
namespace MobileGL::MG_Backend::DirectGLES {
|
|
namespace BufferImpl {
|
|
// P13 W4a (§1.4): THE GPU-WRITE REFRESH OF AN ALIASING STORE. Monolith's record arm reads
|
|
// the frontend shadow in place (StagedShadowStore keeps the caller's pointer), so a store a
|
|
// shader wrote is refreshed the way the client refreshes it under split: the bytes are
|
|
// read back from this process's GL buffer and handed to OnBufferWriteback, whose consumer
|
|
// (the frontend, one call away) copies them into that shadow. The backend never writes
|
|
// frontend memory itself. Ops_H_Readback's order, for its reasons: queued uploads first,
|
|
// resident writes next, then the read, the serial stamp and - last - the epoch bump.
|
|
const Uint8* RefreshAliasedStoreFromGpu(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res,
|
|
const char* site) {
|
|
if (!CanTouchGLNow() || resource.id == 0 || !resource.storageInitialized || resource.pendingRespecify ||
|
|
resource.contextGeneration != g_bufferContextGeneration || !g_GLESFuncs.glMapBufferRange ||
|
|
!g_GLESFuncs.glUnmapBuffer) {
|
|
MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU but its GL store cannot be read "
|
|
"back here, so the CPU reads the shadow as it is",
|
|
site, res.Slot, res.Gen);
|
|
return SplitHostBytes(resource);
|
|
}
|
|
const SizeT width = ResourceWidthOf(res);
|
|
const SizeT size = std::min(width, resource.storageSize);
|
|
if (size == 0) return SplitHostBytes(resource);
|
|
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == nullptr) {
|
|
MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU and no reverse channel is "
|
|
"installed to refresh the shadow it aliases",
|
|
site, res.Slot, res.Gen);
|
|
return SplitHostBytes(resource);
|
|
}
|
|
FlushPendingRangesFrom(resource, resource.hostBytes, width);
|
|
DrainResidentWritesNow(resource);
|
|
BindBufferId(TempBufferTarget, resource.id);
|
|
void* mapped =
|
|
g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size), GL_MAP_READ_BIT);
|
|
if (mapped == nullptr) {
|
|
MGLOG_E_ONCE("%s: glMapBufferRange(read) failed for GPU-written buffer %u; the CPU reads the "
|
|
"shadow as it is",
|
|
site, resource.id);
|
|
return SplitHostBytes(resource);
|
|
}
|
|
MG_Pipe::gMGPipeCallbacks.OnBufferWriteback(res, 0,
|
|
MG_Pipe::MGPBlobRef{reinterpret_cast<Uint64>(mapped),
|
|
static_cast<Uint64>(size),
|
|
MG_Pipe::kMGHostSpanSegNone, 0});
|
|
if (resource.hostBytes == nullptr) {
|
|
// Every content-carrying record and every respecify of a store with a size hands
|
|
// its base over on this arm, so no base here means a shape nothing recorded. Keep
|
|
// the bytes in a private copy rather than answer "nothing" for GPU-written bytes.
|
|
MGLOG_W_ONCE("%s: GPU-written buffer {%u, %u} has no recorded shadow base; its refreshed "
|
|
"bytes are kept in a private copy",
|
|
site, res.Slot, res.Gen);
|
|
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
|
|
resource.hostBytes = ServerStaged().AdoptPrivateCopy(&resource, width, mapped, size);
|
|
}
|
|
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
|
|
resource.serverGpuWritten = false;
|
|
resource.syncedChangeSerial = ResourceSerialOf(res);
|
|
BumpBufferMutationEpoch();
|
|
return resource.hostBytes;
|
|
}
|
|
|
|
// P8-C. The declaration says what; this says in what order, and it is Ops_H_Readback's
|
|
// order because it is the same question asked by the server of itself instead of by the
|
|
// client: (1) queued uploads land in the GL store first - reading back past them would
|
|
// revert them in the copy - under the same staged-coverage assertion that drain carries;
|
|
// (2) queued resident writes land (GPU-ordered, behind every command already recorded);
|
|
// (3) the whole store is read back into the staged copy, which leaves its coverage whole
|
|
// and so also answers RequireStagedCoverage for an orphaned store's first copy; (4) the
|
|
// serial stamp and the epoch bump, as Ops_H_ReadbackTracked does them.
|
|
const Uint8* SplitHostBytesForCpuRead(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res,
|
|
const char* site) {
|
|
// A coherent store - T0's import, or (P13 W4a) a map monolith's record arm donated -
|
|
// has no copy: its bytes are read through the map once queued writes have landed.
|
|
const Bool coherent = resource.persistentMapped && resource.persistentPtr != nullptr;
|
|
Bool residentWritesQueued = false;
|
|
if (coherent) {
|
|
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
|
|
residentWritesQueued = !resource.pendingResidentWrites.empty();
|
|
}
|
|
if (!resource.serverGpuWritten && !residentWritesQueued) return SplitHostBytes(resource);
|
|
if (!ServerStaged().CopiesIntoServerStorage() && !coherent) {
|
|
// P13 W4a (§1.4): an ALIASING store (monolith's record arm) is refreshed through
|
|
// the frontend - the writeback lands in the shadow the base points into. Any other
|
|
// non-copying store has no marks to answer.
|
|
if (MG_Config::RecordArmAliasesFrontend()) return RefreshAliasedStoreFromGpu(resource, res, site);
|
|
return SplitHostBytes(resource);
|
|
}
|
|
if (!CanTouchGLNow() || resource.id == 0 || !resource.storageInitialized || resource.pendingRespecify ||
|
|
resource.contextGeneration != g_bufferContextGeneration || !g_GLESFuncs.glMapBufferRange ||
|
|
!g_GLESFuncs.glUnmapBuffer) {
|
|
MGLOG_E_ONCE("%s: buffer {%u, %u} was written on the GPU but its GL store cannot be read "
|
|
"back here, so the CPU reads the staged copy as it is",
|
|
site, res.Slot, res.Gen);
|
|
return SplitHostBytes(resource);
|
|
}
|
|
if (coherent) {
|
|
// T0: the copy IS this server's coherent map of the client's pages. Landing the
|
|
// queued writes and finishing makes them, and every shader write already queued,
|
|
// visible through it - Ops_H_Readback's persistent-map arm, for the same reason.
|
|
DrainResidentWritesNow(resource);
|
|
if (g_GLESFuncs.glFinish) g_GLESFuncs.glFinish();
|
|
resource.serverGpuWritten = false;
|
|
return SplitHostBytes(resource);
|
|
}
|
|
const SizeT width = ResourceWidthOf(res);
|
|
const SizeT size = std::min(width, resource.storageSize);
|
|
if (size == 0) return SplitHostBytes(resource);
|
|
MGL_SERVER_STAGED_REQUIRE_PENDING(resource, resource.hostBytes, width, "gpu_written_refresh_flush_pending");
|
|
FlushPendingRangesFrom(resource, resource.hostBytes, width);
|
|
DrainResidentWritesNow(resource);
|
|
BindBufferId(TempBufferTarget, resource.id);
|
|
void* mapped =
|
|
g_GLESFuncs.glMapBufferRange(TempBufferTarget, 0, static_cast<GLsizeiptr>(size), GL_MAP_READ_BIT);
|
|
if (mapped == nullptr) {
|
|
MGLOG_E_ONCE("%s: glMapBufferRange(read) failed for GPU-written buffer %u; the CPU reads the "
|
|
"staged copy as it is",
|
|
site, resource.id);
|
|
return SplitHostBytes(resource);
|
|
}
|
|
{
|
|
const std::lock_guard<std::mutex> lock(resource.pendingMutex);
|
|
resource.hostBytes = MGL_SERVER_STAGED_ADOPT(resource, width, mapped, 0, size);
|
|
}
|
|
g_GLESFuncs.glUnmapBuffer(TempBufferTarget);
|
|
resource.serverGpuWritten = false;
|
|
resource.syncedChangeSerial = ResourceSerialOf(res);
|
|
BumpBufferMutationEpoch();
|
|
MGLOG_D("%s: refreshed the staged copy of GPU-written buffer {%u, %u} (%zu bytes)", site, res.Slot, res.Gen,
|
|
size);
|
|
return resource.hostBytes;
|
|
}
|
|
} // namespace BufferImpl
|
|
|
|
namespace SamplerViewImpl {
|
|
BackendSamplerViewTable g_backendSamplerViews;
|
|
|
|
BackendSamplerViewObject* FindSamplerViewForHandle(MG_Pipe::MGPipeHandle view) {
|
|
auto* twin = g_backendSamplerViews.FindByHandle(view);
|
|
return twin ? twin->get() : nullptr;
|
|
}
|
|
} // namespace SamplerViewImpl
|
|
|
|
namespace VertexArrayImpl {
|
|
namespace {
|
|
SizeT GetDataTypeSize(DataType type) {
|
|
switch (type) {
|
|
case DataType::Int8:
|
|
case DataType::Uint8:
|
|
return 1;
|
|
case DataType::Int16:
|
|
case DataType::Uint16:
|
|
case DataType::Float16:
|
|
return 2;
|
|
case DataType::Int32:
|
|
case DataType::Uint32:
|
|
case DataType::Float32:
|
|
case DataType::Fixed32:
|
|
return 4;
|
|
case DataType::Float64:
|
|
return 8;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Tightly-packed byte size of one vertex element: 4 for the 2_10_10_10 types and GL_BGRA
|
|
// (one 32-bit word / 4 bytes), componentSize * size otherwise. 0 for unknown types.
|
|
SizeT GetAttributeByteSize(DataType type, int size, Bool isBgra) {
|
|
if (type == DataType::Int2101010Rev || type == DataType::Uint2101010Rev || isBgra) {
|
|
return 4;
|
|
}
|
|
const SizeT componentSize = GetDataTypeSize(type);
|
|
return componentSize == 0 ? 0 : componentSize * static_cast<SizeT>(size);
|
|
}
|
|
|
|
// Deinterleaves elementCount elements of componentCount doubles into a tightly packed
|
|
// float32 stream - the fetch half of the fp64 demotion the shader side already does
|
|
// unconditionally (DemoteFloat64Pass). GL byte strides and offsets are arbitrary, so
|
|
// no component carries an 8-byte alignment guarantee and each is copied out before it
|
|
// is narrowed.
|
|
void NarrowDoubleStreamToFloat32(const Uint8* sourceBase, SizeT sourceStride, SizeT componentCount,
|
|
SizeT elementCount, Vector<Float>& outData) {
|
|
outData.resize(elementCount * componentCount);
|
|
for (SizeT element = 0; element < elementCount; ++element) {
|
|
const Uint8* sourceElement = sourceBase + element * sourceStride;
|
|
Float* destinationElement = outData.data() + element * componentCount;
|
|
for (SizeT component = 0; component < componentCount; ++component) {
|
|
Double value = 0.0;
|
|
Memcpy(&value, sourceElement + component * sizeof(Double), sizeof(Double));
|
|
destinationElement[component] = static_cast<Float>(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// GL_BGRA as a vertex SIZE is desktop-only - ES rejects it outright - and it means
|
|
// nothing more than "the element's first and third components are stored swapped".
|
|
// So the element is put back in RGBA order and fetched as an ordinary size-4 array of
|
|
// the same type: for GL_UNSIGNED_BYTE the first and third BYTES swap, for the two
|
|
// 2_10_10_10_REV types the 10-bit fields in bits 0-9 and 20-29 (the core spec's packed
|
|
// component layout: with BGRA, x comes from bits 20-29 and z from bits 0-9). The 2-bit
|
|
// w field and the middle field stay where they are. Elements are copied word-wise, so
|
|
// no source alignment is assumed; the output is tightly packed, one word per element.
|
|
void SwizzleBgraElementToRgba(Uint8* element, DataType type) {
|
|
if (type == DataType::Uint8) {
|
|
std::swap(element[0], element[2]);
|
|
return;
|
|
}
|
|
Uint32 word = 0;
|
|
Memcpy(&word, element, sizeof(word));
|
|
constexpr Uint32 kField = 0x3FFu;
|
|
const Uint32 low = word & kField;
|
|
const Uint32 high = (word >> 20) & kField;
|
|
word = (word & ~(kField | (kField << 20))) | high | (low << 20);
|
|
Memcpy(element, &word, sizeof(word));
|
|
}
|
|
|
|
void SwizzleBgraStreamToRgba(const Uint8* sourceBase, SizeT sourceStride, SizeT elementCount, DataType type,
|
|
Vector<Uint8>& outData) {
|
|
outData.resize(elementCount * 4);
|
|
for (SizeT element = 0; element < elementCount; ++element) {
|
|
Uint8* destination = outData.data() + element * 4;
|
|
Memcpy(destination, sourceBase + element * sourceStride, 4);
|
|
SwizzleBgraElementToRgba(destination, type);
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
BackendVertexArrayObject::BackendVertexArrayObject() {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
m_clientAttributeBufferIds.fill(0);
|
|
m_convertedAttributeBufferIds.fill(0);
|
|
g_GLESFuncs.glGenVertexArrays(1, &m_backendVAOId);
|
|
if (m_backendVAOId == 0) {
|
|
MGLOG_E_ONCE("Failed to generate vertex array object.");
|
|
MGLOG_E_ONCE("ES glGetError(): %s", MG_Util::ConvertGLEnumToString(g_GLESFuncs.glGetError()).c_str());
|
|
} else {
|
|
MGLOG_D("Generated vertex array object with ID: %u.", m_backendVAOId);
|
|
}
|
|
}
|
|
|
|
BackendVertexArrayObject::~BackendVertexArrayObject() {
|
|
if (InProcessTeardown()) {
|
|
return; // see InProcessTeardown(): the driver may be unloaded already
|
|
}
|
|
if (m_backendVAOId != 0) {
|
|
NoteVAOIdDeleted(m_backendVAOId);
|
|
g_GLESFuncs.glDeleteVertexArrays(1, &m_backendVAOId);
|
|
m_backendVAOId = 0;
|
|
}
|
|
for (auto& bufferId : m_clientAttributeBufferIds) {
|
|
if (bufferId != 0) {
|
|
BufferImpl::NoteBufferIdDeleted(bufferId);
|
|
g_GLESFuncs.glDeleteBuffers(1, &bufferId);
|
|
bufferId = 0;
|
|
}
|
|
}
|
|
for (auto& bufferId : m_convertedAttributeBufferIds) {
|
|
if (bufferId != 0) {
|
|
BufferImpl::NoteBufferIdDeleted(bufferId);
|
|
g_GLESFuncs.glDeleteBuffers(1, &bufferId);
|
|
bufferId = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
Uint g_boundBackendVAOId = 0;
|
|
Bool g_boundBackendVAOKnown = false;
|
|
} // namespace
|
|
|
|
void BindBackendVAOId(Uint id) {
|
|
if (g_boundBackendVAOKnown && g_boundBackendVAOId == id) {
|
|
return;
|
|
}
|
|
g_GLESFuncs.glBindVertexArray(id);
|
|
g_boundBackendVAOId = id;
|
|
g_boundBackendVAOKnown = true;
|
|
}
|
|
|
|
void InvalidateVAOBindingCache() {
|
|
g_boundBackendVAOKnown = false;
|
|
}
|
|
|
|
void NoteVAOIdDeleted(Uint id) {
|
|
if (g_boundBackendVAOKnown && g_boundBackendVAOId == id) {
|
|
g_boundBackendVAOId = 0; // glDeleteVertexArrays reverts a bound VAO to 0
|
|
}
|
|
}
|
|
|
|
void BackendVertexArrayObject::Bind() const {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
BindBackendVAOId(m_backendVAOId);
|
|
}
|
|
|
|
// P15: the driver-VAO shadow (Managers.h). Callers have bound this twin's VAO.
|
|
|
|
namespace {
|
|
inline Bool VaoShadowOn() {
|
|
return MG_Config::Features.EsprytVaoShadow != MG_Config::QuirkOverride::ForceOff;
|
|
}
|
|
} // namespace
|
|
|
|
void BackendVertexArrayObject::HostSetEnabled(Uint index, Bool enabled) {
|
|
const Uint32 bit = 1u << index;
|
|
if (VaoShadowOn() && (m_hostEnabledKnownMask & bit) && ((m_hostEnabledMask & bit) != 0) == enabled) return;
|
|
if (enabled)
|
|
g_GLESFuncs.glEnableVertexAttribArray(index);
|
|
else
|
|
g_GLESFuncs.glDisableVertexAttribArray(index);
|
|
m_hostEnabledKnownMask |= bit;
|
|
m_hostEnabledMask = enabled ? (m_hostEnabledMask | bit) : (m_hostEnabledMask & ~bit);
|
|
}
|
|
|
|
void BackendVertexArrayObject::HostSetDivisor(Uint index, Uint32 divisor) {
|
|
const Uint32 bit = 1u << index;
|
|
if (VaoShadowOn() && (m_hostDivisorKnownMask & bit) && m_hostDivisor[index] == divisor) return;
|
|
g_GLESFuncs.glVertexAttribDivisor(index, divisor);
|
|
m_hostDivisorKnownMask |= bit;
|
|
m_hostDivisor[index] = divisor;
|
|
}
|
|
|
|
void BackendVertexArrayObject::HostForgetAll() {
|
|
m_hostEnabledKnownMask = 0;
|
|
m_hostDivisorKnownMask = 0;
|
|
for (auto& pointer : m_hostPointer)
|
|
pointer.valid = false;
|
|
}
|
|
|
|
// ES 3.1 core. Queried through the loader rather than the version, because the whole
|
|
// point of using it is to express something the pointer API cannot, and falling back
|
|
// silently on a driver that lacks it is better than crashing on a null entry point.
|
|
inline Bool HasVertexBindingApi() {
|
|
return g_GLESFuncs.glBindVertexBuffer != nullptr && g_GLESFuncs.glVertexAttribFormat != nullptr &&
|
|
g_GLESFuncs.glVertexAttribIFormat != nullptr && g_GLESFuncs.glVertexAttribBinding != nullptr &&
|
|
g_GLESFuncs.glVertexBindingDivisor != nullptr;
|
|
}
|
|
|
|
Bool BackendUsesNativeBaseInstance() {
|
|
return g_GLESCapabilities.SupportsBaseInstance;
|
|
}
|
|
|
|
// The "+ baseInstance" of GL's instanced-array element index, expressed as a byte shift
|
|
// of the array's own offset. Only divisor'd arrays step per instance, so only they move.
|
|
//
|
|
// baseInstance is added to the ELEMENT index, not to instance/divisor - the divisor
|
|
// therefore does not appear here, and the shift is a whole number of strides.
|
|
//
|
|
// A resolved stride of zero is the binding model's "never advance" (see
|
|
// VertexAttribute::Stride), so such an array reads the same element for every instance
|
|
// and a baseInstance cannot move it. The arithmetic already yields zero for that case.
|
|
inline SizeT BaseInstanceByteShift(const MG_State::GLState::VertexAttribute& attrib, Uint32 baseInstance) {
|
|
if (baseInstance == 0 || attrib.Divisor == 0) {
|
|
return 0;
|
|
}
|
|
return static_cast<SizeT>(baseInstance) * static_cast<SizeT>(attrib.Stride);
|
|
}
|
|
|
|
// ---- the handle arm's small helpers -------------------------------------------
|
|
//
|
|
// Each one is its object-shaped counterpart above with the frontend reads replaced by
|
|
// the two wire views, and nothing else. Divisor is deliberately NOT on the attribute
|
|
// view: it is resolved per binding point and rides in MGPVertexBuffer::Divisor, which
|
|
// is where glVertexAttribDivisor reads it (D-G2).
|
|
|
|
// The entry of the applied set that feeds ATTRIBUTE `attributeIndex`.
|
|
//
|
|
// THE KEY IS THE ATTRIBUTE INDEX, NOT MGPVertexAttribWire::BindingIndex, and the two
|
|
// live in different spaces. Espryt consumes RESOLVED attributes, so the client emits
|
|
// set_vertex_buffers as one entry per attribute slot with
|
|
// MGPVertexBuffer::BindingIndex == the attribute index (EmitVertexBuffers in VertexInputEmit.h -
|
|
// "Espryt consumes RESOLVED attributes, so the set is one entry per attribute
|
|
// slot with BindingIndex == the attribute index"), each carrying that attribute's
|
|
// already-folded buffer, stride and divisor. MGPVertexAttribWire::BindingIndex is the
|
|
// OTHER thing: the GL binding POINT the attribute was attached to by
|
|
// glVertexAttribBinding, which is the key of the binding-point view this arm never
|
|
// reads (it needs no separate view, because the resolution already happened on the
|
|
// client). The two coincide whenever the attribute was configured through
|
|
// glVertexAttribPointer, which is why every scenario that uses the pointer API stayed
|
|
// green while glVertexAttribBinding(2, 3) / (0, 5) fetched the wrong entry or none -
|
|
// KHR-GL43.vertex_attrib_binding's whole subject.
|
|
const MG_Pipe::MGPVertexBuffer* VertexBufferForAttributeIndex(const MG_Pipe::MGPipeApplierState& st,
|
|
Uint32 attributeIndex) {
|
|
if (st.VertexBufferCount == 0) return nullptr;
|
|
const Uint32 begin = st.VertexBufferStart;
|
|
const Uint32 end = begin + st.VertexBufferCount;
|
|
if (attributeIndex >= begin && attributeIndex < end && attributeIndex < MG_Pipe::kMGPipeMaxVertexAttribs &&
|
|
st.VertexBuffers[attributeIndex].BindingIndex == attributeIndex) {
|
|
return &st.VertexBuffers[attributeIndex];
|
|
}
|
|
for (Uint32 i = begin; i < end && i < MG_Pipe::kMGPipeMaxVertexAttribs; ++i) {
|
|
if (st.VertexBuffers[i].BindingIndex == attributeIndex) return &st.VertexBuffers[i];
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// BaseInstanceByteShift, on the wire views. baseInstance is added to the ELEMENT index,
|
|
// so the divisor does not appear here; a resolved stride of zero never advances and the
|
|
// arithmetic already yields zero for it.
|
|
inline SizeT BaseInstanceByteShiftWire(Int32 stride, Uint32 divisor, Uint32 baseInstance) {
|
|
if (baseInstance == 0 || divisor == 0) return 0;
|
|
return static_cast<SizeT>(baseInstance) * static_cast<SizeT>(stride);
|
|
}
|
|
|
|
// (The attribute-buffer bind lives in SyncToBackendFromApplier's pointer path now: it
|
|
// resolves the driver id from the slot table and skips the bind and the pointer when the
|
|
// driver VAO already holds them. It does NOT ensure storage: on this arm the draw's
|
|
// buffers were ensured by SyncNeccessaryBuffers earlier in the same PrepareForDraw.)
|
|
|
|
// SyncZeroStrideAttribute on the wire views: the one spelling that can carry a resolved
|
|
// stride of zero, which glVertexAttribPointer's zero means the opposite of.
|
|
inline Bool SyncZeroStrideAttributeByHandle(Uint attribIndex, const MGPVertexAttribWire& attrib,
|
|
const MG_Pipe::MGPVertexBuffer& binding) {
|
|
if (MG_Pipe::MGPipeHandleIsNull(binding.Res)) {
|
|
MGLOG_W_ONCE("Zero-stride attribute %u has no bound buffer, skipping.", attribIndex);
|
|
return false;
|
|
}
|
|
auto* backendResource = BufferImpl::FindBufferResourceForHandle(binding.Res);
|
|
if (!backendResource || backendResource->id == 0) {
|
|
MGLOG_E_ONCE("No backend buffer for zero-stride attribute %u, cannot bind it.", attribIndex);
|
|
return false;
|
|
}
|
|
if (!attrib.IsInteger) {
|
|
const GLint glSize = static_cast<GLint>(attrib.Size);
|
|
g_GLESFuncs.glVertexAttribFormat(attribIndex, glSize,
|
|
MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type)),
|
|
attrib.Normalized ? GL_TRUE : GL_FALSE, 0);
|
|
} else {
|
|
g_GLESFuncs.glVertexAttribIFormat(attribIndex, static_cast<GLint>(attrib.Size),
|
|
MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type)),
|
|
0);
|
|
}
|
|
g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex);
|
|
// The resolved offset goes on the binding point, not into a relative offset (the
|
|
// relative offset is capped by GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET, a buffer
|
|
// offset is not). BindBufferId is bypassed deliberately.
|
|
g_GLESFuncs.glBindVertexBuffer(attribIndex, backendResource->id, static_cast<GLintptr>(attrib.Offset), 0);
|
|
return true;
|
|
}
|
|
|
|
// The record the applier holds for the bound vertex-elements CSO, or null.
|
|
const MG_Pipe::MGPipeVertexElementsRecord* BoundVertexElementsRecord(const MG_Pipe::MGPipeApplierState& st) {
|
|
const MG_Pipe::MGPipeHandle cso = st.BoundVertexElements;
|
|
if (MG_Pipe::MGPipeHandleIsNull(cso)) return nullptr;
|
|
if (cso.Slot >= st.VertexElementsCsos.size()) return nullptr;
|
|
const auto& record = st.VertexElementsCsos[cso.Slot];
|
|
if (!record.Live || record.Gen != cso.Gen) return nullptr;
|
|
return &record;
|
|
}
|
|
|
|
// P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract; the body is the one
|
|
// SamplerImpl::ResolveSamplerCsoTwin already runs, with the sync left to vi.
|
|
BackendVertexArrayObject* ResolveVaoTwin(MG_Pipe::MGPipeHandle elements) {
|
|
if (MG_Pipe::MGPipeHandleIsNull(elements)) return nullptr;
|
|
// THE RECORD FIRST, before the table is touched: a handle with no record means the
|
|
// client named a vertex-elements CSO it never described (or one it deleted while a
|
|
// binding still names it), and adopting a slot for it would leave a twin nothing
|
|
// can ever sync. The applier's own Gen check is what turns a stale handle into this
|
|
// null rather than into its successor's record.
|
|
if (PipeRecordAt(MG_Pipe::MGPipeApplier().VertexElementsCsos, elements) == nullptr) {
|
|
MGLOG_E_ONCE("MGPipe: vertex-elements CSO {%u, %u} has no applier record on the handle "
|
|
"arm, so no driver VAO can be built for it and the draw keeps what is bound",
|
|
elements.Slot, elements.Gen);
|
|
return nullptr;
|
|
}
|
|
auto* slot = AdoptTwinByHandle(g_backendVertexArrayObjects, elements, "vertex-elements CSO");
|
|
if (slot == nullptr) return nullptr;
|
|
if (!*slot) *slot = MakeShared<BackendVertexArrayObject>();
|
|
return slot->get();
|
|
}
|
|
|
|
// The same function, driven by the applier's records (D-G4). Every branch of the walk
|
|
// survives - the enable/disable block, the fp64 narrowing with its Adreno disable, the
|
|
// zero-stride binding-API path, the attribute-buffer bind, the BGRA swizzle, the
|
|
// pointer/IPointer at the shifted fetch offset and the divisor - and the gate is
|
|
// re-keyed onto three server-owned MGGen counters plus the CSO's {slot, gen}:
|
|
//
|
|
// m_syncedElementsHandle + m_syncedElementsSerial <- config version + the whole
|
|
// per-attribute version array
|
|
// m_syncedVertexBuffersSerial <- (new) the buffer set's own
|
|
// m_syncedIndexSerial <- wrapping Uint16 + identity patch
|
|
// m_syncedBufferIdGeneration <- UNCHANGED, server-local, and
|
|
// still the only thing that
|
|
// catches a driver-id re-mint no
|
|
// counter on either side moves
|
|
// m_hasConvertedAttribute <- UNCHANGED: a narrowed stream is
|
|
// derived from buffer CONTENT,
|
|
// which no serial covers
|
|
// m_syncedFetchBaseInstance <- no longer in the gate as a
|
|
// DIRTY input of its own (a base
|
|
// instance change is a
|
|
// ContentHash input and so moves
|
|
// VertexBuffersSerial), but kept
|
|
// as the record of what was last
|
|
// EMITTED, which is what the next
|
|
// sync has to correct
|
|
void BackendVertexArrayObject::SyncToBackendFromApplier() {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier();
|
|
const auto* rec = BoundVertexElementsRecord(st);
|
|
if (rec == nullptr) {
|
|
MGLOG_E_ONCE("MGPipe: no vertex-elements record is bound, so the driver VAO cannot be "
|
|
"configured - nothing emitted create_vertex_elements/bind_vertex_elements");
|
|
return;
|
|
}
|
|
|
|
const Uint64 currentBufferIdGeneration = BufferImpl::g_bufferBackendIdGeneration;
|
|
const Bool bufferIdsRemitted = m_syncedBufferIdGeneration != currentBufferIdGeneration;
|
|
// THIS GATE DEPENDS ON A SERIAL RULE THAT LIVES IN ANOTHER PACKAGE, and it is named
|
|
// here because nothing else in this file would say it: MGPipeApplierReset() runs on
|
|
// EVERY change of the current GLContext (MG_Impl/Pipe/Tracker.h's `if (m_context !=
|
|
// &ctx) Reset();`, not only on a fresh one), while this twin SURVIVES the excursion -
|
|
// BackendVertexArrayObject has no context-generation member and
|
|
// OnBackendContextDestroyed runs on destroy, not on make-current. A reset that sent
|
|
// VertexBuffersSerial and IndexBufferSerial back to ZERO would therefore walk them
|
|
// back through values this twin has already stamped, and a memo could read clean over
|
|
// state the applier had just cleared. MG_Pipe/PipeApply.cpp's reset must ADVANCE
|
|
// those two serials instead (wire's C2), which is what makes
|
|
// m_syncedVertexBuffersSerial below unable to match after a reset - and that in turn
|
|
// forces the whole AND dirty, which is also what rescues the per-record ContentSerial
|
|
// half (an applier reset is not a slot recycle, so a re-created CSO at the same
|
|
// {slot, gen} restarts its ContentSerial at 1). If that rule is ever reverted, this
|
|
// gate is unsafe on any application that changes contexts.
|
|
const Bool elementsDirty = bufferIdsRemitted || !m_hasSyncedElements ||
|
|
!(m_syncedElementsHandle == st.BoundVertexElements) ||
|
|
m_syncedElementsSerial != rec->ContentSerial;
|
|
const Bool attributesDirty = elementsDirty || m_syncedVertexBuffersSerial != st.VertexBuffersSerial;
|
|
const Bool indexBufferDirty = bufferIdsRemitted || m_syncedIndexSerial != st.IndexBufferSerial;
|
|
// Emulation is server-owned: the client sends the draw's RAW base instance and never
|
|
// learns the answer. Applied here as well as in the applier so the decision is the
|
|
// same whichever side resolved it first - it is idempotent.
|
|
const Uint32 fetchBaseInstance = BackendUsesNativeBaseInstance() ? 0u : st.VertexFetchBaseInstance;
|
|
const Bool baseInstanceDirty = m_syncedFetchBaseInstance != fetchBaseInstance;
|
|
const Bool emitAttributes = attributesDirty || baseInstanceDirty || m_hasConvertedAttribute;
|
|
if (!emitAttributes && !indexBufferDirty) {
|
|
return;
|
|
}
|
|
// P14: ONLY THE BUFFER SET MOVED (a Minecraft chunk draw: same layout, next chunk's
|
|
// buffer). The driver VAO's enable flags are a function of the configuration alone,
|
|
// and the last full walk left them exactly as this configuration wants them, so the
|
|
// walk below re-points just the enabled attributes and skips the 32 enable/disable
|
|
// calls. Only after a walk in which every enabled attribute took the plain pointer
|
|
// path: the fp64, zero-stride, BGRA and no-buffer branches toggle enables or bind
|
|
// through other state, and the client-memory upload rewrites the VAO behind this
|
|
// gate, so any of them sends the next sync through the whole walk again.
|
|
const Bool buffersOnly = emitAttributes && !elementsDirty && m_lastWalkWasPlain && !m_hasConvertedAttribute;
|
|
Bool walkWasPlain = true;
|
|
m_hasConvertedAttribute = false;
|
|
// Rebuilt by every walk; a buffers-only one follows a plain walk, which set none.
|
|
if (emitAttributes && !buffersOnly) m_disabledEnabledAttribMask = 0;
|
|
|
|
Bind();
|
|
if (m_hostPointerBufferGeneration != currentBufferIdGeneration) {
|
|
for (auto& pointer : m_hostPointer)
|
|
pointer.valid = false;
|
|
m_hostPointerBufferGeneration = currentBufferIdGeneration;
|
|
}
|
|
|
|
// ALL 32 SLOTS, not rec->AttributeCount, and the difference is the disable arm.
|
|
// The legacy walk ran over the frontend's whole 32-slot attribute array and reached
|
|
// glDisableVertexAttribArray for every attribute that was off; a walk bounded by the
|
|
// record's count leaves an attribute the configuration has just DROPPED enabled in
|
|
// the driver VAO, pointing at whatever buffer it last held - which is the class the
|
|
// first Adreno workaround below exists to prevent. Nothing in the contract requires
|
|
// the client to emit 32 entries (D-H3 sets the precedent the other way for the
|
|
// BUFFER set: "truncated to the highest enabled attribute + 1"), so the bound was a
|
|
// requirement on B that was neither stated nor pinned. It costs nothing to drop it:
|
|
// MGPipeApplyCreateVertexElements zeroes both arrays before it unpacks, so every
|
|
// entry past AttributeCount reads Enabled = 0, Type = 0 (Int8, not Float64) and
|
|
// IsLong = 0 - i.e. exactly "disable this array", with no dependency on B at all.
|
|
for (Uint attribIndex = 0; attribIndex < MG_Pipe::kMGPipeMaxVertexAttribs && emitAttributes;
|
|
++attribIndex) {
|
|
const MGPVertexAttribWire& attrib = rec->Attributes[attribIndex];
|
|
if (buffersOnly && !attrib.Enabled) continue;
|
|
const MG_Pipe::MGPVertexBuffer* binding = VertexBufferForAttributeIndex(st, attribIndex);
|
|
const Uint32 divisor = binding != nullptr ? binding->Divisor : 0u;
|
|
|
|
// The enable/disable block. On this arm there is no per-attribute version to
|
|
// compare: the applier's Attributes[] IS what was last pushed, so a moved
|
|
// ContentSerial means re-emit and an unchanged one means the early-out above
|
|
// already returned. A buffers-only walk leaves the flags as they are (above).
|
|
if (!buffersOnly) HostSetEnabled(attribIndex, attrib.Enabled != 0);
|
|
|
|
// The fp64 narrowing, verbatim in behaviour including the Adreno workaround:
|
|
// when no float32 stream can be built the array is DISABLED rather than left
|
|
// enabled with no pointer, which is what the driver turns into a SIGSEGV inside
|
|
// the next draw (KHR-GL43.vertex_attrib_binding.basic-input-case4). IsLong is
|
|
// carried separately from Type == Float64 on the wire precisely so this test
|
|
// can still tell the two apart.
|
|
//
|
|
// A GL_BGRA-sized array takes the same stream, swizzled instead of narrowed, and
|
|
// the same disable when it cannot be built: GL_BGRA as a vertex SIZE is
|
|
// desktop-only, ES rejects it, and the rejected call leaves the array ENABLED
|
|
// with no pointer - the shape the Adreno driver dereferences null on inside the
|
|
// next draw (KHR-GL43.vertex_attrib_binding.basic-input-case5).
|
|
const Bool bgraArray = attrib.IsBgra != 0 && !attrib.IsInteger;
|
|
if (attrib.IsLong || attrib.Type == static_cast<Uint32>(DataType::Float64) || bgraArray) {
|
|
walkWasPlain = false;
|
|
const Bool convertible = bgraArray || attrib.Type == static_cast<Uint32>(DataType::Float64);
|
|
if (attrib.Enabled && convertible && binding != nullptr &&
|
|
SyncConvertedAttributeByHandle(attribIndex, attrib, *binding, fetchBaseInstance)) {
|
|
m_hasConvertedAttribute = true;
|
|
// The conversion pointed the attribute at its own stream (and may have
|
|
// used the binding API), so the pointer and divisor shadow are stale.
|
|
// An earlier walk that could not build the stream disabled this array.
|
|
m_hostPointer[attribIndex].valid = false;
|
|
m_hostDivisorKnownMask &= ~(1u << attribIndex);
|
|
HostSetEnabled(attribIndex, true);
|
|
HostSetDivisor(attribIndex, divisor);
|
|
continue;
|
|
}
|
|
if (attrib.Enabled) {
|
|
MGLOG_W_ONCE("DirectGLES: vertex attribute %u is a %s array whose source stream could not "
|
|
"be converted - disabling the array",
|
|
attribIndex, bgraArray ? "GL_BGRA" : "64-bit (GL_DOUBLE)");
|
|
// The draw now reads the attribute's current generic value, which the
|
|
// application had no reason to set for an enabled array.
|
|
m_disabledEnabledAttribMask |= 1u << attribIndex;
|
|
}
|
|
HostSetEnabled(attribIndex, false);
|
|
continue;
|
|
}
|
|
|
|
if (!attrib.Enabled) continue;
|
|
if (binding == nullptr) {
|
|
walkWasPlain = false;
|
|
continue;
|
|
}
|
|
|
|
// A resolved stride of zero is the binding model's "never advance" and
|
|
// glVertexAttribPointer cannot say it - its zero means "tightly packed", i.e.
|
|
// the opposite. ES 3.1's binding-point API can.
|
|
if (attrib.Stride == 0 && HasVertexBindingApi()) {
|
|
walkWasPlain = false;
|
|
// The binding API rewrote this attribute's format and binding: whatever the
|
|
// shadow said about its pointer no longer holds.
|
|
m_hostPointer[attribIndex].valid = false;
|
|
if (!SyncZeroStrideAttributeByHandle(attribIndex, attrib, *binding)) {
|
|
continue;
|
|
}
|
|
// No shift here on purpose: a zero stride never advances. Binding index ==
|
|
// attribute index here, so this IS the attribute's divisor for the shadow.
|
|
g_GLESFuncs.glVertexBindingDivisor(attribIndex, divisor);
|
|
m_hostDivisorKnownMask |= 1u << attribIndex;
|
|
m_hostDivisor[attribIndex] = divisor;
|
|
continue;
|
|
}
|
|
|
|
if (MG_Pipe::MGPipeHandleIsNull(binding->Res)) {
|
|
MGLOG_W_ONCE("Attribute has no bound buffer, skipping.");
|
|
walkWasPlain = false;
|
|
continue;
|
|
}
|
|
const auto* attribBuffer = BufferImpl::FindBufferResourceForHandle(binding->Res);
|
|
if (!attribBuffer || attribBuffer->id == 0) {
|
|
MGLOG_E_ONCE("No backend buffer found for attribute's buffer, cannot bind attribute.");
|
|
walkWasPlain = false;
|
|
continue;
|
|
}
|
|
|
|
const SizeT fetchOffset = static_cast<SizeT>(attrib.Offset) +
|
|
BaseInstanceByteShiftWire(attrib.Stride, divisor, fetchBaseInstance);
|
|
const GLenum glType = MG_Util::ConvertDataTypeToGLEnum(static_cast<DataType>(attrib.Type));
|
|
const GLint glSize = static_cast<GLint>(attrib.Size);
|
|
|
|
// The pointer the driver VAO already holds for this attribute - the buffer it
|
|
// captured included - needs no glBindBuffer + pointer call.
|
|
auto& shadowPointer = m_hostPointer[attribIndex];
|
|
const Bool pointerHeld =
|
|
VaoShadowOn() && shadowPointer.valid && shadowPointer.bufferId == attribBuffer->id &&
|
|
shadowPointer.size == glSize && shadowPointer.type == static_cast<Uint32>(glType) &&
|
|
shadowPointer.stride == static_cast<Int32>(attrib.Stride) && shadowPointer.offset == fetchOffset &&
|
|
shadowPointer.normalized == (attrib.Normalized != 0) &&
|
|
shadowPointer.integer == (attrib.IsInteger != 0);
|
|
if (!pointerHeld) {
|
|
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, attribBuffer->id);
|
|
if (!attrib.IsInteger) {
|
|
g_GLESFuncs.glVertexAttribPointer(attribIndex, glSize, glType,
|
|
attrib.Normalized ? GL_TRUE : GL_FALSE, attrib.Stride,
|
|
(const void*)fetchOffset);
|
|
} else {
|
|
g_GLESFuncs.glVertexAttribIPointer(attribIndex, glSize, glType, attrib.Stride,
|
|
(const void*)fetchOffset);
|
|
}
|
|
shadowPointer = {attribBuffer->id,
|
|
glSize,
|
|
static_cast<Uint32>(glType),
|
|
static_cast<Int32>(attrib.Stride),
|
|
fetchOffset,
|
|
attrib.Normalized != 0,
|
|
attrib.IsInteger != 0,
|
|
true};
|
|
}
|
|
|
|
HostSetDivisor(attribIndex, divisor);
|
|
}
|
|
|
|
if (indexBufferDirty) {
|
|
Bool indexBufferSynced = false;
|
|
if (!MG_Pipe::MGPipeHandleIsNull(st.IndexBuffer.Res)) {
|
|
// RESOLVE ONLY, no ensure - unlike the legacy arm, which called
|
|
// EnsureBufferResource here. PrepareForDraw runs SyncNeccessaryBuffers before
|
|
// SyncCurrentVAO, so an INDEXED draw has already ensured this store; a
|
|
// non-indexed draw whose IndexBufferSerial moved has not, and then this logs
|
|
// once and leaves the binding alone. That is noisy rather than wrong: nothing
|
|
// is stamped on the miss (indexBufferSynced stays false), so the next indexed
|
|
// draw repairs it. Ensuring here would need the frontend object this arm
|
|
// deliberately does not hold.
|
|
auto* backendResource = BufferImpl::FindBufferResourceForHandle(st.IndexBuffer.Res);
|
|
if (backendResource && backendResource->id != 0) {
|
|
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, backendResource->id);
|
|
indexBufferSynced = true;
|
|
} else {
|
|
MGLOG_W_ONCE("No backend buffer found for index buffer binding, cannot bind index buffer.");
|
|
}
|
|
} else {
|
|
g_GLESFuncs.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
|
indexBufferSynced = true;
|
|
}
|
|
if (indexBufferSynced) {
|
|
m_syncedIndexRes = st.IndexBuffer.Res;
|
|
m_syncedIndexValid = true;
|
|
// The two element-array restore scopes (the restart substitution's and
|
|
// MultiDrawImpl's) put the DRIVER id back without touching this serial,
|
|
// which is correct: the serial records what the APPLIER last said, and
|
|
// those scopes restored exactly what the applier said.
|
|
m_syncedIndexSerial = st.IndexBufferSerial;
|
|
}
|
|
}
|
|
|
|
if (attributesDirty) {
|
|
m_syncedElementsHandle = st.BoundVertexElements;
|
|
m_syncedElementsSerial = rec->ContentSerial;
|
|
m_syncedVertexBuffersSerial = st.VertexBuffersSerial;
|
|
m_hasSyncedElements = true;
|
|
// The window this walk emitted, for AdoptUnchangedBufferSets.
|
|
m_syncedBufferStart = st.VertexBufferStart;
|
|
m_syncedBufferCount =
|
|
std::min<Uint32>(st.VertexBufferCount, MG_Pipe::kMGPipeMaxVertexAttribs - st.VertexBufferStart);
|
|
m_syncedBufferBaseInstance = st.VertexFetchBaseInstance;
|
|
for (Uint32 i = 0; i < m_syncedBufferCount; ++i)
|
|
m_syncedBuffers[i] = st.VertexBuffers[m_syncedBufferStart + i];
|
|
m_syncedBuffersValid = st.VertexBufferStart <= MG_Pipe::kMGPipeMaxVertexAttribs;
|
|
m_syncedContextSerial = st.ContextSerial;
|
|
}
|
|
if (emitAttributes) {
|
|
m_syncedFetchBaseInstance = fetchBaseInstance;
|
|
m_lastWalkWasPlain = walkWasPlain;
|
|
}
|
|
m_syncedBufferIdGeneration = currentBufferIdGeneration;
|
|
}
|
|
|
|
void BackendVertexArrayObject::AdoptUnchangedBufferSets(const MG_Pipe::MGPipeApplierState& st) {
|
|
if (!m_hasSyncedElements || m_syncedContextSerial != st.ContextSerial) return;
|
|
if (m_syncedBuffersValid && m_syncedVertexBuffersSerial != st.VertexBuffersSerial &&
|
|
st.VertexBufferStart == m_syncedBufferStart &&
|
|
st.VertexFetchBaseInstance == m_syncedBufferBaseInstance &&
|
|
std::min<Uint32>(st.VertexBufferCount, MG_Pipe::kMGPipeMaxVertexAttribs - st.VertexBufferStart) ==
|
|
m_syncedBufferCount) {
|
|
Bool same = true;
|
|
for (Uint32 i = 0; same && i < m_syncedBufferCount; ++i) {
|
|
const auto& a = m_syncedBuffers[i];
|
|
const auto& b = st.VertexBuffers[m_syncedBufferStart + i];
|
|
same = a.Res == b.Res && a.Offset == b.Offset && a.Stride == b.Stride && a.Divisor == b.Divisor &&
|
|
a.BindingIndex == b.BindingIndex;
|
|
}
|
|
if (same) {
|
|
if (m_resolvedDrawBuffers.valid &&
|
|
m_resolvedDrawBuffers.buffersSerial == m_syncedVertexBuffersSerial)
|
|
m_resolvedDrawBuffers.buffersSerial = st.VertexBuffersSerial;
|
|
m_syncedVertexBuffersSerial = st.VertexBuffersSerial;
|
|
}
|
|
}
|
|
if (m_syncedIndexValid && m_syncedIndexSerial != st.IndexBufferSerial &&
|
|
m_syncedIndexRes == st.IndexBuffer.Res) {
|
|
if (m_resolvedDrawBuffers.iboSerial == m_syncedIndexSerial)
|
|
m_resolvedDrawBuffers.iboSerial = st.IndexBufferSerial;
|
|
m_syncedIndexSerial = st.IndexBufferSerial;
|
|
}
|
|
}
|
|
|
|
void BackendVertexArrayObject::SyncClientSideAttributesForDrawArrays(
|
|
const SharedPtr<MG_State::GLState::VertexArrayObject>& stateVAOObject, GLint first, GLsizei count,
|
|
Uint32 fetchBaseInstance) {
|
|
if (!stateVAOObject || count <= 0 || first < 0) {
|
|
return;
|
|
}
|
|
// This rewrites the driver VAO's arrays behind SyncToBackendFromApplier's gate, so
|
|
// the next sync must not take its buffers-only walk, nor adopt an equal buffer set.
|
|
m_lastWalkWasPlain = false;
|
|
m_syncedBuffersValid = false;
|
|
HostForgetAll(); // the uploads below set enables, pointers and divisors directly
|
|
|
|
Bind();
|
|
|
|
const auto& allAttributes = stateVAOObject->GetAllAttributes();
|
|
for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) {
|
|
const auto& attrib = allAttributes[attribIndex];
|
|
if (!attrib.Enabled || attrib.Buffer) {
|
|
continue;
|
|
}
|
|
|
|
// Same narrowing as SyncToBackend (the long note lives there), with the draw's own
|
|
// fetch range standing in for the buffer extent a client array does not have. The
|
|
// upload starts at element 0 so `first` keeps indexing the stream, exactly as the
|
|
// unconverted upload below does. The test is on the storage rather than on IsLong
|
|
// because glVertexAttribFormat(GL_DOUBLE) is 64-bit data without being long.
|
|
if (attrib.IsLong || attrib.Type == DataType::Float64) {
|
|
const auto* sourceBase = reinterpret_cast<const Uint8*>(attrib.Offset);
|
|
if (attrib.Type != DataType::Float64 || sourceBase == nullptr || attrib.Size < 1 ||
|
|
attrib.Size > 4) {
|
|
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
|
continue;
|
|
}
|
|
|
|
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.");
|
|
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const SizeT componentCount = static_cast<SizeT>(attrib.Size);
|
|
const SizeT sourceElementSize = componentCount * sizeof(Double);
|
|
// A client array only ever reaches here through a pointer call, whose stride 0
|
|
// the frontend already resolved to the element size, so this is a guard rather
|
|
// than a case (VertexAttribute::Stride).
|
|
const SizeT sourceStride =
|
|
attrib.Stride > 0 ? static_cast<SizeT>(attrib.Stride) : sourceElementSize;
|
|
const SizeT elementCount = static_cast<SizeT>(first) + static_cast<SizeT>(count);
|
|
Vector<Float> converted;
|
|
NarrowDoubleStreamToFloat32(sourceBase, sourceStride, componentCount, elementCount, converted);
|
|
|
|
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId);
|
|
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(converted.size() * sizeof(Float)),
|
|
converted.data(), GL_STREAM_DRAW);
|
|
if (MG_Util::PipeStats::Enabled()) {
|
|
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
|
|
static_cast<Uint64>(converted.size() * sizeof(Float)));
|
|
}
|
|
// GL ignores `normalized` for floating-point array types, so it is not
|
|
// forwarded here either.
|
|
g_GLESFuncs.glVertexAttribPointer(
|
|
attribIndex, attrib.Size, GL_FLOAT, GL_FALSE,
|
|
static_cast<GLsizei>(componentCount * sizeof(Float)),
|
|
// 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;
|
|
}
|
|
|
|
const auto* clientData = reinterpret_cast<const Uint8*>(attrib.Offset);
|
|
const SizeT elementSize = GetAttributeByteSize(attrib.Type, attrib.Size, attrib.IsBgra);
|
|
if (!clientData || elementSize == 0 || attrib.Size <= 0) {
|
|
continue;
|
|
}
|
|
|
|
const SizeT stride = attrib.Stride > 0 ? static_cast<SizeT>(attrib.Stride) : elementSize;
|
|
const SizeT uploadSize = static_cast<SizeT>(first + count - 1) * 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.");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// A GL_BGRA array is swizzled into RGBA order on the way, as on the buffer path
|
|
// (SyncConvertedAttributeByHandle): ES has no BGRA vertex size to declare.
|
|
const Bool bgra = attrib.IsBgra && !attrib.IsInteger;
|
|
Vector<Uint8> swizzled;
|
|
if (bgra) {
|
|
swizzled.assign(clientData, clientData + uploadSize);
|
|
for (SizeT element = 0; element < static_cast<SizeT>(first + count); ++element) {
|
|
SwizzleBgraElementToRgba(swizzled.data() + element * stride, attrib.Type);
|
|
}
|
|
}
|
|
|
|
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId);
|
|
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(uploadSize),
|
|
bgra ? static_cast<const void*>(swizzled.data()) : clientData, GL_STREAM_DRAW);
|
|
if (MG_Util::PipeStats::Enabled()) {
|
|
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
|
|
static_cast<Uint64>(uploadSize));
|
|
}
|
|
|
|
if (!attrib.IsInteger) {
|
|
const GLint glSize = bgra ? 4 : attrib.Size;
|
|
g_GLESFuncs.glVertexAttribPointer(
|
|
attribIndex, glSize, MG_Util::ConvertDataTypeToGLEnum(attrib.Type),
|
|
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),
|
|
// See the pointer call above: the divisor'd
|
|
// arrays' baseInstance shift is the pointer.
|
|
reinterpret_cast<const void*>(BaseInstanceByteShift(attrib, fetchBaseInstance)));
|
|
}
|
|
}
|
|
}
|
|
|
|
Bool BackendVertexArrayObject::SyncConvertedAttributeByHandle(Uint attribIndex,
|
|
const MGPVertexAttribWire& attrib,
|
|
const MG_Pipe::MGPVertexBuffer& binding,
|
|
Uint32 fetchBaseInstance) {
|
|
#ifdef TRACY_ENABLE
|
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
|
#endif
|
|
if (attribIndex >= m_convertedAttributeBufferIds.size() || attrib.Size < 1 || attrib.Size > 4) {
|
|
return false;
|
|
}
|
|
// Which of the two conversions: a BGRA array is a 4-byte element whatever its type,
|
|
// and only the three types GL allows with it reach here (the frontend refuses the
|
|
// rest at the format call).
|
|
const DataType sourceType = static_cast<DataType>(attrib.Type);
|
|
const Bool bgra = attrib.IsBgra != 0;
|
|
if (bgra && sourceType != DataType::Uint8 && sourceType != DataType::Int2101010Rev &&
|
|
sourceType != DataType::Uint2101010Rev) {
|
|
return false;
|
|
}
|
|
if (!bgra && sourceType != DataType::Float64) return false;
|
|
if (MG_Pipe::MGPipeHandleIsNull(binding.Res)) {
|
|
// A client-memory 64-bit array is narrowed on the draw path instead, which is
|
|
// the only place its fetch range is known.
|
|
return false;
|
|
}
|
|
auto* resource = BufferImpl::FindBufferResourceForHandle(binding.Res);
|
|
if (resource == nullptr) return false;
|
|
|
|
// WHAT IS NOT HERE, AND IT IS A RULED DEVIATION FOR P3a RATHER THAN AN OVERSIGHT
|
|
// (M-3; integrator ruling, this commit).
|
|
//
|
|
// The legacy arm opens with bufferObject->SyncGpuWrites(), one of the eleven Espryt
|
|
// SyncPersistentMappedRange / SyncGpuWrites sites D-N keeps where they are for P3a.
|
|
// D-N's wording is "no MOVE of those sites off the frontend", and this arm does not
|
|
// move it: it CANNOT MAKE IT AT ALL. The call needs a frontend BufferObject and this
|
|
// path holds only a handle, because the server has no inverse map back to a frontend
|
|
// object - that absence is the design, not a gap in it (ARCHITECTURE.md 4.2). The
|
|
// two ways to keep the site here would each break something D-N or D-J protects: a
|
|
// handle -> object map is the very thing the split removes, and pulling the bytes
|
|
// eagerly on the client at every draw is new behaviour and new cost.
|
|
//
|
|
// SO THE DEVIATION IS DECLARED, WITH ITS BLAST RADIUS. Under the default mask a
|
|
// 64-bit vertex array whose SOURCE buffer was written by a shader and not yet pulled
|
|
// back narrows STALE bytes on this arm and fresh bytes on the legacy one. That is
|
|
// the whole of it: fp64 vertex arrays, fed by a buffer a shader wrote, read without
|
|
// any intervening explicit readback. No other attribute type reads through this
|
|
// path, and a persistently mapped source is excluded separately below (the memo
|
|
// never trusts one, so those re-read every draw through the coherent map).
|
|
//
|
|
// P8-C CLOSES IT UNDER A TRANSPORT, on the server rather than by moving the pull to the
|
|
// client: a source a shader wrote on this side is read back from this server's own GL
|
|
// buffer (SplitHostBytesForCpuRead), and that write also voids the memo below, whose
|
|
// serial a GPU write never moves. The MONOLITH arm of this function keeps the
|
|
// deviation (its hostBytes is the client's shadow and its mark is the frontend
|
|
// object's); that half is dev's (ID-P8-3).
|
|
// WHERE THE BYTES ARE, and it is not one fixed place: an ADOPTED store has no client
|
|
// shadow left at all (PipeResource::AdoptPersistentMap clears and shrinks it), so
|
|
// the coherent map IS the source of truth - which is exactly the case the memo
|
|
// exclusion below makes the most frequent, since a persistently mapped source is
|
|
// never trusted and this narrowing therefore re-reads on every draw. Reading the
|
|
// recorded shadow base for such a resource read freed memory; hostBytes is nulled
|
|
// at adoption for the same reason, so the fallback below is a null and a refusal
|
|
// rather than a stale read.
|
|
const Bool sourceGpuWritten = resource->serverGpuWritten;
|
|
if (sourceGpuWritten) {
|
|
BufferImpl::SplitHostBytesForCpuRead(*resource, binding.Res,
|
|
bgra ? "bgra_vertex_swizzle" : "fp64_vertex_narrowing");
|
|
}
|
|
const Uint8* const sourceBase = (resource->persistentMapped && resource->persistentPtr != nullptr)
|
|
? static_cast<const Uint8*>(resource->persistentPtr)
|
|
: resource->hostBytes;
|
|
const SizeT sourceSize = BufferImpl::ResourceWidthForHandle(binding.Res);
|
|
if (sourceBase == nullptr || attrib.Offset >= sourceSize) {
|
|
return false;
|
|
}
|
|
|
|
// A BGRA element is always one 32-bit word, and so is its RGBA replacement; a 64-bit
|
|
// element narrows to the float32 vector of the same component count.
|
|
const SizeT componentCount = static_cast<SizeT>(attrib.Size);
|
|
const SizeT sourceElementSize = bgra ? 4 : componentCount * sizeof(Double);
|
|
const SizeT convertedElementSize = bgra ? 4 : componentCount * sizeof(Float);
|
|
const SizeT available = sourceSize - static_cast<SizeT>(attrib.Offset);
|
|
if (available < sourceElementSize) {
|
|
return false;
|
|
}
|
|
|
|
const Bool neverAdvances = attrib.Stride <= 0;
|
|
const SizeT sourceStride = neverAdvances ? sourceElementSize : static_cast<SizeT>(attrib.Stride);
|
|
const SizeT elementCount = neverAdvances ? 1 : ((available - sourceElementSize) / sourceStride) + 1;
|
|
|
|
const SizeT firstElement = (fetchBaseInstance != 0 && binding.Divisor != 0 && !neverAdvances)
|
|
? static_cast<SizeT>(fetchBaseInstance)
|
|
: 0;
|
|
if (firstElement >= elementCount) {
|
|
return false;
|
|
}
|
|
|
|
auto& stream = m_convertedAttributeStreams[attribIndex];
|
|
Uint& convertedBufferId = m_convertedAttributeBufferIds[attribIndex];
|
|
const Uint64 sourceSerial = BufferImpl::ResourceSerialForHandle(binding.Res);
|
|
// The memo, re-keyed: the pin is the source buffer's {slot, gen} instead of a
|
|
// frontend lifetime id, and the freshness input is the applier's server-owned
|
|
// Serial instead of the frontend change serial. A PERSISTENTLY MAPPED source is
|
|
// still never trusted - it is written through the pointer with no call at all, so
|
|
// no serial on either side can prove the converted copy is current. The conversion
|
|
// itself is part of the key: one attribute index can carry both shapes over its life,
|
|
// over the same bytes.
|
|
const Bool memoHit =
|
|
stream.valid && convertedBufferId != 0 && !resource->persistentMapped && !sourceGpuWritten &&
|
|
stream.sourceHandle == binding.Res && stream.sourceChangeSerial == sourceSerial &&
|
|
stream.sourceOffset == static_cast<SizeT>(attrib.Offset) && stream.sourceStride == sourceStride &&
|
|
stream.bgra == bgra && stream.sourceType == attrib.Type && stream.componentCount == componentCount &&
|
|
stream.elementCount == elementCount;
|
|
if (!memoHit) {
|
|
if (convertedBufferId == 0) {
|
|
g_GLESFuncs.glGenBuffers(1, &convertedBufferId);
|
|
if (convertedBufferId == 0) {
|
|
MGLOG_E_ONCE("Failed to create the scratch buffer for the converted vertex array at "
|
|
"attribute %u.",
|
|
attribIndex);
|
|
return false;
|
|
}
|
|
}
|
|
Vector<Float> narrowed;
|
|
Vector<Uint8> swizzled;
|
|
const void* convertedData = nullptr;
|
|
SizeT convertedBytes = 0;
|
|
if (bgra) {
|
|
SwizzleBgraStreamToRgba(sourceBase + attrib.Offset, sourceStride, elementCount, sourceType,
|
|
swizzled);
|
|
convertedData = swizzled.data();
|
|
convertedBytes = swizzled.size();
|
|
} else {
|
|
NarrowDoubleStreamToFloat32(sourceBase + attrib.Offset, sourceStride, componentCount, elementCount,
|
|
narrowed);
|
|
convertedData = narrowed.data();
|
|
convertedBytes = narrowed.size() * sizeof(Float);
|
|
}
|
|
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, convertedBufferId);
|
|
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(convertedBytes), convertedData,
|
|
GL_STREAM_DRAW);
|
|
if (MG_Util::PipeStats::Enabled()) {
|
|
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
|
|
static_cast<Uint64>(convertedBytes));
|
|
}
|
|
stream.valid = true;
|
|
stream.sourceHandle = binding.Res;
|
|
stream.sourceChangeSerial = sourceSerial;
|
|
stream.sourceOffset = static_cast<SizeT>(attrib.Offset);
|
|
stream.sourceStride = sourceStride;
|
|
stream.bgra = bgra;
|
|
stream.sourceType = attrib.Type;
|
|
stream.componentCount = componentCount;
|
|
stream.elementCount = elementCount;
|
|
MGLOG_D("DirectGLES: converted the %s vertex array at attribute %u, %zu element(s).",
|
|
bgra ? "BGRA" : "64-bit", attribIndex, elementCount);
|
|
}
|
|
|
|
// What the converted stream is to the driver. The swizzled BGRA stream keeps its
|
|
// source type and `normalized` (GL accepts BGRA only normalized, and a packed type's
|
|
// signedness is the type's own). The narrowed stream is float32, and `normalized` is
|
|
// deliberately GL_FALSE there rather than attrib.Normalized: GL ignores it for
|
|
// floating-point array types, and honouring it would scale the fetched values.
|
|
const GLint convertedSize = bgra ? 4 : static_cast<GLint>(attrib.Size);
|
|
const GLenum convertedType = bgra ? MG_Util::ConvertDataTypeToGLEnum(sourceType) : GL_FLOAT;
|
|
const GLboolean convertedNormalized = (bgra && attrib.Normalized) ? GL_TRUE : GL_FALSE;
|
|
if (neverAdvances) {
|
|
// Only the binding-point API can say "stride 0".
|
|
if (!HasVertexBindingApi()) {
|
|
return false;
|
|
}
|
|
g_GLESFuncs.glVertexAttribFormat(attribIndex, convertedSize, convertedType, convertedNormalized, 0);
|
|
g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex);
|
|
g_GLESFuncs.glBindVertexBuffer(attribIndex, convertedBufferId, 0, 0);
|
|
return true;
|
|
}
|
|
|
|
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, convertedBufferId);
|
|
g_GLESFuncs.glVertexAttribPointer(attribIndex, convertedSize, convertedType, convertedNormalized,
|
|
static_cast<GLsizei>(convertedElementSize),
|
|
(const void*)(firstElement * convertedElementSize));
|
|
return true;
|
|
}
|
|
|
|
TwinRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject,
|
|
MG_Pipe::MGPipeKind::VertexElementsCso>
|
|
g_backendVertexArrayObjects;
|
|
} // namespace VertexArrayImpl
|
|
} // namespace MobileGL::MG_Backend::DirectGLES
|