[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
@@ -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