mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 13:18:31 +09:00
[Perf] (MG_Backend): memoise DirectVulkan's per-draw vertex binding resolution
Every draw re-resolved its whole vertex binding array: for each enabled binding, look up the buffer, acquire a slice from the buffer manager, apply the binding's base offset, fill the VkBuffer and offset arrays, bind. In the Minecraft-shaped benchmark the same few hundred vertex array objects cycle for the whole run and each one's answer is stable, so UploadAndBindVertexBuffers was the single largest cost in the backend at 7.9% of the render thread, with AcquireResidentSlice another 3.8% underneath it. The resolved array is now kept per vertex array object and revalidated instead of rebuilt. Validation is two-tier. The vertex array's own configuration version already invalidates its backend vertex-input state, so a changed attribute, format, buffer or base offset yields a different state object - the memo compares both that object's address and its hash, which mixes the bound buffers and the whole layout. What that does not cover is the slice moving underneath an unchanged configuration, so the buffer manager now carries a monotonic epoch that every writer of slice-deciding state bumps: resident storage creation, respecify, sub-data, flush of a mapped range, the promotion and demotion between streamed and resident storage, each fresh arena allocation, and bulk release. The counter is manager-wide and never reset, so a resource created at a recycled address cannot reproduce a value some memo still holds. The miss path was the thing to get right, because the previous attempt in this area regressed the texture-upload and sampler-churn cases by 60-85%: it added a verification pass that re-ran the resolution work it was trying to skip, so every miss paid for it twice. Here a miss is one pointer-keyed lookup and a few stores, and nothing else runs that the full path would not have run anyway. ns per draw, DriverBench on a GTX 1660 SUPER: mc_ubo_range 924 -> 767, mc_vanilla_draw 1346 -> 1227, mc_sampler_churn 1397 -> 1279, mc_sodium_multidraw 3365 -> 3266. Magma is now 4.1x the native driver on the per-draw uniform-range case, from 5.4x when this round started. No case regressed on either backend. Unit tests 421/421.
This commit is contained in:
@@ -271,6 +271,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
g_dynamicStateShadow = {};
|
||||
}
|
||||
|
||||
// vkCmdBindVertexBuffers, skipped when this command buffer already holds these
|
||||
// buffers and offsets at binding 0.
|
||||
static void ShadowedBindVertexBuffers(VkCommandBuffer commandBuffer, const VkBuffer* buffers,
|
||||
const VkDeviceSize* offsets, Uint32 count) {
|
||||
auto& shadow = g_dynamicStateShadow;
|
||||
Bool identical = shadow.vertexBindValid && shadow.vertexBindingCount == count &&
|
||||
count <= DynamicStateShadow::kMaxShadowedVertexBindings;
|
||||
if (identical) {
|
||||
for (Uint32 i = 0; i < count; ++i) {
|
||||
if (shadow.vertexBuffers[i] != buffers[i] || shadow.vertexOffsets[i] != offsets[i]) {
|
||||
identical = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (identical) {
|
||||
return;
|
||||
}
|
||||
vkCmdBindVertexBuffers(commandBuffer, 0, count, buffers, offsets);
|
||||
if (count <= DynamicStateShadow::kMaxShadowedVertexBindings) {
|
||||
shadow.vertexBindValid = true;
|
||||
shadow.vertexBindingCount = count;
|
||||
std::copy_n(buffers, count, shadow.vertexBuffers);
|
||||
std::copy_n(offsets, count, shadow.vertexOffsets);
|
||||
} else {
|
||||
shadow.vertexBindValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void ShadowedSetScissor(VkCommandBuffer commandBuffer, const VkRect2D& scissor) {
|
||||
auto& shadow = g_dynamicStateShadow;
|
||||
if (shadow.scissorValid && shadow.scissor.offset.x == scissor.offset.x &&
|
||||
@@ -3015,10 +3044,66 @@ void main() {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::TryBindResolvedVertexBindings(
|
||||
VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||
const ResolvedVertexBindings& entry,
|
||||
const VertexInputStateFactory::BackendVertexInputState& vertexInputState, Uint32 activeAttribMask,
|
||||
Uint32 bindingCount, Uint64 frameSerial) {
|
||||
// Frame-scoped: a streamed binding's slice moves to a new arena block every
|
||||
// frame by design, and the frame's first resolve is also what stamps every
|
||||
// bound buffer's GPU-use serial, which the buffer manager's busy tracking (and
|
||||
// therefore glBufferSubData's choice between a host write and a staged copy)
|
||||
// depends on.
|
||||
if (entry.frameSerial != frameSerial || entry.vertexInputState != &vertexInputState ||
|
||||
entry.vertexInputHash != vertexInputState.hash || entry.activeAttribMask != activeAttribMask ||
|
||||
entry.bindingCount != bindingCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The layout identity above already fixes which buffer each binding reads: the
|
||||
// factory entry is reached through the VAO's config-version-gated memo and its
|
||||
// hash mixes the bound buffers' addresses, so rebinding a buffer resolves to a
|
||||
// different entry. What is left to establish is that those buffers still hand
|
||||
// back the slices recorded here, and that none of them is a host map whose
|
||||
// shadow needs pushing down. An unmoved manager-wide epoch counter says both.
|
||||
if (!entry.anyBufferMapped && entry.sliceEpochCounter == m_bufferManager.GetSliceEpochCounter()) {
|
||||
ShadowedBindVertexBuffers(commandBuffer, entry.vkBuffers, entry.vkOffsets, bindingCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Something moved somewhere; ask the buffers themselves.
|
||||
const auto& attributes = vao.GetAllAttributes();
|
||||
const MG_State::GLState::BufferObject* synced = nullptr;
|
||||
for (Uint32 binding = 0; binding < bindingCount; ++binding) {
|
||||
auto* bufferObject = attributes[entry.attributeLocations[binding]].Buffer.get();
|
||||
if (bufferObject != entry.buffers[binding]) {
|
||||
return false;
|
||||
}
|
||||
// What the resolving path does before every acquire: a persistent map the
|
||||
// backend could not adopt into coherent GPU storage mutates its shadow with
|
||||
// no API call, so the write range has to be pushed down here too. It is a
|
||||
// no-op for every buffer that is not such a map; when it is not, it dispatches
|
||||
// a SubData that retires the epoch below, and this draw resolves in full.
|
||||
if (bufferObject != synced) {
|
||||
bufferObject->SyncPersistentMappedRange();
|
||||
synced = bufferObject;
|
||||
}
|
||||
const auto* resource = static_cast<const VkBufferResource*>(bufferObject->GetBackendResource().get());
|
||||
if (resource == nullptr || resource->sliceEpoch != entry.sliceEpochs[binding]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ShadowedBindVertexBuffers(commandBuffer, entry.vkBuffers, entry.vkOffsets, bindingCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::UploadAndBindVertexBuffers(
|
||||
VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
|
||||
const ProgramFactory::VkProgramObject& programObj, const DrawCmdParam& drawParams,
|
||||
const IndexBufferView* pIndexBufferView) {
|
||||
static_assert(ResolvedVertexBindings::kMaxBindings == DynamicStateShadow::kMaxShadowedVertexBindings,
|
||||
"the resolved-binding memo is sized to what the bind shadow can compare");
|
||||
const Bool indexedDraw = pIndexBufferView != nullptr;
|
||||
// Exclusive upper bound on the vertex-stream elements this draw can fetch through
|
||||
// vertex-rate bindings, or 0 when unbounded (indirect/multi draws). Computed lazily
|
||||
@@ -3050,6 +3135,30 @@ void main() {
|
||||
|
||||
const auto bindingCount = vertexInputState.bindings.size() + static_cast<SizeT>(std::popcount(missingAttribMask));
|
||||
|
||||
const Uint64 frameSerial = m_bufferManager.GetFrameSerial();
|
||||
if (frameSerial != m_resolvedVertexBindingsFrameSerial) {
|
||||
m_resolvedVertexBindingsFrameSerial = frameSerial;
|
||||
if (m_resolvedVertexBindings.size() > kMaxResolvedVertexBindings) {
|
||||
m_resolvedVertexBindings.clear();
|
||||
}
|
||||
}
|
||||
ResolvedVertexBindings* memo = nullptr;
|
||||
if (auto found = m_resolvedVertexBindings.find(&vao); found != m_resolvedVertexBindings.end()) {
|
||||
memo = &found->second;
|
||||
if (TryBindResolvedVertexBindings(commandBuffer, vao, *memo, vertexInputState, activeAttribMask,
|
||||
static_cast<Uint32>(bindingCount), frameSerial)) {
|
||||
return true;
|
||||
}
|
||||
// Whatever it described is stale; a resolve that bails out below must not
|
||||
// leave the old contents matchable either.
|
||||
memo->frameSerial = 0;
|
||||
}
|
||||
// Anything the memo cannot key on (see ResolvedVertexBindings) clears this as
|
||||
// the resolve below discovers it.
|
||||
Bool memoisable = missingAttribMask == 0 && bindingCount > 0 &&
|
||||
bindingCount <= ResolvedVertexBindings::kMaxBindings;
|
||||
Bool anyBufferMapped = false;
|
||||
|
||||
auto& vkBuffers = m_vertexBuffersScratch;
|
||||
auto& vkOffsets = m_vertexOffsetsScratch;
|
||||
vkBuffers.assign(bindingCount, VK_NULL_HANDLE);
|
||||
@@ -3101,6 +3210,7 @@ void main() {
|
||||
? vertexInputState.bindingConversions[binding]
|
||||
: VertexInputStateFactory::VertexStreamConversion::None;
|
||||
if (usesClientMemory) {
|
||||
memoisable = false;
|
||||
const Uint32 location = bindingLocation;
|
||||
MOBILEGL_ASSERT(location < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS,
|
||||
"UploadAndBindVertexStreams failed to resolve client attribute location");
|
||||
@@ -3174,6 +3284,7 @@ void main() {
|
||||
binding, baseOffset, sourceSize);
|
||||
|
||||
if (conversion != VertexInputStateFactory::VertexStreamConversion::None) {
|
||||
memoisable = false;
|
||||
MOBILEGL_ASSERT(bindingLocation < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS,
|
||||
"UploadAndBindVertexStreams failed to resolve converted attribute location");
|
||||
const auto& attr = vao.GetAttribute(bindingLocation);
|
||||
@@ -3261,6 +3372,24 @@ void main() {
|
||||
}
|
||||
vkBuffers[binding] = slice.buffer;
|
||||
vkOffsets[binding] = slice.offset + static_cast<VkDeviceSize>(baseOffset);
|
||||
// bindingAttributeLocations carries MAX_VERTEX_ATTRIBS as its "no location"
|
||||
// sentinel; GetAttribute() folds that to an empty attribute but the memo
|
||||
// indexes the raw array, so such a binding is not memoisable.
|
||||
memoisable = memoisable && bindingLocation < MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS;
|
||||
if (memoisable) {
|
||||
if (memo == nullptr) {
|
||||
memo = &m_resolvedVertexBindings[&vao];
|
||||
memo->frameSerial = 0;
|
||||
}
|
||||
memo->attributeLocations[binding] = static_cast<Uint8>(bindingLocation);
|
||||
memo->buffers[binding] = sourceBufferShared.get();
|
||||
// Read after the acquire: it is the acquire that creates the resource
|
||||
// and mints the epoch this slice belongs to.
|
||||
const auto* resource =
|
||||
static_cast<const VkBufferResource*>(sourceBufferShared->GetBackendResource().get());
|
||||
memo->sliceEpochs[binding] = resource != nullptr ? resource->sliceEpoch : 0;
|
||||
anyBufferMapped = anyBufferMapped || sourceBufferShared->IsMapped();
|
||||
}
|
||||
}
|
||||
|
||||
SizeT syntheticBinding = vertexInputState.bindings.size();
|
||||
@@ -3299,29 +3428,22 @@ void main() {
|
||||
}
|
||||
|
||||
if (bindingCount > 0) {
|
||||
auto& shadow = g_dynamicStateShadow;
|
||||
const Uint32 count = static_cast<Uint32>(bindingCount);
|
||||
Bool identical = shadow.vertexBindValid && shadow.vertexBindingCount == count &&
|
||||
count <= DynamicStateShadow::kMaxShadowedVertexBindings;
|
||||
if (identical) {
|
||||
for (Uint32 i = 0; i < count; ++i) {
|
||||
if (shadow.vertexBuffers[i] != vkBuffers[i] || shadow.vertexOffsets[i] != vkOffsets[i]) {
|
||||
identical = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!identical) {
|
||||
vkCmdBindVertexBuffers(commandBuffer, 0, count, vkBuffers.data(), vkOffsets.data());
|
||||
if (count <= DynamicStateShadow::kMaxShadowedVertexBindings) {
|
||||
shadow.vertexBindValid = true;
|
||||
shadow.vertexBindingCount = count;
|
||||
std::copy_n(vkBuffers.data(), count, shadow.vertexBuffers);
|
||||
std::copy_n(vkOffsets.data(), count, shadow.vertexOffsets);
|
||||
} else {
|
||||
shadow.vertexBindValid = false;
|
||||
}
|
||||
if (memoisable && memo != nullptr) {
|
||||
std::copy_n(vkBuffers.data(), count, memo->vkBuffers);
|
||||
std::copy_n(vkOffsets.data(), count, memo->vkOffsets);
|
||||
memo->vertexInputState = &vertexInputState;
|
||||
memo->vertexInputHash = vertexInputState.hash;
|
||||
memo->activeAttribMask = activeAttribMask;
|
||||
memo->bindingCount = count;
|
||||
memo->anyBufferMapped = anyBufferMapped;
|
||||
// Read after every acquire above, so it covers the epochs they minted.
|
||||
memo->sliceEpochCounter = m_bufferManager.GetSliceEpochCounter();
|
||||
// Published last: the entry is only matchable once every field above is
|
||||
// the one this completed resolve produced.
|
||||
memo->frameSerial = frameSerial;
|
||||
}
|
||||
ShadowedBindVertexBuffers(commandBuffer, vkBuffers.data(), vkOffsets.data(), count);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user