mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user