[Perf] (MG_Backend/DirectVulkan): skip the per-draw sampled-texture walk when the bound set is unchanged (texture-bind generation + program state version); CollectSampledTextures 5.0%->0.2%, fps 228->249

This commit is contained in:
2026-07-13 04:41:14 -04:00
parent 516d2a659e
commit f098983c9f
5 changed files with 60 additions and 3 deletions
@@ -3389,6 +3389,9 @@ void main() {
// Begin command recording if not yet
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
// New command buffer: a program/FBO address from a previous frame may have been
// recycled, so start the sampled-set skip cache fresh this frame.
m_lastSampledSetValid = false;
}
auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
@@ -3397,9 +3400,31 @@ void main() {
// which probably indicates it's been gone through codepath like `fbo attach` -> `clear` -> `fbo detach`, and
// without draws in between to give it a chance to materialize such clear.
// Deal with this situation here.
auto& sampledTextures = m_sampledTexturesScratch; // cleared by CollectSampledTextures
Bool hasSampledTextures = m_uniformManager->CollectSampledTextures(program, programObj, sampledTextures);
MOBILEGL_ASSERT(hasSampledTextures, "%s: CollectSampledTextures failed", __func__);
// Reuse the previous draw's sampled-texture list when the set is provably unchanged (same
// program+state+transform and no bind/unbind/delete since), skipping the per-draw GL walk.
// The layout/feedback/transition loops below still run on the list every draw, so this only
// elides re-resolving *which* textures are sampled, never their layout handling.
auto& sampledTextures = m_sampledTexturesScratch;
{
const Uint programIndex = program.GetExternalIndex();
const Uint32 programVersion = program.GetBackendStateVersion();
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
const Bool sampledSetUnchanged =
m_lastSampledSetValid && m_lastSampledSetProgramIndex == programIndex &&
m_lastSampledSetProgramVersion == programVersion &&
m_lastSampledSetTransformFlags == transformFlags &&
m_lastSampledSetBindGeneration == bindGeneration;
if (!sampledSetUnchanged) {
const Bool hasSampledTextures =
m_uniformManager->CollectSampledTextures(program, programObj, sampledTextures);
MOBILEGL_ASSERT(hasSampledTextures, "%s: CollectSampledTextures failed", __func__);
m_lastSampledSetValid = true;
m_lastSampledSetProgramIndex = programIndex;
m_lastSampledSetProgramVersion = programVersion;
m_lastSampledSetTransformFlags = transformFlags;
m_lastSampledSetBindGeneration = bindGeneration;
}
}
MGLOG_D("SetupDraw: program=%u drawFbo=%u sampledTextureCount=%zu activeRenderPass=%s",
program.GetExternalIndex(), drawFbo ? drawFbo->GetExternalIndex() : 0u, sampledTextures.size(),
activeRenderPass ? "true" : "false");
@@ -403,6 +403,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DepthMipmapResources m_depthMipmapResources;
Vector<DeferredDepthMipmapCleanup> m_deferredDepthMipmapCleanup;
// Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled
// texture SET is provably unchanged from the previous draw: same program (external index +
// backend-state version, which covers sampler-uniform reassignment / relink) and transform
// flags, and no texture bind/unbind/delete since (GetTextureBindGeneration). On a hit,
// m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback /
// layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL
// walk is skipped. Reset per command-buffer recording so a reused program/FBO address can't
// outlive a frame.
Bool m_lastSampledSetValid = false;
Uint m_lastSampledSetProgramIndex = 0;
Uint32 m_lastSampledSetProgramVersion = 0;
ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
Uint64 m_lastSampledSetBindGeneration = 0;
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
// draw call and must not allocate.
Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;