mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[Perf] (MG_Backend): key DirectVulkan's pipeline memo on state values, not a version that never repeats
Two per-draw churn costs, one cause each. A blend toggle switched pipelines through a memo keyed on a monotonic pipeline-state version - which never repeats, so flipping GL_BLEND off and back on produced a "new" key both times, forced the full SetupDraw and rebuilt the whole pipeline payload for a pipeline the cache already held. The memo now keys on a value hash of the pipeline-relevant fixed-function state, recomputed only when the state version moved, and the consecutive-draw fast path re-resolves just the pipeline through it when nothing but render state changed. Blaze3D brackets every batch with exactly this toggle; mc_state_toggle drops 36% (6629 -> 4230 ns/op, 4.8x native to 3.7x). The sampler-churn cost had the same shape as the Espryt side fixed separately: glBindSampler bumps the frontend texture-bind generation even when it re-binds the sampler the unit already holds, so the per-draw fast path died every draw. The fast path now proves each binding's descriptor inputs unchanged - texture and sampler lifetime ids, parameter and content sums, the sampling-resolution generation, image epochs and exact layouts - and reuses the binding's cached VkDescriptorImageInfo instead of re-running the resolve chain. mc_sampler_churn drops 30% (1597 -> 1125), and the proof machinery pays for itself on the uniform-range case too (-17%). mc_tex_param stays where it is on this backend deliberately: profiling shows its remaining cost is frontend validation with zero backend work, unreachable from Renderer/. All nine cases measured on both backends, interleaved A/B, no case worse than noise. Unit tests 421/421.
This commit is contained in:
@@ -212,6 +212,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// reuse cannot outlive a single frame (see SamplerResolveMemo).
|
||||
for (auto& memo : m_samplerResolveMemo) {
|
||||
memo.valid = false;
|
||||
memo.infoValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,9 +255,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool UniformManager::ResolveSamplerDescriptor(VkCommandBuffer commandBuffer,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 binding, VkDescriptorImageInfo& outImageInfo) const {
|
||||
Uint32 binding, VkDescriptorImageInfo& outImageInfo,
|
||||
Bool trustUnchangedHint) const {
|
||||
MOBILEGL_ASSERT(m_textureManager != nullptr, "ResolveSamplerDescriptor: texture manager is null");
|
||||
MOBILEGL_ASSERT(m_samplerManager != nullptr, "ResolveSamplerDescriptor: sampler manager is null");
|
||||
// The caller proved every input of this binding's resolution unchanged since the
|
||||
// last full resolve (which also filled the cache), so the whole chain below -
|
||||
// texture/sampler resolution, completeness probe, sync, layout handling, sampler
|
||||
// and view lookups - would recompute the identical descriptor.
|
||||
if (trustUnchangedHint && binding < m_samplerResolveMemo.size() &&
|
||||
m_samplerResolveMemo[binding].infoValid) {
|
||||
outImageInfo = m_samplerResolveMemo[binding].info;
|
||||
return true;
|
||||
}
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerNameByBinding.size(),
|
||||
"ResolveSamplerDescriptor: sampler binding %u name lookup out of range", binding);
|
||||
// Raw-pointer resolve to skip the SharedPtr atomic refcount churn: the bound texture stays
|
||||
@@ -430,7 +441,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.imageView = sampledImageView,
|
||||
.imageLayout = resource->layout,
|
||||
};
|
||||
return outImageInfo.sampler != VK_NULL_HANDLE;
|
||||
if (outImageInfo.sampler == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
}
|
||||
if (binding < m_samplerResolveMemo.size()) {
|
||||
m_samplerResolveMemo[binding].info = outImageInfo;
|
||||
m_samplerResolveMemo[binding].infoValid = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool UniformManager::ResolveSamplerDescriptorOverride(
|
||||
@@ -808,10 +826,55 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return m_fallbackTexture2D;
|
||||
}
|
||||
|
||||
Bool UniformManager::ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 binding,
|
||||
MG_State::GLState::ITextureObject*& outTexture,
|
||||
const MG_State::GLState::SamplerObject*& outSampler) const {
|
||||
// Open-coded ResolveSamplerTextureRaw so the unit is resolved once for both the
|
||||
// texture and the sampler override - this runs per binding per full-path draw,
|
||||
// and program-alternating draw streams take the full path on every draw.
|
||||
MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, "ResolveSampledBinding: GL context is null");
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerUniformLocationByBinding.size(),
|
||||
"ResolveSampledBinding: sampler location binding %u out of range", binding);
|
||||
MOBILEGL_ASSERT(binding < programObj.samplerTextureTargetByBinding.size(),
|
||||
"ResolveSampledBinding: sampler target binding %u out of range", binding);
|
||||
const Int location = programObj.samplerUniformLocationByBinding[binding];
|
||||
const Int unit = ResolveSamplerUnitIndex(program, location, binding);
|
||||
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
MG_State::GLState::ITextureObject* texture =
|
||||
textureUnit.GetBindingSlot(preferredTarget).GetBoundObject().get();
|
||||
// Undefined default texture (name 0, no image) resolves as "unbound", exactly
|
||||
// like ResolveSamplerTextureRaw reports it.
|
||||
if (MG_State::GLState::IsUndefinedDefaultTexture(texture)) {
|
||||
texture = nullptr;
|
||||
}
|
||||
if (texture == nullptr) {
|
||||
// ResolveSamplerDescriptor will substitute the fallback texture for this binding;
|
||||
// include it in the sampled set so the pre-render-pass sync/transition pass covers
|
||||
// its first use instead of leaving that work to happen inside an active pass.
|
||||
if (preferredTarget != TextureTarget::Texture2D &&
|
||||
preferredTarget != TextureTarget::TextureRectangle) {
|
||||
return false;
|
||||
}
|
||||
texture = GetFallbackTexture(preferredTarget).get();
|
||||
}
|
||||
const auto& samplerOverride = textureUnit.GetSamplerObject();
|
||||
outTexture = texture;
|
||||
outSampler = samplerOverride ? samplerOverride.get()
|
||||
: (texture != nullptr ? texture->GetSamplerObject().get() : nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool UniformManager::CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) {
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures,
|
||||
Vector<SampledBindingRecord>* outBindingRecords) {
|
||||
outTextures.clear();
|
||||
if (outBindingRecords != nullptr) {
|
||||
outBindingRecords->clear();
|
||||
}
|
||||
|
||||
const Uint32 bindingCount =
|
||||
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
|
||||
@@ -820,17 +883,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
|
||||
MG_State::GLState::ITextureObject* texture = ResolveSamplerTextureRaw(program, programObj, binding);
|
||||
if (!texture) {
|
||||
// ResolveSamplerDescriptor will substitute the fallback texture for this binding;
|
||||
// include it in the sampled set so the pre-render-pass sync/transition pass covers
|
||||
// its first use instead of leaving that work to happen inside an active pass.
|
||||
const TextureTarget preferredTarget = programObj.samplerTextureTargetByBinding[binding];
|
||||
if (preferredTarget != TextureTarget::Texture2D &&
|
||||
preferredTarget != TextureTarget::TextureRectangle) {
|
||||
continue;
|
||||
}
|
||||
texture = GetFallbackTexture(preferredTarget).get();
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||
if (!ResolveSampledBinding(program, programObj, binding, texture, sampler)) {
|
||||
continue;
|
||||
}
|
||||
if (outBindingRecords != nullptr) {
|
||||
outBindingRecords->push_back({texture != nullptr ? texture->GetLifetimeId() : 0,
|
||||
sampler != nullptr ? sampler->GetLifetimeId() : 0});
|
||||
}
|
||||
|
||||
auto found = std::find(outTextures.begin(), outTextures.end(), texture);
|
||||
@@ -841,6 +901,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool UniformManager::SampledBindingsUnchanged(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
const Vector<SampledBindingRecord>& previousRecords) const {
|
||||
const Uint32 bindingCount =
|
||||
std::min<Uint32>(m_maxBindings, static_cast<Uint32>(programObj.bindingKinds.size()));
|
||||
SizeT recordIndex = 0;
|
||||
for (Uint32 binding = 0; binding < bindingCount; ++binding) {
|
||||
if (programObj.bindingKinds[binding] != ProgramFactory::DescriptorBindingKind::CombinedImageSampler) {
|
||||
continue;
|
||||
}
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||
if (!ResolveSampledBinding(program, programObj, binding, texture, sampler)) {
|
||||
continue;
|
||||
}
|
||||
if (recordIndex >= previousRecords.size()) {
|
||||
return false;
|
||||
}
|
||||
const SampledBindingRecord& record = previousRecords[recordIndex++];
|
||||
if (record.textureLifetimeId != (texture != nullptr ? texture->GetLifetimeId() : 0) ||
|
||||
record.samplerLifetimeId != (sampler != nullptr ? sampler->GetLifetimeId() : 0)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return recordIndex == previousRecords.size();
|
||||
}
|
||||
|
||||
Bool UniformManager::CollectStorageImageTextures(
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
@@ -1160,7 +1247,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 frameIndex,
|
||||
VkPipelineBindPoint bindPoint,
|
||||
const SamplerBindingOverride* samplerBindingOverride) {
|
||||
const SamplerBindingOverride* samplerBindingOverride,
|
||||
Bool samplerDescriptorsUnchangedHint) {
|
||||
auto& frame = m_frames[frameIndex];
|
||||
if (frame.descriptorPools.empty()) {
|
||||
MGLOG_E("UniformDescriptorBinder::BindProgramUniformBuffers failed: frame descriptor pools are invalid");
|
||||
@@ -1341,7 +1429,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
samplerBindingOverride->sampler != nullptr) {
|
||||
hasImage = ResolveSamplerDescriptorOverride(*samplerBindingOverride, imageInfo);
|
||||
} else {
|
||||
hasImage = ResolveSamplerDescriptor(commandBuffer, program, programObj, binding, imageInfo);
|
||||
hasImage = ResolveSamplerDescriptor(commandBuffer, program, programObj, binding, imageInfo,
|
||||
samplerDescriptorsUnchangedHint);
|
||||
}
|
||||
if (!hasImage) {
|
||||
MGLOG_E(
|
||||
|
||||
@@ -53,18 +53,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// caches - a live layout's entry must never be purged (its sets would be
|
||||
// unreachable pool slots), so there is deliberately no age-based sweep here.
|
||||
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
|
||||
// One record per visited CombinedImageSampler binding (post fallback substitution,
|
||||
// in binding order): the resolved texture and effective sampler, as never-reused
|
||||
// lifetime ids so a freed-and-reallocated object at the same heap address can only
|
||||
// MISS a comparison, never false-hit it (same ABA rule as SamplerResolveMemo).
|
||||
struct SampledBindingRecord {
|
||||
Uint64 textureLifetimeId = 0;
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
};
|
||||
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures);
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures,
|
||||
Vector<SampledBindingRecord>* outBindingRecords = nullptr);
|
||||
// Shadow-compare for the SetupDraw fast path: re-runs the CollectSampledTextures
|
||||
// walk and reports whether every visited binding still resolves to the recorded
|
||||
// (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a
|
||||
// redundant glBindSampler, which always bumps it) does not prove the sampled set
|
||||
// moved; this walk does, without rebuilding the set or falling off the fast path.
|
||||
Bool SampledBindingsUnchanged(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
const Vector<SampledBindingRecord>& previousRecords) const;
|
||||
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
||||
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
|
||||
// every input of every combined-image-sampler resolution is unchanged since the
|
||||
// previous draw's resolve - same (texture, sampler) per binding, texture params
|
||||
// sum, sampling-resolution generation (sampler params + texture shape), image
|
||||
// epochs AND per-resource layout values - so the per-binding cached
|
||||
// VkDescriptorImageInfo may be reused without re-running the resolve chain.
|
||||
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 frameIndex,
|
||||
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
const SamplerBindingOverride* samplerBindingOverride = nullptr);
|
||||
const SamplerBindingOverride* samplerBindingOverride = nullptr,
|
||||
Bool samplerDescriptorsUnchangedHint = false);
|
||||
|
||||
// Pure format-policy helper kept public for host regression tests. Formatted storage
|
||||
// images use their shader qualifier; transformed float images use glBindImageTexture's
|
||||
@@ -114,6 +138,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||
// Shared per-binding resolution for CollectSampledTextures and
|
||||
// SampledBindingsUnchanged, so membership and comparison can never diverge:
|
||||
// texture after the fallback substitution (may still be null when no fallback
|
||||
// exists), effective sampler = unit override else the texture's own sampler.
|
||||
// False = the binding is skipped (unbound with a non-2D fallback target).
|
||||
Bool ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
MG_State::GLState::ITextureObject*& outTexture,
|
||||
const MG_State::GLState::SamplerObject*& outSampler) const;
|
||||
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
|
||||
// the bound texture stays alive through the draw via GL binding state, so callers that
|
||||
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
|
||||
@@ -121,9 +154,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
|
||||
// trustUnchangedHint: reuse this binding's cached VkDescriptorImageInfo outright
|
||||
// (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof
|
||||
// obligations the caller carries).
|
||||
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
VkDescriptorImageInfo& outImageInfo) const;
|
||||
VkDescriptorImageInfo& outImageInfo,
|
||||
Bool trustUnchangedHint = false) const;
|
||||
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
|
||||
VkDescriptorImageInfo& outImageInfo) const;
|
||||
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
@@ -253,6 +290,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SamplerNumericDomain viewFormatDomain = SamplerNumericDomain::Unknown;
|
||||
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
||||
Bool viewFormatValid = false;
|
||||
// Whole resolved descriptor from this binding's last full resolve. Reused
|
||||
// ONLY under ResolveSamplerDescriptor's trustUnchangedHint, whose caller
|
||||
// proves every resolve input unchanged; cleared with the per-frame reset
|
||||
// (the cached VkSampler outlives a frame only via a fresh resolve, which
|
||||
// also re-stamps it against VkSamplerManager's frame-boundary sweep).
|
||||
VkDescriptorImageInfo info{};
|
||||
Bool infoValid = false;
|
||||
};
|
||||
mutable Vector<SamplerResolveMemo> m_samplerResolveMemo;
|
||||
};
|
||||
|
||||
@@ -4164,6 +4164,77 @@ void main() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Boost-style hash combine. The inputs are tiny enum ordinals and bit masks, so
|
||||
// full avalanche is unnecessary; the combine only has to keep distinct state
|
||||
// vectors apart under the memo's otherwise-exact key.
|
||||
static inline Uint64 CombinePipelineStateWord(Uint64 hash, Uint64 word) {
|
||||
return hash ^ (word + 0x9E3779B97F4A7C15ull + (hash << 6) + (hash >> 2));
|
||||
}
|
||||
|
||||
// Value hash over every fixed-function GL state the pipeline payload reads that
|
||||
// the memo key's other fields (mode, program hash, vertex-input hash, render-pass
|
||||
// hash, transform flags) do not already pin down. Enumerated against the payload
|
||||
// build in GetOrCreatePipeline - any new GL-state read there must be added here:
|
||||
// - capability bits: CullFace, DepthTest, PolygonOffsetFill (mode gating rides
|
||||
// the memo's mode key), RasterizerDiscard, ColorLogicOp, StencilTest,
|
||||
// PrimitiveRestart(+FixedIndex), plus the depth write mask
|
||||
// - patch vertices, polygon mode, cull face mode, depth func, logic op
|
||||
// - front/back stencil ops + compare funcs (ref/mask are dynamic state)
|
||||
// - per draw buffer up to the render pass's colour span: indexed blend enable,
|
||||
// blend factors/equations, indexed colour write mask (broadcast from index 0
|
||||
// when the device lacks independentBlend - the same read the payload does)
|
||||
// FBO-derived payload inputs (attachment presence/formats/draw-buffer gating) are
|
||||
// pinned by the render-pass hash key, exactly as the version-keyed memo relied on.
|
||||
Uint64 VulkanRenderer::ComputePipelineStateHash(Uint32 colorAttachmentCount) const {
|
||||
auto& ctx = *MG_State::pGLContext;
|
||||
Uint64 capabilityBits = 0;
|
||||
capabilityBits |= ctx.IsCapabilityEnabled(CapabilityInput::CullFace) ? 1ull << 0 : 0;
|
||||
capabilityBits |= ctx.IsCapabilityEnabled(CapabilityInput::DepthTest) ? 1ull << 1 : 0;
|
||||
capabilityBits |= ctx.IsCapabilityEnabled(CapabilityInput::PolygonOffsetFill) ? 1ull << 2 : 0;
|
||||
capabilityBits |= ctx.IsCapabilityEnabled(CapabilityInput::RasterizerDiscard) ? 1ull << 3 : 0;
|
||||
capabilityBits |= ctx.IsCapabilityEnabled(CapabilityInput::ColorLogicOp) ? 1ull << 4 : 0;
|
||||
capabilityBits |= ctx.IsCapabilityEnabled(CapabilityInput::StencilTest) ? 1ull << 5 : 0;
|
||||
capabilityBits |= ctx.IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ? 1ull << 6 : 0;
|
||||
capabilityBits |= ctx.IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex) ? 1ull << 7 : 0;
|
||||
capabilityBits |= ctx.GetDepthMask() ? 1ull << 8 : 0;
|
||||
Uint64 hash = CombinePipelineStateWord(0x243F6A8885A308D3ull, capabilityBits);
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(ctx.GetPatchVertices()));
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(ctx.GetPolygonModeFront()));
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(ctx.GetCullFaceMode()));
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(ctx.GetDepthFunc()));
|
||||
hash = CombinePipelineStateWord(hash, static_cast<Uint64>(ctx.GetLogicOp()));
|
||||
for (const StencilFace face : {StencilFace::Front, StencilFace::Back}) {
|
||||
const StencilFaceState& stencil = ctx.GetStencilState(face);
|
||||
hash = CombinePipelineStateWord(hash,
|
||||
static_cast<Uint64>(stencil.FailOp) |
|
||||
(static_cast<Uint64>(stencil.PassDepthPassOp) << 16) |
|
||||
(static_cast<Uint64>(stencil.PassDepthFailOp) << 32) |
|
||||
(static_cast<Uint64>(stencil.Func) << 48));
|
||||
}
|
||||
for (Uint32 i = 0; i < colorAttachmentCount; ++i) {
|
||||
BlendFactor srcRGB = BlendFactor::One;
|
||||
BlendFactor dstRGB = BlendFactor::Zero;
|
||||
BlendFactor srcAlpha = BlendFactor::One;
|
||||
BlendFactor dstAlpha = BlendFactor::Zero;
|
||||
BlendEquation colorEquation = BlendEquation::Add;
|
||||
BlendEquation alphaEquation = BlendEquation::Add;
|
||||
ctx.GetBlendFuncIndexed(i, srcRGB, dstRGB, srcAlpha, dstAlpha);
|
||||
ctx.GetBlendEquationIndexed(i, colorEquation, alphaEquation);
|
||||
const BoolVec4 mask = ctx.GetColorMaskIndexed(m_independentBlendFeatureEnabled ? i : 0);
|
||||
Uint64 attachmentWord = ctx.IsCapabilityEnabledIndexed(CapabilityInput::Blend, i) ? 1ull : 0;
|
||||
attachmentWord |= (mask.r() ? 1ull << 1 : 0) | (mask.g() ? 1ull << 2 : 0) |
|
||||
(mask.b() ? 1ull << 3 : 0) | (mask.a() ? 1ull << 4 : 0);
|
||||
attachmentWord |= static_cast<Uint64>(srcRGB) << 8;
|
||||
attachmentWord |= static_cast<Uint64>(dstRGB) << 16;
|
||||
attachmentWord |= static_cast<Uint64>(srcAlpha) << 24;
|
||||
attachmentWord |= static_cast<Uint64>(dstAlpha) << 32;
|
||||
attachmentWord |= static_cast<Uint64>(colorEquation) << 40;
|
||||
attachmentWord |= static_cast<Uint64>(alphaEquation) << 48;
|
||||
hash = CombinePipelineStateWord(hash, attachmentWord);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
// A program that runs a geometry shader AND captures transform feedback. Both halves are
|
||||
// link-time properties, so this is safe to fold into a pipeline keyed on the program hash.
|
||||
static Bool ProgramCapturesXfbFromGeometryStage(const MG_State::GLState::ProgramObject& program) {
|
||||
@@ -4192,7 +4263,7 @@ void main() {
|
||||
// PipelineCreatePayload field: draw mode (topology + polygon-fill depth-bias gate), program
|
||||
// content hash (folds program identity + link version + transform flags + shader stages),
|
||||
// vertex-input hash (VAO layout), render-pass hash (render targets + the draw-buffer/format
|
||||
// driven blend & write-mask gating), and the render-state version (all fixed-function state).
|
||||
// driven blend & write-mask gating), and the pipeline-state value hash (all fixed-function state).
|
||||
// Reset per-frame and on pipeline destruction so a memoized handle can never dangle.
|
||||
// The identity hash mixes buffer heap addresses (per-chunk VBOs mint a new
|
||||
// one per buffer); the memo and the pipeline payload key on the resolved
|
||||
@@ -4203,14 +4274,25 @@ void main() {
|
||||
const Uint64 renderPassHash = renderPassEntry.hash;
|
||||
// The pipeline-relevant subset only: glViewport / glScissor / glBlendColor / glStencilMask
|
||||
// and friends are dynamic state or not pipeline state at all, and keying the memo on the
|
||||
// all-state counter made any of them evict a perfectly good VkPipeline.
|
||||
// all-state counter made any of them evict a perfectly good VkPipeline. The memo compares
|
||||
// the VALUE hash of that subset, never the version itself: the version is monotonic, so
|
||||
// per-draw state flips (GL_BLEND toggles) would otherwise miss entries the memo holds.
|
||||
// The version only guards recomputing the hash - unchanged version, unchanged bytes.
|
||||
const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
|
||||
if (!m_pipelineStateHashValid || m_pipelineStateHashVersion != renderStateVersion ||
|
||||
m_pipelineStateHashColorCount != renderPassEntry.colorAttachmentCount) {
|
||||
m_pipelineStateHash = ComputePipelineStateHash(renderPassEntry.colorAttachmentCount);
|
||||
m_pipelineStateHashVersion = renderStateVersion;
|
||||
m_pipelineStateHashColorCount = renderPassEntry.colorAttachmentCount;
|
||||
m_pipelineStateHashValid = true;
|
||||
}
|
||||
const Uint64 pipelineStateHash = m_pipelineStateHash;
|
||||
for (Uint32 i = 0; i < m_pipelineMemoCount; ++i) {
|
||||
const PipelineMemoEntry& entry = m_pipelineMemo[i];
|
||||
if (entry.pipeline != VK_NULL_HANDLE && entry.mode == mode &&
|
||||
entry.programHash == programObj.hash && entry.vertexInputHash == vertexLayoutHash &&
|
||||
entry.renderPassHash == renderPassHash &&
|
||||
entry.renderStateVersion == renderStateVersion &&
|
||||
entry.pipelineStateHash == pipelineStateHash &&
|
||||
entry.transformFlags == transformFlags) {
|
||||
return entry.pipeline;
|
||||
}
|
||||
@@ -4704,7 +4786,7 @@ void main() {
|
||||
entry.programHash = programObj.hash;
|
||||
entry.vertexInputHash = vertexLayoutHash;
|
||||
entry.renderPassHash = renderPassHash;
|
||||
entry.renderStateVersion = renderStateVersion;
|
||||
entry.pipelineStateHash = pipelineStateHash;
|
||||
entry.transformFlags = transformFlags;
|
||||
entry.pipeline = pipeline;
|
||||
m_pipelineMemoNext = (m_pipelineMemoNext + 1) % kPipelineMemoSize;
|
||||
@@ -4814,7 +4896,7 @@ void main() {
|
||||
Bool VulkanRenderer::TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode,
|
||||
Flags<DrawSetupAspect> aspects, const DrawCmdParam& drawParams,
|
||||
const IndexBufferView* pIndexBufferView) {
|
||||
const SetupDrawSnapshot& snap = m_setupDrawSnapshot;
|
||||
SetupDrawSnapshot& snap = m_setupDrawSnapshot;
|
||||
if (!snap.valid || !frame.isCommandRecording) {
|
||||
return false;
|
||||
}
|
||||
@@ -4844,9 +4926,25 @@ void main() {
|
||||
drawFbo->GetObjectVersion() != snap.fboVersion) {
|
||||
return false;
|
||||
}
|
||||
if (MG_State::pGLContext->GetPipelineStateVersion() != snap.renderStateVersion ||
|
||||
MG_State::pGLContext->GetTextureBindGeneration() != snap.bindGeneration) {
|
||||
return false;
|
||||
// The two monotonic counters get a shadow-compare rescue instead of an
|
||||
// unconditional decline: both bump on state writes whose VALUE often lands
|
||||
// back on what the snapshot already describes (a GL_BLEND toggle between
|
||||
// two draws, a redundant glBindSampler), and declining here sends every
|
||||
// such draw through the full SetupDraw.
|
||||
const Uint renderStateVersion = MG_State::pGLContext->GetPipelineStateVersion();
|
||||
const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration();
|
||||
const Bool renderStateMoved = renderStateVersion != snap.renderStateVersion;
|
||||
const Bool bindsMoved = bindGeneration != snap.bindGeneration;
|
||||
if (renderStateMoved) {
|
||||
// Only the pipeline depends on the moved state - except the render-pass
|
||||
// flavor input (depth/stencil participation); a flip of that must take
|
||||
// the full path's pass selection.
|
||||
const Bool drawUsesDepthStencil =
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest) ||
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::StencilTest);
|
||||
if (drawUsesDepthStencil != snap.drawUsesDepthStencil) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (GetShaderTransformFlags(m_swapchainObject.GetPreTransform()).GetRaw() != snap.baseTransformFlags) {
|
||||
return false;
|
||||
@@ -4856,6 +4954,12 @@ void main() {
|
||||
m_renderPassManager->GetRenderbufferImageEpoch() != snap.renderbufferImageEpoch) {
|
||||
return false;
|
||||
}
|
||||
const auto& programObj = m_programFactory->GetOrCreateProgram(
|
||||
program, ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags));
|
||||
if (bindsMoved &&
|
||||
!m_uniformManager->SampledBindingsUnchanged(program, programObj, m_sampledBindingRecordsScratch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Same sampled set as the snapshotting draw (program/bind keys above);
|
||||
// verify content and params are untouched and every layout is still
|
||||
@@ -4869,6 +4973,16 @@ void main() {
|
||||
}
|
||||
Uint64 contentSum = 0;
|
||||
Uint64 paramsSum = 0;
|
||||
// The descriptor-reuse hint (see BindProgramUniformBuffers) additionally needs
|
||||
// every sampled resource still in the exact layout the cached descriptors hold.
|
||||
// A layout that moved to a different-but-sampleable one only clears the hint
|
||||
// (this draw re-resolves and re-caches) - the fast path itself stays valid.
|
||||
// bindsMoved does not clear the hint: reaching this point with a moved bind
|
||||
// generation means SampledBindingsUnchanged proved the per-binding (texture,
|
||||
// sampler) pairs identical, and the sums/generation checks below cover every
|
||||
// remaining descriptor input.
|
||||
const Bool layoutSnapshotUsable = m_sampledLayoutSnapshots.size() == sampledTextures.size();
|
||||
Bool samplerDescriptorsUnchanged = layoutSnapshotUsable;
|
||||
for (SizeT i = 0; i < sampledTextures.size(); ++i) {
|
||||
const auto* sampledTexture = sampledTextures[i];
|
||||
if (sampledTexture == nullptr) {
|
||||
@@ -4878,30 +4992,64 @@ void main() {
|
||||
if (resource == nullptr || !IsValidSampledImageLayout(resource->layout)) {
|
||||
return false;
|
||||
}
|
||||
if (layoutSnapshotUsable && m_sampledLayoutSnapshots[i] != resource->layout) {
|
||||
m_sampledLayoutSnapshots[i] = resource->layout;
|
||||
samplerDescriptorsUnchanged = false;
|
||||
}
|
||||
contentSum += sampledTexture->GetContentVersion();
|
||||
paramsSum += sampledTexture->GetTextureParamsVersion();
|
||||
}
|
||||
if (contentSum != snap.sampledContentSum || paramsSum != snap.sampledParamsSum) {
|
||||
return false;
|
||||
}
|
||||
const Uint64 samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
if (samplingResolutionGeneration != snap.samplingResolutionGeneration) {
|
||||
snap.samplingResolutionGeneration = samplingResolutionGeneration;
|
||||
samplerDescriptorsUnchanged = false;
|
||||
}
|
||||
for (SizeT i = 0; i < sampledTextures.size(); ++i) {
|
||||
if (sampledTextures[i] != nullptr && sampledResources[i] != nullptr) {
|
||||
m_textureManager->StampResourceRecordingUse(*sampledResources[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Everything the full path would re-resolve is provably unchanged; run
|
||||
// Everything the full path would re-resolve is provably unchanged - or, for
|
||||
// a moved pipeline-state version, reduces to re-resolving just the pipeline
|
||||
// through the value-keyed memo against the still-active render pass. Run
|
||||
// only the per-draw tail.
|
||||
VkPipeline pipeline = snap.pipeline;
|
||||
if (renderStateMoved) {
|
||||
// Same lookup the full path would do; every input (FBO + version, image
|
||||
// index, depth/stencil participation, image epochs, no pending clears)
|
||||
// was verified unchanged above, so this is a pure cache hit on the same
|
||||
// entry the snapshot's pipeline was built against.
|
||||
const RenderPassEntry& renderPassEntry = m_renderPassManager->GetOrCreateRenderPass(
|
||||
*drawFbo, m_imageIndexAcquired, snap.drawUsesDepthStencil);
|
||||
if (!activeRenderPass->CompatibleWith(renderPassEntry)) {
|
||||
return false;
|
||||
}
|
||||
pipeline = GetOrCreatePipeline(mode, program, programObj,
|
||||
ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags),
|
||||
vao, renderPassEntry);
|
||||
if (pipeline == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Every decline is behind us: the snapshot again describes the current
|
||||
// counters, so the next draw's compare is two integer loads.
|
||||
snap.renderStateVersion = renderStateVersion;
|
||||
snap.bindGeneration = bindGeneration;
|
||||
snap.pipeline = pipeline;
|
||||
if (!g_dynamicStateShadow.graphicsPipelineValid ||
|
||||
g_dynamicStateShadow.graphicsPipeline != snap.pipeline) {
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, snap.pipeline);
|
||||
g_dynamicStateShadow.graphicsPipeline != pipeline) {
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||
g_dynamicStateShadow.graphicsPipelineValid = true;
|
||||
g_dynamicStateShadow.graphicsPipeline = snap.pipeline;
|
||||
g_dynamicStateShadow.graphicsPipeline = pipeline;
|
||||
}
|
||||
const auto& programObj = m_programFactory->GetOrCreateProgram(
|
||||
program, ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags));
|
||||
if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj,
|
||||
m_frameContext.GetCurrentFrameIndex())) {
|
||||
m_frameContext.GetCurrentFrameIndex(),
|
||||
VK_PIPELINE_BIND_POINT_GRAPHICS, nullptr,
|
||||
samplerDescriptorsUnchanged)) {
|
||||
return false;
|
||||
}
|
||||
if (!UploadAndBindVertexBuffers(frame.commandBuffer, vao, programObj, drawParams, pIndexBufferView)) {
|
||||
@@ -5043,8 +5191,8 @@ void main() {
|
||||
m_lastSampledSetTransformFlags == transformFlags &&
|
||||
m_lastSampledSetBindGeneration == bindGeneration;
|
||||
if (!sampledSetUnchanged) {
|
||||
const Bool hasSampledTextures =
|
||||
m_uniformManager->CollectSampledTextures(program, programObj, sampledTextures);
|
||||
const Bool hasSampledTextures = m_uniformManager->CollectSampledTextures(
|
||||
program, programObj, sampledTextures, &m_sampledBindingRecordsScratch);
|
||||
MOBILEGL_ASSERT(hasSampledTextures, "%s: CollectSampledTextures failed", __func__);
|
||||
m_lastSampledSetValid = true;
|
||||
m_lastSampledSetProgramLifetimeId = programLifetimeId;
|
||||
@@ -5317,14 +5465,25 @@ void main() {
|
||||
snap.textureEraseEpoch = m_textureManager->GetResourceEraseEpoch();
|
||||
snap.textureImageEpoch = m_textureManager->GetTextureImageEpoch();
|
||||
snap.renderbufferImageEpoch = m_renderPassManager->GetRenderbufferImageEpoch();
|
||||
snap.drawUsesDepthStencil = drawUsesDepthStencil;
|
||||
snap.renderPassExtent = renderPassEntry->extent;
|
||||
snap.pipeline = pipeline;
|
||||
snap.samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
Uint64 snapContentSum = 0;
|
||||
Uint64 snapParamsSum = 0;
|
||||
for (const auto* sampledTexture : sampledTextures) {
|
||||
if (sampledTexture != nullptr) {
|
||||
snapContentSum += sampledTexture->GetContentVersion();
|
||||
snapParamsSum += sampledTexture->GetTextureParamsVersion();
|
||||
// Record each resource's layout VALUE for the descriptor-reuse hint;
|
||||
// transitions above updated the resources in place, so this reads the
|
||||
// layouts the descriptors just resolved against.
|
||||
m_sampledLayoutSnapshots.assign(sampledTextures.size(), VK_IMAGE_LAYOUT_UNDEFINED);
|
||||
for (SizeT i = 0; i < sampledTextures.size(); ++i) {
|
||||
const auto* sampledTexture = sampledTextures[i];
|
||||
if (sampledTexture == nullptr) {
|
||||
continue;
|
||||
}
|
||||
snapContentSum += sampledTexture->GetContentVersion();
|
||||
snapParamsSum += sampledTexture->GetTextureParamsVersion();
|
||||
if (sampledResources[i] != nullptr) {
|
||||
m_sampledLayoutSnapshots[i] = sampledResources[i]->layout;
|
||||
}
|
||||
}
|
||||
snap.sampledContentSum = snapContentSum;
|
||||
|
||||
@@ -531,7 +531,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Per-pipeline provoking-vertex mode. capturesXfbFromGeometryStage must be a LINK-TIME
|
||||
// property of the program, never the dynamic "is transform feedback active" flag: the
|
||||
// 8-entry m_pipelineMemo and the SetupDrawSnapshot fast path key on programObj.hash and
|
||||
// GetRenderStateParametersVersion(), neither of which moves when glBeginTransformFeedback is
|
||||
// the pipeline-state value hash, neither of which moves when glBeginTransformFeedback is
|
||||
// called, so a dynamic input here would hand back a stale VkPipeline.
|
||||
VkProvokingVertexModeEXT SelectProvokingVertexMode(VkPrimitiveTopology topology,
|
||||
Bool capturesXfbFromGeometryStage) const;
|
||||
@@ -623,7 +623,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 programHash = 0;
|
||||
Uint64 vertexInputHash = 0;
|
||||
Uint64 renderPassHash = 0;
|
||||
Uint renderStateVersion = 0;
|
||||
// VALUE hash of the pipeline-relevant fixed-function state (see
|
||||
// ComputePipelineStateHash), not the monotonic pipeline-state version:
|
||||
// the version never repeats, so a per-draw GL_BLEND toggle would miss
|
||||
// all entries forever even though the state alternates between two
|
||||
// values the memo already holds.
|
||||
Uint64 pipelineStateHash = 0;
|
||||
ProgramFactory::CompileOptionFlags transformFlags = {};
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
@@ -631,11 +636,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
|
||||
Uint32 m_pipelineMemoCount = 0;
|
||||
Uint32 m_pipelineMemoNext = 0;
|
||||
// Hash of every fixed-function GL state the pipeline payload reads that the
|
||||
// memo key's other fields (mode / program / vertex input / render pass /
|
||||
// transform flags) do not already pin down. Equal hash under an equal rest
|
||||
// of key => byte-identical PipelineCreatePayload. Cached per pipeline-state
|
||||
// version: the version is monotonic and bumps on every pipeline-state
|
||||
// change, so an unchanged (version, colorAttachmentCount) proves the state
|
||||
// bytes are unchanged and the hash can be reused without re-reading them.
|
||||
Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount) const;
|
||||
Uint m_pipelineStateHashVersion = 0;
|
||||
Uint32 m_pipelineStateHashColorCount = 0;
|
||||
Uint64 m_pipelineStateHash = 0;
|
||||
Bool m_pipelineStateHashValid = false;
|
||||
// Drops every memoized pipeline handle. Required at command-buffer
|
||||
// boundaries and whenever any pipeline may have been destroyed.
|
||||
// boundaries and whenever any pipeline may have been destroyed. Also drops
|
||||
// the cached pipeline-state hash: the same boundaries can retire the GL
|
||||
// context whose monotonic version the cache is keyed on.
|
||||
void InvalidatePipelineMemo() {
|
||||
m_pipelineMemoCount = 0;
|
||||
m_pipelineMemoNext = 0;
|
||||
m_pipelineStateHashValid = false;
|
||||
}
|
||||
UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
|
||||
UniquePtr<ProgramFactory> m_programFactory;
|
||||
@@ -707,6 +727,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 renderbufferImageEpoch = 0;
|
||||
Uint64 sampledContentSum = 0;
|
||||
Uint64 sampledParamsSum = 0;
|
||||
// Guards the sampler-descriptor reuse hint: bumped by any sampler-object
|
||||
// parameter or texture shape change (see GetSamplingResolutionGeneration),
|
||||
// none of which the sums above cover.
|
||||
Uint64 samplingResolutionGeneration = 0;
|
||||
// Render-pass flavor input (DepthTest || StencilTest at snapshot time).
|
||||
// A pipeline-state change that leaves this equal cannot change which
|
||||
// render pass GetOrCreateRenderPass would pick, so the fast path may
|
||||
// re-resolve just the pipeline against the active pass; a change that
|
||||
// flips it must fall back to the full path's pass selection.
|
||||
Bool drawUsesDepthStencil = false;
|
||||
IntVec2 renderPassExtent = {0, 0};
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
};
|
||||
@@ -715,11 +745,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// 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;
|
||||
// Per-binding (texture, effective sampler) lifetime-id records from the same
|
||||
// CollectSampledTextures walk that filled m_sampledTexturesScratch. The fast
|
||||
// path shadow-compares against them (SampledBindingsUnchanged) when the
|
||||
// texture bind generation moved, so a redundant glBindSampler/glBindTexture
|
||||
// storm that resolves to the same bindings keeps the fast path.
|
||||
Vector<UniformManager::SampledBindingRecord> m_sampledBindingRecordsScratch;
|
||||
// Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's
|
||||
// first sampled-texture loop: the resolved backend resources, so the
|
||||
// post-transition loop can skip re-resolving textures whose layout is
|
||||
// already sampleable.
|
||||
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
|
||||
// Layout VALUE of each sampled resource when the snapshot (and so the cached
|
||||
// sampler descriptors) was built, parallel to m_sampledResourcesScratch. The
|
||||
// fast path's validity check only proves the layout is still sampleable; the
|
||||
// descriptor-reuse hint additionally needs it to be the SAME sampleable
|
||||
// layout (a mid-frame compute dispatch can move a sampled texture from
|
||||
// READ_ONLY_OPTIMAL to GENERAL, both valid, different descriptor).
|
||||
Vector<VkImageLayout> m_sampledLayoutSnapshots;
|
||||
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
||||
Vector<VkBuffer> m_vertexBuffersScratch;
|
||||
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
||||
|
||||
Reference in New Issue
Block a user