[Perf] (MG_Backend): stop DirectVulkan re-proving sampler sets and re-walking render passes

Two per-draw costs from the round-10 profiles. A per-program sampled-set
epoch inside UniformManager skips the per-binding descriptor proof walk
when no texture or sampler API ran since that program's previous draw -
the mc_sampler_churn/mc_tex_param pattern. The pass-switch path stops
re-deriving render-pass state that its own value hash already pins.

Load-gated 6-round order-alternating A/B (medians): magma tex_param
-13.3%, pass_switch -10.9%, state_toggle -5.4%; espryt untouched and
unmoved. The two matrix flags (sodium +7.5%, tex_stream +6.2%) reversed
under 10-pair isolated alternating re-runs (-5.5% and +2.5%) - the same
position-bias artifact every previous round's flags showed. Unit tests
421/421; retrace subset and the 52-entry integration suite pass.

Landing note: this diff was authored by a round-10 agent whose session
died before adjudication; the A/B data survived (r10bmag_ab_raw.csv)
and the flags were adjudicated before landing. Its relink also exposed
the pre-existing exit-teardown SIGSEGV fixed in the previous commit.
This commit is contained in:
BZLZHH
2026-08-07 04:13:55 -04:00
parent fb1ad96c04
commit 335f2decbd
4 changed files with 171 additions and 21 deletions
@@ -116,6 +116,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_frameCount = frameCount;
m_maxBindings = maxBindings;
m_samplerResolveMemo.assign(m_maxBindings, SamplerResolveMemo{});
// Every entry is freshly constructed (all-invalid), so nothing needs sweeping until
// a resolve writes one.
m_samplerResolveMemoHighWater = 0;
m_setsPerFrame = setsPerFrame;
m_peakDescriptorSetsObserved = 0;
m_textureManager = textureManager;
@@ -174,6 +177,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_minDynamicOffsetAlignment = 1;
m_frameCount = 0;
m_maxBindings = 0;
m_samplerResolveMemo.clear();
m_samplerResolveMemoHighWater = 0;
m_setsPerFrame = 0;
m_peakDescriptorSetsObserved = 0;
m_textureManager = nullptr;
@@ -210,10 +215,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_fastRebindMemo.valid = false;
m_lastBindValid = false;
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
// reuse cannot outlive a single frame (see SamplerResolveMemo).
for (auto& memo : m_samplerResolveMemo) {
memo.valid = false;
memo.infoValid = false;
// reuse cannot outlive a single frame (see SamplerResolveMemo). Only the entries a
// resolve has actually written can be valid, so the high-water mark bounds the
// sweep - the vector itself is sized to the device's binding cap (256 here), which
// is ~30x more entries than any program declares.
const Uint32 touchedBindings =
std::min<Uint32>(m_samplerResolveMemoHighWater, static_cast<Uint32>(m_samplerResolveMemo.size()));
for (Uint32 binding = 0; binding < touchedBindings; ++binding) {
m_samplerResolveMemo[binding].valid = false;
m_samplerResolveMemo[binding].infoValid = false;
}
}
@@ -379,6 +389,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
viewFormatMemo->viewFormatDomain = numericDomain;
viewFormatMemo->viewFormat = sampledViewFormat;
viewFormatMemo->viewFormatValid = true;
NoteSamplerResolveMemoTouched(binding);
}
}
if (sampledViewFormat == VK_FORMAT_UNDEFINED) {
@@ -434,6 +445,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
memo.viewLevelCount = viewLevelCount;
memo.sampler = resolvedSampler;
memo.valid = true;
NoteSamplerResolveMemoTouched(binding);
}
} else {
resolvedSampler = m_samplerManager->GetOrCreateSampler(*samplerToUse, *texture, forceNearestFiltering,
@@ -450,6 +462,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (binding < m_samplerResolveMemo.size()) {
m_samplerResolveMemo[binding].info = outImageInfo;
m_samplerResolveMemo[binding].infoValid = true;
NoteSamplerResolveMemoTouched(binding);
}
return true;
}
@@ -345,5 +345,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool infoValid = false;
};
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
// Exclusive upper bound on the entries of m_samplerResolveMemo that any resolve
// has ever written. The vector is sized to the DEVICE binding cap (256 on desktop
// NVIDIA), but a program declares 1-8 bindings, so the per-frame reset below was
// memsetting ~22 KB of never-touched entries every frame - a measurable slice of
// the per-frame fixed cost on draw-light frames. Every site that can turn any of
// an entry's *Valid flags on raises this mark first, so entries at or above it are
// provably still in their constructed (all-invalid) state and clearing them is a
// no-op. Never lowered except by Initialize/Shutdown, which rebuild the vector.
mutable Uint32 m_samplerResolveMemoHighWater = 0;
void NoteSamplerResolveMemoTouched(Uint32 binding) const {
if (binding >= m_samplerResolveMemoHighWater) {
m_samplerResolveMemoHighWater = binding + 1;
}
}
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -273,12 +273,76 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// re-derive the exact values already applied on this command buffer. The
// remaining input, the swapchain pre-transform, cannot change mid-recording
// (a swapchain recreate retires the command buffer, and recording begin resets
// this whole shadow).
// this whole shadow); the value key below pins it anyway.
Bool dynamicTailValid = false;
Uint dynamicTailParamsVersion = 0;
Int dynamicTailExtentX = 0;
Int dynamicTailExtentY = 0;
Bool dynamicTailIsDefaultFbo = false;
// VALUE key over the tail's inputs, as a second-level gate behind the version.
// The parameters version is ONE counter for all of RenderState, so anything that
// is not tail input - a GL_BLEND toggle, a glBlendFuncSeparate, a glColorMask -
// moves it and forced a full tail re-run. Blaze3D toggles blend around every
// batch, so that was a per-draw re-derivation of six dynamic states that could
// not have changed. Equal key => the six Apply* below would each re-derive the
// value their shadow already holds and emit nothing, so the tail is skippable.
//
// Complete input inventory of ApplyDynamicDrawStateTail, one line per reader
// (each accessor it replaces is a verified plain field read of the same
// RenderStateParameters field - RenderState.cpp):
// ApplyGLViewportState : Viewport, DepthRange, + extent/isDefaultFbo/preTransform
// ApplyBlendConstants : BlendColor
// ApplyPolygonOffsetState : PolygonOffsetUnits, PolygonOffsetFactor
// ApplyLineWidthState : LineWidth (see the caveat below)
// ApplyStencilState : StencilStates[0..1].{ValueMask, WriteMask, Ref}
// scissor rect : ScissorTestEnabled, ScissorBox,
// + extent/isDefaultFbo/preTransform
// Caveat, unchanged from the version-only gate: ApplyLineWidthState also clamps
// to the ACTIVE BACKEND OBJECT's aliased line-width range. Those are device
// limits queried once at backend init and constant for the renderer's lifetime,
// so they are not part of the key (the version gate never covered them either).
struct DynamicTailKey {
Int viewport[4] = {0, 0, 0, 0};
Float depthRange[2] = {0.0f, 0.0f};
Float blendColor[4] = {0.0f, 0.0f, 0.0f, 0.0f};
Float polygonOffsetFactor = 0.0f;
Float polygonOffsetUnits = 0.0f;
Float lineWidth = 0.0f;
Uint32 stencilValueMask[2] = {0, 0};
Uint32 stencilWriteMask[2] = {0, 0};
Int stencilRef[2] = {0, 0};
Int scissorBox[4] = {0, 0, 0, 0};
Int extentX = 0;
Int extentY = 0;
Uint32 preTransform = 0;
Bool scissorEnabled = false;
Bool isDefaultFbo = false;
Bool operator==(const DynamicTailKey& other) const {
// NaN in any float input makes this false, which only costs a redundant
// tail run - never a skipped one.
for (Uint32 i = 0; i < 4; ++i) {
if (viewport[i] != other.viewport[i] || blendColor[i] != other.blendColor[i] ||
scissorBox[i] != other.scissorBox[i]) {
return false;
}
}
for (Uint32 i = 0; i < 2; ++i) {
if (depthRange[i] != other.depthRange[i] ||
stencilValueMask[i] != other.stencilValueMask[i] ||
stencilWriteMask[i] != other.stencilWriteMask[i] ||
stencilRef[i] != other.stencilRef[i]) {
return false;
}
}
return polygonOffsetFactor == other.polygonOffsetFactor &&
polygonOffsetUnits == other.polygonOffsetUnits && lineWidth == other.lineWidth &&
extentX == other.extentX && extentY == other.extentY &&
preTransform == other.preTransform && scissorEnabled == other.scissorEnabled &&
isDefaultFbo == other.isDefaultFbo;
}
};
DynamicTailKey dynamicTailKey{};
};
static DynamicStateShadow g_dynamicStateShadow;
@@ -5049,18 +5113,61 @@ void main() {
shadow.dynamicTailIsDefaultFbo == isDefaultFbo) {
return;
}
ApplyGLViewportState(frame.commandBuffer, extent, m_swapchainObject.GetPreTransform(), isDefaultFbo);
const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
// Second-level VALUE gate: the version moved, but RenderState's version counts
// every parameter, most of which this tail never reads. Build the key over
// exactly the tail's inputs (inventory in DynamicTailKey) out of one bulk
// parameters fetch and compare; an equal key means every Apply* below would
// re-derive the value its shadow already holds.
DynamicStateShadow::DynamicTailKey key;
{
const RenderStateParameters& p = MG_State::pGLContext->GetRenderStateParameters();
key.viewport[0] = p.Viewport.x();
key.viewport[1] = p.Viewport.y();
key.viewport[2] = p.Viewport.z();
key.viewport[3] = p.Viewport.w();
key.depthRange[0] = p.DepthRange.x();
key.depthRange[1] = p.DepthRange.y();
key.blendColor[0] = p.BlendColor.x();
key.blendColor[1] = p.BlendColor.y();
key.blendColor[2] = p.BlendColor.z();
key.blendColor[3] = p.BlendColor.w();
key.polygonOffsetFactor = p.PolygonOffsetFactor;
key.polygonOffsetUnits = p.PolygonOffsetUnits;
key.lineWidth = p.LineWidth;
// StencilStates[0] is Front, [1] is Back (RenderState::GetStencilFaceIndex),
// the same order ApplyStencilState reads them in.
for (Uint32 face = 0; face < 2; ++face) {
key.stencilValueMask[face] = p.StencilStates[face].ValueMask;
key.stencilWriteMask[face] = p.StencilStates[face].WriteMask;
key.stencilRef[face] = p.StencilStates[face].Ref;
}
key.scissorEnabled = p.ScissorTestEnabled;
key.scissorBox[0] = p.ScissorBox.x();
key.scissorBox[1] = p.ScissorBox.y();
key.scissorBox[2] = p.ScissorBox.z();
key.scissorBox[3] = p.ScissorBox.w();
key.extentX = extent.x();
key.extentY = extent.y();
key.preTransform = static_cast<Uint32>(preTransform);
key.isDefaultFbo = isDefaultFbo;
}
if (shadow.dynamicTailValid && shadow.dynamicTailKey == key) {
// Re-arm the cheap version gate so an unchanged-parameters run of draws after
// this one costs the four-integer compare again.
shadow.dynamicTailParamsVersion = paramsVersion;
return;
}
ApplyGLViewportState(frame.commandBuffer, extent, preTransform, isDefaultFbo);
ApplyBlendConstants(frame.commandBuffer);
ApplyPolygonOffsetState(frame.commandBuffer);
ApplyLineWidthState(frame.commandBuffer);
ApplyStencilState(frame.commandBuffer);
const Bool scissorEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest);
VkRect2D scissor{};
if (scissorEnabled) {
const auto& scissorBox = MG_State::pGLContext->GetScissorBox();
scissor = isDefaultFbo
? MakeDefaultFramebufferScissorRect(scissorBox, extent, m_swapchainObject.GetPreTransform())
: MakeClampedScissorRect(scissorBox, extent);
if (key.scissorEnabled) {
const IntVec4 scissorBox(key.scissorBox[0], key.scissorBox[1], key.scissorBox[2], key.scissorBox[3]);
scissor = isDefaultFbo ? MakeDefaultFramebufferScissorRect(scissorBox, extent, preTransform)
: MakeClampedScissorRect(scissorBox, extent);
} else {
scissor.offset = {0, 0};
scissor.extent = { (Uint)extent.x(), (Uint)extent.y() };
@@ -5071,18 +5178,28 @@ void main() {
shadow.dynamicTailExtentX = extent.x();
shadow.dynamicTailExtentY = extent.y();
shadow.dynamicTailIsDefaultFbo = isDefaultFbo;
shadow.dynamicTailKey = key;
}
Uint32 VulkanRenderer::GetBaseTransformFlagsRaw() {
Uint32 VulkanRenderer::GetBaseTransformFlagsRaw(Bool isDefaultFbo) {
// GetShaderTransformFlags is a function of the pre-transform AND of whether
// the bound draw framebuffer is the default one (the Y-flip/rotation bits
// apply only when presenting). Memo keyed on both; keying on the
// pre-transform alone served an FBO pass's unflipped flags to the following
// default-framebuffer pass and flipped the whole frame.
// isDefaultFbo is supplied by the caller: every draw-path caller has already
// resolved the bound draw framebuffer (and its default-ness) for its own
// guards, and re-walking the binding slot + the virtual IsDefaultFramebuffer
// per draw showed up in the profile. Callers MUST pass the value derived from
// the SAME draw-framebuffer binding the draw uses - see the assert below.
MOBILEGL_ASSERT(
[&] {
const auto& fbo =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
return isDefaultFbo == (fbo != nullptr && fbo->IsDefaultFramebuffer());
}(),
"GetBaseTransformFlagsRaw: isDefaultFbo does not match the bound draw framebuffer");
const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
const auto& currentDrawFBO =
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
const Bool isDefaultFbo = currentDrawFBO != nullptr && currentDrawFBO->IsDefaultFramebuffer();
if (!m_baseTransformFlagsKeyValid || preTransform != m_baseTransformFlagsPreTransform ||
isDefaultFbo != m_baseTransformFlagsIsDefaultFbo) {
m_baseTransformFlagsCache = GetShaderTransformFlags(preTransform).GetRaw();
@@ -5173,7 +5290,10 @@ void main() {
return false;
}
}
if (GetBaseTransformFlagsRaw() != snap.baseTransformFlags) {
// The FBO identity+version compare above proved this draw's framebuffer is the
// snapshotting draw's, so its default-ness is the snapshot's too - no second walk
// of the binding slot and no virtual IsDefaultFramebuffer call.
if (GetBaseTransformFlagsRaw(snap.drawFboIsDefault) != snap.baseTransformFlags) {
return false;
}
if (m_textureManager->GetResourceEraseEpoch() != snap.textureEraseEpoch ||
@@ -5476,8 +5596,9 @@ void main() {
fillSnap->valid = false;
m_setupDrawSnapshotMru = fillIndex;
}
const Bool drawFboIsDefault = drawFbo != nullptr && drawFbo->IsDefaultFramebuffer();
ProgramFactory::CompileOptionFlags transformFlags =
ProgramFactory::CompileOptionFlags(GetBaseTransformFlagsRaw());
ProgramFactory::CompileOptionFlags(GetBaseTransformFlagsRaw(drawFboIsDefault));
// Captured draws take the xfb-decorated program variant.
if (m_transformFeedbackFeatureEnabled && MG_State::pGLContext->IsTransformFeedbackActive() &&
program.GetTransformFeedbackVaryingCount() > 0) {
@@ -5813,10 +5934,10 @@ void main() {
snap.vaoConfigVersion = vao.GetConfigVersion();
snap.drawFbo = drawFbo.get();
snap.fboVersion = drawFbo->GetObjectVersion();
snap.drawFboIsDefault = drawFbo->IsDefaultFramebuffer();
snap.drawFboIsDefault = drawFboIsDefault;
snap.renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
snap.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
snap.baseTransformFlags = GetBaseTransformFlagsRaw();
snap.baseTransformFlags = GetBaseTransformFlagsRaw(drawFboIsDefault);
snap.resolvedTransformFlags = transformFlags.GetRaw();
snap.renderPassHash = nowActiveRenderPass->hash;
snap.imageIndex = m_imageIndexAcquired;
@@ -659,7 +659,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool m_baseTransformFlagsIsDefaultFbo = false;
Bool m_baseTransformFlagsKeyValid = false;
Uint32 m_baseTransformFlagsCache = 0;
Uint32 GetBaseTransformFlagsRaw();
// isDefaultFbo must be the default-ness of the CURRENTLY bound draw framebuffer;
// every caller already has it in hand from its own guards.
Uint32 GetBaseTransformFlagsRaw(Bool isDefaultFbo);
// Drops every memoized pipeline handle. Required at command-buffer
// boundaries and whenever any pipeline may have been destroyed. Also drops
// the cached pipeline-state hash: the same boundaries can retire the GL