[Perf] (DirectVulkan): memoize per-draw texture sync in SetupDraw

Each sampled texture was resolved ~3x per draw: SetupDraw's layout-probe
loop, its post-transition loop, and again inside ResolveSamplerDescriptor.
No GL texture mutation happens mid-SetupDraw, and layout is tracked on the
TextureResource independently of SyncTexture, so the repeat SyncTexture work
(mip-completeness / resource+view resync / dirty scan) is pure redundancy.

Add a per-draw memo in VkTextureManager (BeginDrawSyncScope/EndDrawSyncScope
+ RAII DrawSyncScope guard around SetupDraw): after the first successful sync
of a texture in a draw, repeat SyncTextureAndGetDescriptor calls short-circuit
to the already-synced resource.

Device-verified on Adreno 830 (MC 26.3-snapshot3, Magma): rendering correct,
no validation errors; wall-clock profile of the render thread shows
SyncTextureAndGetDescriptor dropping from 15.2% to ~5% and SetupDraw from
43.7% to 28.9%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 22:06:28 -04:00
co-authored by Claude Opus 4.8
parent c1743aa42d
commit b253df881d
3 changed files with 74 additions and 0 deletions
@@ -683,10 +683,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
void VkTextureManager::BeginDrawSyncScope() {
m_drawSyncedThisDraw.clear();
m_drawSyncScopeActive = true;
}
void VkTextureManager::EndDrawSyncScope() {
m_drawSyncScopeActive = false;
m_drawSyncedThisDraw.clear();
}
VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& texture) {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureAndGetDescriptor: m_device == VK_NULL_HANDLE");
const TextureIdentity identity = MakeTextureIdentity(&texture);
// Per-draw memo fast path (see BeginDrawSyncScope): a texture already fully
// synced earlier in this draw cannot have changed since (no GL mutation runs
// mid-SetupDraw), so skip the heavy SyncTexture work and hand back the
// already-synced resource. Layout lives on the resource and is updated by the
// transition path, so the short-circuited resource still reflects the truth.
const Bool memoActive = m_drawSyncScopeActive;
if (memoActive) {
for (const TextureIdentity& synced : m_drawSyncedThisDraw) {
if (synced == identity) {
auto cachedIt = m_textureResources.find(identity);
if (cachedIt != m_textureResources.end() && cachedIt->second.image != VK_NULL_HANDLE) {
return &(cachedIt->second);
}
break; // resource unexpectedly gone -> fall through to a full sync
}
}
}
auto aliveIt = m_aliveObjects.find(identity);
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
EraseTrackedTexture(aliveIt->first);
@@ -717,6 +746,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return nullptr;
}
if (memoActive) {
Bool recorded = false;
for (const TextureIdentity& synced : m_drawSyncedThisDraw) {
if (synced == identity) {
recorded = true;
break;
}
}
if (!recorded) {
m_drawSyncedThisDraw.push_back(identity);
}
}
return &(it->second);
}