mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
[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:
@@ -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) {
|
VkTextureManager::TextureResource* VkTextureManager::SyncTextureAndGetDescriptor(MG_State::GLState::ITextureObject& texture) {
|
||||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureAndGetDescriptor: m_device == VK_NULL_HANDLE");
|
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureAndGetDescriptor: m_device == VK_NULL_HANDLE");
|
||||||
|
|
||||||
const TextureIdentity identity = MakeTextureIdentity(&texture);
|
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);
|
auto aliveIt = m_aliveObjects.find(identity);
|
||||||
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
|
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
|
||||||
EraseTrackedTexture(aliveIt->first);
|
EraseTrackedTexture(aliveIt->first);
|
||||||
@@ -717,6 +746,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
return nullptr;
|
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);
|
return &(it->second);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -200,6 +200,30 @@ public:
|
|||||||
Uint32 layerCount = 1);
|
Uint32 layerCount = 1);
|
||||||
|
|
||||||
SizeT CollectGarbage();
|
SizeT CollectGarbage();
|
||||||
|
|
||||||
|
// Per-draw sync memo. Within a single SetupDraw the same sampled texture is
|
||||||
|
// resolved ~3x (SetupDraw's layout-probe loop, its post-transition loop, and
|
||||||
|
// again inside ResolveSamplerDescriptor). No GL texture mutation can happen
|
||||||
|
// mid-SetupDraw, and layout is tracked on the TextureResource independently of
|
||||||
|
// SyncTexture, so after the first successful sync of a texture in a draw the
|
||||||
|
// heavy SyncTexture work (mip-completeness/resource/view resync + dirty scan)
|
||||||
|
// is pure redundancy. BeginDrawSyncScope opens a window in which repeat
|
||||||
|
// SyncTextureAndGetDescriptor calls short-circuit to the already-synced
|
||||||
|
// resource; EndDrawSyncScope closes it. Use the RAII DrawSyncScope guard.
|
||||||
|
void BeginDrawSyncScope();
|
||||||
|
void EndDrawSyncScope();
|
||||||
|
|
||||||
|
// RAII guard that opens/closes a per-draw sync memo window (see above).
|
||||||
|
class DrawSyncScope {
|
||||||
|
public:
|
||||||
|
explicit DrawSyncScope(VkTextureManager& manager) : m_manager(manager) { m_manager.BeginDrawSyncScope(); }
|
||||||
|
~DrawSyncScope() { m_manager.EndDrawSyncScope(); }
|
||||||
|
DrawSyncScope(const DrawSyncScope&) = delete;
|
||||||
|
DrawSyncScope& operator=(const DrawSyncScope&) = delete;
|
||||||
|
private:
|
||||||
|
VkTextureManager& m_manager;
|
||||||
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
|
Bool SyncTexture(MG_State::GLState::ITextureObject &texture,
|
||||||
@@ -242,6 +266,10 @@ private:
|
|||||||
Uint32 m_currentFrameIndex = 0;
|
Uint32 m_currentFrameIndex = 0;
|
||||||
|
|
||||||
Uint8 m_gcCounter = 0;
|
Uint8 m_gcCounter = 0;
|
||||||
|
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
|
||||||
|
// textures already fully synced in the current draw (small N -> flat scan).
|
||||||
|
Bool m_drawSyncScopeActive = false;
|
||||||
|
Vector<TextureIdentity> m_drawSyncedThisDraw;
|
||||||
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
std::unordered_map<TextureIdentity, WeakPtr<MG_State::GLState::ITextureObject>, TextureIdentityHash> m_aliveObjects;
|
||||||
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
std::unordered_map<TextureIdentity, TextureResource, TextureIdentityHash> m_textureResources;
|
||||||
Vector<Vector<TextureResource>> m_deferredReleases;
|
Vector<Vector<TextureResource>> m_deferredReleases;
|
||||||
|
|||||||
@@ -3285,6 +3285,10 @@ void main() {
|
|||||||
Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
|
||||||
const DrawCmdParam& drawParams,
|
const DrawCmdParam& drawParams,
|
||||||
const IndexBufferView* pIndexBufferView) {
|
const IndexBufferView* pIndexBufferView) {
|
||||||
|
// Sync each sampled texture at most once across this whole draw: the layout
|
||||||
|
// probe loop, the post-transition loop, and ResolveSamplerDescriptor would
|
||||||
|
// otherwise each re-run the full SyncTexture path on the same textures.
|
||||||
|
VkTextureManager::DrawSyncScope drawSyncScope(*m_textureManager);
|
||||||
m_textureManager->CollectGarbage();
|
m_textureManager->CollectGarbage();
|
||||||
const auto& drawFbo =
|
const auto& drawFbo =
|
||||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||||
|
|||||||
Reference in New Issue
Block a user