[Perf] (pipe): a vertex-elements record identical to the one its VAO already published is not re-created (FCL Espryt, shadow off: host calls 3724 -> 1228/frame).

This commit is contained in:
2026-10-08 12:31:57 -04:00
parent 49c6394dfd
commit c8a5abe581
4 changed files with 101 additions and 13 deletions
+4
View File
@@ -144,6 +144,10 @@ namespace MobileGL::MG_Config {
// pointer to the driver VAO only when they differ from what that VAO already holds (a
// shadow on the VAO twin). Auto is ON; ForceOff re-emits the whole configuration per walk.
QuirkOverride EsprytVaoShadow = QuirkOverride::Auto;
// MOBILEGL_PIPE_VERTEX_ELEMENTS_DEDUP: a VAO configuration change whose vertex-elements
// record is byte-identical to the one its handle last published (a glBindVertexBuffer
// that only changed the buffer) re-publishes nothing. Auto is ON; ForceOff re-creates.
QuirkOverride PipeVertexElementsDedup = QuirkOverride::Auto;
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
+1
View File
@@ -373,6 +373,7 @@ namespace MobileGL::MG_ConfigLoader {
features.MagmaVertexConversionCache = QueryEnvQuirkOverride("MOBILEGL_MAGMA_VERTEX_CONVERSION_CACHE");
features.MagmaWriteRename = QueryEnvQuirkOverride("MOBILEGL_MAGMA_WRITE_RENAME");
features.EsprytVaoShadow = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_VAO_SHADOW");
features.PipeVertexElementsDedup = QueryEnvQuirkOverride("MOBILEGL_PIPE_VERTEX_ELEMENTS_DEDUP");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
+42 -13
View File
@@ -8,6 +8,7 @@
#pragma once
#include <Includes.h>
#include <Config.h>
// The CLIENT side of P3a's vertex-input family (brief D-G, D-H, D-I): the bound VAO's
// format as create/bind_vertex_elements, its buffers as set_vertex_buffers with an explicit
@@ -182,10 +183,25 @@ namespace MobileGL::MG_Pipe {
const Bool configMoved = !latch.Published || latch.ConfigVersion != configVersion ||
latch.Gen != handle.Gen;
if (configMoved) {
bytes += EmitCreate(*vao, handle, latch, configVersion);
// The applier drops the record's kept bindings at the same create.
memo.StoredBuffersValid = false;
memo.StoredIndexValid = false;
BuildBlob(*vao);
// P15: the version moves on every glBindVertexBuffer, but the record only names
// formats and offsets. An identical record for the same handle and generation is
// already the server's: re-creating it moved its content serial for nothing, which
// sent the backend through a whole attribute walk per draw (FCL MC 1.21.5: one per
// chunk draw). Kill switch MOBILEGL_PIPE_VERTEX_ELEMENTS_DEDUP=0.
const Bool unchanged = MG_Config::Features.PipeVertexElementsDedup != MG_Config::QuirkOverride::ForceOff &&
latch.Published && latch.Gen == handle.Gen &&
latch.PublishedBlob.size() == kBlobBytes &&
std::memcmp(latch.PublishedBlob.data(), m_blob.data(), kBlobBytes) == 0;
if (unchanged) {
latch.ConfigVersion = configVersion;
++m_createsDeduped;
} else {
bytes += EmitCreate(handle, latch, configVersion);
// The applier drops the record's kept bindings at the same create.
memo.StoredBuffersValid = false;
memo.StoredIndexValid = false;
}
}
if (lifetimeId != m_boundLifetimeId || m_boundHandle != handle) {
MGPipeRouteBindVertexElements(HandleOnly(handle));
@@ -385,6 +401,7 @@ namespace MobileGL::MG_Pipe {
const MGPIndexBuffer& LastIndexBuffer() const { return m_lastIndex; }
MGPipeHandle BoundHandle() const { return m_boundHandle; }
Uint64 CreateCount() const { return m_creates; }
Uint64 DedupedCreateCount() const { return m_createsDeduped; }
Uint64 BindCount() const { return m_binds; }
Uint64 VertexBufferSetCount() const { return m_bufferSets; }
Uint64 IndexBufferSetCount() const { return m_indexSets; }
@@ -449,7 +466,7 @@ namespace MobileGL::MG_Pipe {
m_serverIndexKnown = false;
}
void ResetCounters() { m_creates = m_binds = m_bufferSets = m_indexSets = 0; }
void ResetCounters() { m_creates = m_createsDeduped = m_binds = m_bufferSets = m_indexSets = 0; }
private:
// P14: what EmitVertexElements / EmitVertexBuffers / EmitIndexBuffer resolve from a VAO,
@@ -568,6 +585,11 @@ namespace MobileGL::MG_Pipe {
// RecordIsPublished.
Bool RecordLive = false;
Uint32 RecordGen = 0;
// P15: the blob this handle last published, so a configuration version that moved
// without moving the record (a glBindVertexBuffer that only changed the buffer,
// whose identity rides set_vertex_buffers) re-publishes nothing. A full copy, not a
// hash: the compare decides what the server holds.
Vector<Uint8> PublishedBlob;
};
static MGPHandleOnly HandleOnly(MGPipeHandle handle) {
@@ -577,12 +599,15 @@ namespace MobileGL::MG_Pipe {
return only;
}
Uint64 EmitCreate(const VertexArrayObject& vao, MGPipeHandle handle, Latch& latch, Uint32 configVersion) {
// ALL 32 OF EACH, deliberately. The record DECLARES both counts and the applier
// refuses one whose counts do not describe its own blob, so a self-describing
// record is the cheap shape - and G6 is stated over all 32 slots, which a
// truncated set could not answer. It rides create_vertex_elements only, i.e.
// once per configuration change, never per draw.
static constexpr SizeT kAttribBytes = kAttribs * sizeof(MGPVertexAttribWire);
static constexpr SizeT kBindingBytes = kBindings * sizeof(MGPVertexBindingPointWire);
static constexpr SizeT kBlobBytes = kAttribBytes + kBindingBytes;
// The record's blob for this configuration, into m_attributes / m_bindingPoints / m_blob.
// ALL 32 OF EACH, deliberately. The record DECLARES both counts and the applier refuses
// one whose counts do not describe its own blob, so a self-describing record is the cheap
// shape - and G6 is stated over all 32 slots, which a truncated set could not answer.
void BuildBlob(const VertexArrayObject& vao) {
for (SizeT i = 0; i < kAttribs; ++i) {
m_attributes[i] = MGPipeBuildVertexAttribWire(vao.GetAttribute(static_cast<Uint>(i)),
vao.GetAttributeBindingIndex(static_cast<Uint>(i)));
@@ -595,11 +620,13 @@ namespace MobileGL::MG_Pipe {
if (!vao.GetBindingPoint(static_cast<Uint>(i)).Buffer) m_bindingPoints[i].Offset = 0;
}
// Attributes first, then binding points, both ascending and contiguous.
constexpr SizeT kAttribBytes = kAttribs * sizeof(MGPVertexAttribWire);
constexpr SizeT kBindingBytes = kBindings * sizeof(MGPVertexBindingPointWire);
std::memcpy(m_blob.data(), m_attributes.data(), kAttribBytes);
std::memcpy(m_blob.data() + kAttribBytes, m_bindingPoints.data(), kBindingBytes);
}
// create_vertex_elements for the blob BuildBlob just made. It rides only a configuration
// change whose record differs from the one this handle published, never per draw.
Uint64 EmitCreate(MGPipeHandle handle, Latch& latch, Uint32 configVersion) {
m_lastElements = MGPVertexElements{};
m_lastElements.Cso = handle;
m_lastElements.AttributeCount = static_cast<Uint32>(kAttribs);
@@ -616,6 +643,7 @@ namespace MobileGL::MG_Pipe {
// only thing that makes delete_vertex_elements a legal call for this handle.
latch.RecordLive = true;
latch.RecordGen = handle.Gen;
latch.PublishedBlob.assign(m_blob.begin(), m_blob.begin() + kBlobBytes);
return sizeof(MGPVertexElements) + kAttribBytes + kBindingBytes;
}
@@ -634,6 +662,7 @@ namespace MobileGL::MG_Pipe {
Uint64 m_boundLifetimeId = 0;
Uint64 m_creates = 0;
Uint64 m_createsDeduped = 0;
Uint64 m_binds = 0;
Uint64 m_bufferSets = 0;
Uint64 m_indexSets = 0;
@@ -237,4 +237,58 @@ void main() { o0 = vColor; }
Destroy(s);
}
// The binding-point API, as a chunk renderer uses it: one format, glBindVertexBuffer per draw.
// A buffer-only switch leaves the vertex-elements record byte-identical, so the client does
// not re-publish it (MOBILEGL_PIPE_VERTEX_ELEMENTS_DEDUP); the draw must still read the new
// buffer. An offset change does change the record and must reach the server.
TEST_F(VaoShadowScenario, BindVertexBufferSwitchesAndOffsetsAreReadByEachDraw) {
if (!Ready()) return;
std::string error;
Scene s = MakeScene(&error);
ASSERT_NE(s.program, 0u) << error;
// Interleaved {x, y, r, g, b, a}: buffer A a red left quad, buffer B a green right quad,
// buffer C two quads - the first blue on the left, the second (offset 4 vertices) a
// white right quad.
const auto interleave = [](const std::vector<float>& pos, const std::vector<float>& col) {
std::vector<float> v;
for (int i = 0; i < 4; ++i) {
v.insert(v.end(), {pos[i * 2], pos[i * 2 + 1]});
v.insert(v.end(), col.begin() + i * 4, col.begin() + i * 4 + 4);
}
return v;
};
const GLuint a = Buffer(interleave(Quad(0.0f, 0.5f), Colors(1.0f, 0.0f, 0.0f)));
const GLuint b = Buffer(interleave(Quad(0.5f, 1.0f), Colors(0.0f, 1.0f, 0.0f)));
std::vector<float> two = interleave(Quad(0.0f, 0.5f), Colors(0.0f, 0.0f, 1.0f));
const std::vector<float> second = interleave(Quad(0.5f, 1.0f), Colors(1.0f, 1.0f, 1.0f));
two.insert(two.end(), second.begin(), second.end());
const GLuint c = Buffer(two);
constexpr GLsizei kStride = 6 * sizeof(float);
glVertexAttribFormat(0, 2, GL_FLOAT, GL_FALSE, 0);
glVertexAttribFormat(1, 4, GL_FLOAT, GL_FALSE, 2 * sizeof(float));
glVertexAttribBinding(0, 0);
glVertexAttribBinding(1, 0);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glBindVertexBuffer(0, a, 0, kStride);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // red left
glBindVertexBuffer(0, b, 0, kStride);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // green right
EXPECT_TRUE(Near(At(kSize / 4, kSize / 2), 255, 0, 0)) << "buffer A";
EXPECT_TRUE(Near(At(kSize * 3 / 4, kSize / 2), 0, 255, 0)) << "buffer B, same format";
glClear(GL_COLOR_BUFFER_BIT);
glBindVertexBuffer(0, c, 0, kStride);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // blue left (offset 0)
glBindVertexBuffer(0, c, 4 * kStride, kStride);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // white right (offset 4 vertices)
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(Near(At(kSize / 4, kSize / 2), 0, 0, 255)) << "buffer C at offset 0";
EXPECT_TRUE(Near(At(kSize * 3 / 4, kSize / 2), 255, 255, 255)) << "buffer C at offset 4 vertices";
GLuint bufs[] = {a, b, c};
glDeleteBuffers(3, bufs);
Destroy(s);
}
} // namespace MGITest