[Perf] (MG_Backend/DirectVulkan, MG_State): stop re-deriving per-draw state

- Memoize the program content hash on ProgramObject (keyed by the backend
  state version + compile flags; relinking and binding changes invalidate
  it) and the vertex-input hash on VertexArrayObject (keyed by a new
  aggregate config version bumped by every attribute mutation). Full-SPIRV
  XXH64 hashing fell from 13.7% to 1.4% of the render thread.
- ProgramObject also gains a link version and a global-UBO content version
  (bumped by uniform writes and on relink, wrap-safe around the backends'
  "never uploaded" sentinel) for backends to gate uploads and link caches.
- Reuse member scratch vectors in SetupDraw, UploadAndBindVertexBuffers,
  GetOrCreatePipeline and BindProgramUniformBuffers instead of allocating
  per draw (~12% of render-thread time was in the allocator).
- Replace hot-path dynamic_cast with AsMipmapTexture (storage-type tag +
  static_cast); TextureObjectMipmap is the only Mipmap-tagged branch.
- Register/prune texture aliases only when a new (texture, lifetimeId)
  identity appears instead of scanning the entire alive map on every
  sampled-texture sync.
- Make the fallback VkPresentModeKHR log strings report the actual mode.

Vanilla render-thread share of libMobileGL dropped from 48% to 35% on
DirectVulkan (simpleperf, Adreno 830).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 12:12:50 +00:00
co-authored by Claude Fable 5
parent 08e808ef20
commit bd208783d7
15 changed files with 166 additions and 24 deletions
@@ -1666,7 +1666,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const ProgramFactory::VkProgramObject& ProgramFactory::GetOrCreateProgram(
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) {
const HashType hash = ComputeHash(program, flags);
// Hashing the full SPIR-V of every stage is far too expensive to repeat per draw;
// reuse the program's memoized hash while its backend state version is unchanged.
HashType hash = 0;
if (!program.GetBackendHashMemo(flags.GetRaw(), hash)) {
hash = ComputeHash(program, flags);
program.SetBackendHashMemo(flags.GetRaw(), hash);
}
auto it = m_cache.find(hash);
if (it != m_cache.end()) {
return it->second;
@@ -31,8 +31,19 @@ static const char* string_VkColorSpaceKHR(VkColorSpaceKHR) {
return "VkColorSpaceKHR(unknown)";
}
static const char* string_VkPresentModeKHR(VkPresentModeKHR) {
return "VkPresentModeKHR(unknown)";
static const char* string_VkPresentModeKHR(VkPresentModeKHR presentMode) {
switch (presentMode) {
case VK_PRESENT_MODE_IMMEDIATE_KHR:
return "VK_PRESENT_MODE_IMMEDIATE_KHR";
case VK_PRESENT_MODE_MAILBOX_KHR:
return "VK_PRESENT_MODE_MAILBOX_KHR";
case VK_PRESENT_MODE_FIFO_KHR:
return "VK_PRESENT_MODE_FIFO_KHR";
case VK_PRESENT_MODE_FIFO_RELAXED_KHR:
return "VK_PRESENT_MODE_FIFO_RELAXED_KHR";
default:
return "VkPresentModeKHR(unknown)";
}
}
static const char* string_VkSurfaceTransformFlagBitsKHR(VkSurfaceTransformFlagBitsKHR) {
@@ -822,12 +822,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(m_samplerManager != nullptr, "BindProgramUniformBuffers: sampler manager is null");
MOBILEGL_ASSERT(m_bufferManager != nullptr, "BindProgramUniformBuffers: buffer manager is null");
Vector<VkWriteDescriptorSet> writes;
auto& writes = m_writesScratch;
auto& bufferInfos = m_bufferInfosScratch;
auto& imageInfos = m_imageInfosScratch;
auto& texelBufferViews = m_texelBufferViewsScratch;
auto& dynamicOffsets = m_dynamicOffsetsScratch;
writes.clear();
bufferInfos.clear();
imageInfos.clear();
texelBufferViews.clear();
dynamicOffsets.clear();
writes.reserve(m_maxBindings);
Vector<VkDescriptorBufferInfo> bufferInfos;
Vector<VkDescriptorImageInfo> imageInfos;
Vector<VkBufferView> texelBufferViews;
Vector<Uint32> dynamicOffsets;
bufferInfos.reserve(m_maxBindings);
imageInfos.reserve(m_maxBindings);
texelBufferViews.reserve(m_maxBindings);
@@ -113,6 +113,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkTextureManager* m_textureManager = nullptr;
VkSamplerManager* m_samplerManager = nullptr;
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
// Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps
// capacity) so the descriptor-write path stops allocating on every draw.
Vector<VkWriteDescriptorSet> m_writesScratch;
Vector<VkDescriptorBufferInfo> m_bufferInfosScratch;
Vector<VkDescriptorImageInfo> m_imageInfosScratch;
Vector<VkBufferView> m_texelBufferViewsScratch;
Vector<Uint32> m_dynamicOffsetsScratch;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -38,10 +38,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return XXH64_digest(m_hashState);
}
VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash(
const MG_State::GLState::VertexArrayObject& vao) const {
HashType hash = 0;
if (!vao.GetBackendHashMemo(hash)) {
hash = ComputeHash(vao);
vao.SetBackendHashMemo(hash);
}
return hash;
}
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao) {
const HashType hash = ComputeHash(vao);
return GetOrCreateVertexInputState(vao, hash);
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
}
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
@@ -38,6 +38,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
HashType ComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
// is unchanged. Use this on per-draw paths.
HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
const BackendVertexInputState& GetOrCreateVertexInputState(
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
@@ -675,7 +675,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Vector<TextureIdentity> staleAliases;
staleAliases.reserve(m_aliveObjects.size());
for (auto it = m_aliveObjects.begin(); it != m_aliveObjects.end(); ++it) {
if (it->first.texture != texture) {
continue;
@@ -698,12 +697,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto aliveIt = m_aliveObjects.find(identity);
if (aliveIt != m_aliveObjects.end() && aliveIt->second.expired()) {
EraseTrackedTexture(aliveIt->first);
aliveIt = m_aliveObjects.end();
}
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
if (liveTexture && liveTexture.get() == &texture) {
m_aliveObjects[identity] = WeakPtr<MG_State::GLState::ITextureObject>(liveTexture);
PruneStaleTextureAliases(&texture);
// Only (re)register and prune when this (texture, lifetime) pair is new: stale
// aliases can only come into existence through an address reuse, which by
// construction introduces a new identity. Doing this unconditionally made every
// sampled-texture sync scan the entire alive-texture map per draw.
if (aliveIt == m_aliveObjects.end()) {
const auto& liveTexture = MG_State::pGLContext->GetTextureObject(texture.GetExternalIndex());
if (liveTexture && liveTexture.get() == &texture) {
m_aliveObjects[identity] = WeakPtr<MG_State::GLState::ITextureObject>(liveTexture);
PruneStaleTextureAliases(&texture);
}
}
auto it = m_textureResources.find(identity);
@@ -1033,7 +1039,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
auto* mipTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(&texture);
auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
if (!mipTexture) {
MGLOG_D("%s: not TextureObjectMipmap", __func__);
return false;
@@ -1579,7 +1585,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
IntVec3& outTexelSize,
SizeT& outByteSize,
Uint32& outMipLevelCount) {
const auto* mipTexture = dynamic_cast<const MG_State::GLState::TextureObjectMipmap*>(&texture);
const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture);
if (!mipTexture) {
MGLOG_D("%s: not TextureObjectMipmap", __func__);
return false;
@@ -1639,7 +1645,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(mipLevels > 0, "ResolveViewMipRange: mipLevels must be > 0");
Uint32 definedMipLevels = mipLevels;
if (const auto* mipTexture = dynamic_cast<const MG_State::GLState::TextureObjectMipmap*>(&texture)) {
if (const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture)) {
const auto& targets = texture.GetUploadTargets();
for (const auto target : targets) {
const Uint32 uploadMipLevels = GetUploadMipLevelCount(*mipTexture, target);
@@ -2077,8 +2077,10 @@ void main() {
const auto bindingCount = vertexInputState.bindings.size() + static_cast<SizeT>(std::popcount(missingAttribMask));
Vector<VkBuffer> vkBuffers(bindingCount, VK_NULL_HANDLE);
Vector<VkDeviceSize> vkOffsets(bindingCount, 0);
auto& vkBuffers = m_vertexBuffersScratch;
auto& vkOffsets = m_vertexOffsetsScratch;
vkBuffers.assign(bindingCount, VK_NULL_HANDLE);
vkOffsets.assign(bindingCount, 0);
auto findBufferByKey = [&](SizeT bufferKey) -> const MG_State::GLState::BufferObject* {
const auto& attrs = vao.GetAllAttributes();
@@ -2690,6 +2692,7 @@ void main() {
MOBILEGL_ASSERT(offset + size <= m_depthMipmapResources.program->GetUBOSize(),
"GenerateDepthMipmapWithShader: uniform write out of bounds");
memcpy(depthProgramData + offset, data, size);
m_depthMipmapResources.program->MarkUBOContentDirty();
};
for (Uint32 level = baseMipLevel + 1; level < generateMipLevelCount; ++level) {
@@ -2843,12 +2846,13 @@ void main() {
}
#endif
auto vertexInputHash = m_vertexInputStateFactory->ComputeHash(vao);
auto vertexInputHash = m_vertexInputStateFactory->GetOrComputeHash(vao);
auto& vis = m_vertexInputStateFactory->GetOrCreateVertexInputState(vao, vertexInputHash);
const Uint32 vertexInputAttribMask = BuildVertexInputAttributeMask(vis.attributes);
const Uint32 activeAttribMask = programObj.activeVertexInputLocationMask;
const Uint32 missingAttribMask = activeAttribMask & ~vertexInputAttribMask;
Vector<VkVertexInputAttributeDescription> patchedAttributes = vis.attributes;
auto& patchedAttributes = m_patchedAttributesScratch;
patchedAttributes.assign(vis.attributes.begin(), vis.attributes.end());
Bool hasPatchedVertexAttributes = false;
for (auto& attribute : patchedAttributes) {
if (attribute.location >= 32 || (activeAttribMask & (1u << attribute.location)) == 0) {
@@ -3188,7 +3192,7 @@ 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.
Vector<MG_State::GLState::ITextureObject*> sampledTextures;
auto& sampledTextures = m_sampledTexturesScratch; // cleared by CollectSampledTextures
Bool hasSampledTextures = m_uniformManager->CollectSampledTextures(program, programObj, sampledTextures);
MOBILEGL_ASSERT(hasSampledTextures, "%s: CollectSampledTextures failed", __func__);
MGLOG_D("SetupDraw: program=%u drawFbo=%u sampledTextureCount=%zu activeRenderPass=%s",
@@ -3848,6 +3852,7 @@ void main() {
writeUniform(m_blitResources.dstRectLocation, blitUniformData.dstRect, sizeof(blitUniformData.dstRect));
writeUniform(m_blitResources.surfaceTransformLocation, &blitUniformData.surfaceTransform,
sizeof(blitUniformData.surfaceTransform));
m_blitResources.program->MarkUBOContentDirty();
const auto samplerBindingOverride = UniformManager::SamplerBindingOverride{
.binding = m_blitResources.samplerBinding,
@@ -4875,7 +4880,7 @@ void main() {
MOBILEGL_ASSERT(texture != nullptr, "GenerateMipmap requires a bound texture.");
MOBILEGL_ASSERT(texture->IsComplete(), "GenerateMipmap requires a complete texture.");
auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(texture.get());
auto* mipmapTexture = MG_State::GLState::AsMipmapTexture(texture.get());
MOBILEGL_ASSERT(mipmapTexture != nullptr, "GenerateMipmap requires a mipmapped texture object.");
const Uint32 currentMipLevelCount = static_cast<Uint32>(mipmapTexture->GetMipmapLevelCount());
@@ -272,6 +272,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DepthMipmapResources m_depthMipmapResources;
Vector<DeferredDepthMipmapCleanup> m_deferredDepthMipmapCleanup;
// 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;
Vector<VkBuffer> m_vertexBuffersScratch;
Vector<VkDeviceSize> m_vertexOffsetsScratch;
Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
void CreateInstance();
VkResult SetupDebugMessenger();
VkResult DestroyDebugMessenger();
@@ -105,6 +105,14 @@ namespace {
namespace MobileGL::MG_State::GLState {
void ProgramObject::ResetLinkArtifacts() {
// Relinking regenerates the SPIR-V, so any backend-cached state keyed on
// m_backendStateVersion (e.g. the content-hash memo) must be invalidated,
// along with every link-derived backend cache (m_linkVersion) and the
// last-uploaded-UBO gate (a relink resets uniforms to their initial values,
// and that reset must reach the GPU).
++m_backendStateVersion;
++m_linkVersion;
MarkUBOContentDirty();
m_program.reset();
m_generatedSpirv.clear();
m_uniformLocations.clear();
@@ -170,7 +170,33 @@ namespace MobileGL::MG_State::GLState {
void* MapUBO() { return m_globalUboScratch.data(); }
const void* GetUBOData() const { return m_globalUboScratch.data(); }
Uint GetUBOSize() const { return static_cast<Uint>(m_globalUboScratch.size()); }
// Content version of the CPU-side global-UBO shadow: writers bump it so backends
// can skip re-uploading an unchanged UBO on every draw. ~0u is reserved as the
// backends' "never uploaded" sentinel, so skip over it on wrap.
Uint32 GetUBOContentVersion() const { return m_uboContentVersion; }
void MarkUBOContentDirty() {
if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0;
}
Uint32 GetBackendStateVersion() const { return m_backendStateVersion; }
// Bumped only by (re)linking — lets backends detect that every piece of
// link-derived reflection (locations, block order, UBO layout) is stale.
Uint32 GetLinkVersion() const { return m_linkVersion; }
// Content-hash memo for backends: avoids re-hashing the generated SPIR-V on every
// draw. The memo is keyed by (backendStateVersion, flags); ResetLinkArtifacts and
// the binding setters below invalidate it by bumping m_backendStateVersion.
Bool GetBackendHashMemo(Uint flags, Uint64& outHash) const {
if (m_backendHashMemoVersion != m_backendStateVersion || m_backendHashMemoFlags != flags) {
return false;
}
outHash = m_backendHashMemo;
return true;
}
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
m_backendHashMemo = hash;
m_backendHashMemoVersion = m_backendStateVersion;
m_backendHashMemoFlags = flags;
}
void SetUniformSamplerOrImageUnitIndex(Uint location, Int unit) {
if (location >= m_uniformSamplerOrImageUnitIndex.size() ||
@@ -308,5 +334,13 @@ namespace MobileGL::MG_State::GLState {
Bool m_linkStatus = false;
Bool m_validateStatus = true;
Uint32 m_backendStateVersion = 0;
// Backend-owned content-hash memo (see GetBackendHashMemo): valid only while
// m_backendStateVersion and the compile flags match the recorded values.
mutable Uint64 m_backendHashMemo = 0;
mutable Uint32 m_backendHashMemoVersion = ~0u;
mutable Uint m_backendHashMemoFlags = 0;
Uint32 m_uboContentVersion = 0;
Uint32 m_linkVersion = 0;
};
} // namespace MobileGL::MG_State::GLState
@@ -128,6 +128,20 @@ namespace MobileGL::MG_State::GLState {
virtual Bool IsStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) const = 0;
};
// Cheap replacement for dynamic_cast on the hot path: TextureObjectMipmap is the
// only hierarchy branch whose storage type reports Mipmap, so the tag check makes
// the static_cast safe.
inline TextureObjectMipmap* AsMipmapTexture(ITextureObject* texture) {
return (texture && texture->GetStorageType() == TextureStorageType::Mipmap)
? static_cast<TextureObjectMipmap*>(texture)
: nullptr;
}
inline const TextureObjectMipmap* AsMipmapTexture(const ITextureObject* texture) {
return (texture && texture->GetStorageType() == TextureStorageType::Mipmap)
? static_cast<const TextureObjectMipmap*>(texture)
: nullptr;
}
class TextureObjectWithOneMipmap : public TextureObjectMipmap {
public:
TextureObjectWithOneMipmap(TextureTarget target, Uint externalIndex)
@@ -207,16 +207,19 @@ namespace MobileGL::MG_State::GLState {
void VertexArrayObject::BumpAttributeFormatVersion(Uint index) {
if (index >= MAX_VERTEX_ATTRIBS) return;
++m_attributeVersions[index].FormatVersion;
++m_configVersion;
}
void VertexArrayObject::BumpAttributeBufferVersion(Uint index) {
if (index >= MAX_VERTEX_ATTRIBS) return;
++m_attributeVersions[index].BufferVersion;
++m_configVersion;
}
void VertexArrayObject::BumpAttributeSwitchVersion(Uint index) {
if (index >= MAX_VERTEX_ATTRIBS) return;
++m_attributeVersions[index].SwitchVersion;
++m_configVersion;
}
const VertexAttributeVersion& VertexArrayObject::GetAttributeVersion(Uint index) const {
@@ -81,6 +81,23 @@ namespace MobileGL {
const VertexAttributeVersion& GetAttributeVersion(Uint index) const;
const Array<VertexAttributeVersion, MAX_VERTEX_ATTRIBS>& GetAllAttributeVersions() const;
// Aggregate of every per-attribute version bump; lets backends detect
// "any vertex-input state changed" with one compare.
Uint32 GetConfigVersion() const { return m_configVersion; }
// Backend-owned content-hash memo, valid while the config version matches
// (same idea as ProgramObject's hash memo — avoids re-hashing all
// attributes on every draw).
Bool GetBackendHashMemo(Uint64& outHash) const {
if (m_backendHashMemoVersion != m_configVersion) return false;
outHash = m_backendHashMemo;
return true;
}
void SetBackendHashMemo(Uint64 hash) const {
m_backendHashMemo = hash;
m_backendHashMemoVersion = m_configVersion;
}
private:
void BumpAttributeFormatVersion(Uint index);
void BumpAttributeBufferVersion(Uint index);
@@ -100,6 +117,10 @@ namespace MobileGL {
// ARB_vertex_attrib_binding API; only such attributes are re-resolved, so the
// classic glVertexAttribPointer path keeps its exact historical behavior.
Array<Bool, MAX_VERTEX_ATTRIBS> m_attributeUsesBindingModel = {};
Uint32 m_configVersion = 0;
mutable Uint64 m_backendHashMemo = 0;
mutable Uint32 m_backendHashMemoVersion = ~0u;
};
} // namespace GLState
} // namespace MG_State
+2
View File
@@ -336,6 +336,8 @@ namespace MobileGL {
operator Bool() const { return Any(); }
typename Underlying::type GetRaw() const { return flags; }
private:
Bool Any() const { return static_cast<typename Underlying::type>(flags) != 0; }