mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
Audit of every memoization implementation; sixteen verified defects fixed: DirectGLES backend: - Broadcast draw-buffer memo: cleared at MakeCurrent/DestroyEGLContext like its sibling shadows; its identity+version key is only monotonic within one GLContext, so a library teardown + re-init could false-hit on a recycled FBO address. - Backend texture id re-mint (RecreateBackendTexture) now bumps an attachment generation that the SyncCurrentFBO gate and every FBO twin compare, so driver FBOs re-attach instead of keeping the deleted texture name; the attachment walk re-enters until the generation is quiescent (a walk itself can re-mint). - Buffer id re-mint (persistent-map adoption, immutable-store retire) now bumps a generation the VAO twin sync compares, forcing a full re-emit of the baked glVertexAttribPointer / element-array bindings that frontend versions cannot see. - VAO element-array sync memo: bound-object identity joins the wrapping Uint16 slot version (same pairing the ResolvedDrawBuffers IBO memo already uses). DirectVulkan backend: - EBO slice memo gains the mapped-buffer guard its vertex-binding sibling has: a shadow-backed persistent map mutates with no epoch bump, so a hit must decline. - VkClearManager::MergeClearPayload keeps colorEncoding/colorInt/colorUint with the color, so deferred glClearBufferiv/uiv no longer degrade to all-zero float. - GetOrCreateComputePipeline no longer memoizes a failed creation (same contract as PipelineFactory): a transient driver failure was permanently disabling every dispatch of that program. - Explicit-LOD-0 verdict memo keys on the sampling-resolution generation; sampler filter/aniso/LOD setters bump only that counter, so the old key served a stale verdict (wrong SPIR-V variant) after glTexParameter/glSamplerParameter changes. - SetupDraw fast path declines instead of re-arming on a moved sampling-resolution generation (the snapshot bakes the LOD verdict into its pipeline), and recomputes the XfbCapture bit so the first draw after glBeginTransformFeedback cannot bind the undecorated variant and silently capture nothing. - VertexInputStateFactory eviction epoch is drawn from a process-wide source: VAO state-pointer memos outlive the factory across renderer recreation, and a fresh factory restarting at epoch 1 would dereference a dead factory's entry. - Cached render passes re-read the live renderbuffer clear payload at begin (the clear VALUE is not in the pass hash; the entry's inline snapshot replayed the creation-time color and dropped the newly queued one). - FramebufferObject gains a never-reused lifetime id, keyed into the render-pass fast-path memo and the SetupDraw snapshot beside the raw pointer + Uint16 version pair, which address reuse plus fresh version counts could equal. - SyncTextureResource's preserved-content image goes through the deferred-release ring on both failure paths instead of a synchronous destructor under the GPU. MG_State frontend: - Layer-1 compile memo is env-disciplined like layers 2/3: a node computed against a dead CompileEnv (e.g. pre-capability fallback limits) no longer answers glCompileShader forever once the environment's content changes. - Pipeline composite cache rebuilds from each stage program's last-link shader snapshot (new LinkedShaderRef list + pinned link inputs) instead of the live attach list and current compile nodes: post-link glAttachShader/glCompileShader must not leak into the composite while the (lifetimeId, linkVersion) signature still hits - GL's "as last linked" rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MsqQQF7ugn7MqZXcmnmz1z
142 lines
8.2 KiB
C++
142 lines
8.2 KiB
C++
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h
|
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
|
// Licensed under the GNU Lesser General Public License v3.0:
|
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// End of Source File Header
|
|
|
|
#pragma once
|
|
|
|
#include "Config.h"
|
|
#include "VertexInputStateBuilder.h"
|
|
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
|
#include <Includes.h>
|
|
#include "../VkIncludes.h"
|
|
|
|
namespace MobileGL::MG_Backend::DirectVulkan {
|
|
class VertexInputStateFactory {
|
|
public:
|
|
using HashType = Uint64;
|
|
|
|
enum class VertexStreamConversion : Uint8 {
|
|
None = 0,
|
|
Repack,
|
|
ScaledIntegerToFloat32,
|
|
};
|
|
|
|
struct BackendVertexInputState {
|
|
HashType hash = 0;
|
|
// Hash of the resolved Vulkan vertex layout only (bindings, attributes,
|
|
// unsupported mask) - NO buffer identities. `hash` mixes each bound
|
|
// buffer's never-reused LIFETIME ID, so per-chunk VBOs mint a fresh
|
|
// identity per buffer; keying pipelines on that minted one VkPipeline per
|
|
// chunk section for an identical layout, defeating pipeline reuse and the
|
|
// per-draw memo. Pipelines depend only on the layout, so they key on this
|
|
// instead.
|
|
HashType layoutHash = 0;
|
|
// Frame boundary of the last cache hit; entries idle past the
|
|
// OnFrameBoundary retirement age are evicted (CPU heap only).
|
|
// Mutable: the VAO's state-pointer memo fast path stamps it through
|
|
// a const entry reference.
|
|
mutable Uint64 lastUsedFrameBoundary = 0;
|
|
Vector<VkVertexInputBindingDescription> bindings;
|
|
Vector<VkVertexInputAttributeDescription> attributes;
|
|
Vector<SizeT> bindingBufferKeys;
|
|
Vector<SizeT> bindingBaseOffsets;
|
|
Vector<Uint32> bindingAttributeLocations;
|
|
Vector<Bool> bindingUsesClientMemory;
|
|
Vector<VertexStreamConversion> bindingConversions;
|
|
// Locations whose array is ENABLED but whose GL format has no VkFormat mapping. They are
|
|
// absent from `attributes`, so without this mask the draw path cannot tell them apart from
|
|
// a genuinely disabled array and would silently feed the shader the current attribute value.
|
|
Uint32 unsupportedAttribMask = 0;
|
|
// Bitmask of `attributes[i].location` - the draw path needs it up to
|
|
// three times per draw, so it is baked once at build time.
|
|
Uint32 attributeLocationMask = 0;
|
|
// Per-binding glVertexAttribDivisor values other than 1. Vulkan's instance input
|
|
// rate advances once per instance and nothing else, so anything else has to be
|
|
// stated through VK_EXT_vertex_attribute_divisor. Empty when every instanced
|
|
// binding uses divisor 1, which is what the plain input rate already means.
|
|
Vector<VkVertexInputBindingDivisorDescriptionEXT> bindingDivisors;
|
|
VkPipelineVertexInputDivisorStateCreateInfoEXT divisorState{
|
|
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_DIVISOR_STATE_CREATE_INFO_EXT
|
|
};
|
|
VkPipelineVertexInputStateCreateInfo state{
|
|
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
|
|
};
|
|
};
|
|
|
|
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
|
|
m_config(config), m_physicalDevice(physicalDevice) {}
|
|
~VertexInputStateFactory() = default;
|
|
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
|
|
|
// The VAO aux-memo payload GetOrCreateVertexInputState(vao) stamps: aux0 is the
|
|
// entry's layoutHash, aux1 packs (unsupportedAttribMask << 32) | attributeLocationMask.
|
|
// Readers that find the aux memo valid can use these without resolving the entry.
|
|
static Uint64 PackVertexInputAuxMasks(Uint32 unsupportedAttribMask, Uint32 attributeLocationMask) {
|
|
return (static_cast<Uint64>(unsupportedAttribMask) << 32) | attributeLocationMask;
|
|
}
|
|
|
|
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);
|
|
// Frame boundary hook: ages the cache and evicts entries not hit for many
|
|
// frames. The key mixes each bound buffer's never-reused lifetime id, so
|
|
// buffer/VAO churn keeps minting fresh keys - and does so by construction,
|
|
// not by luck: a recreated buffer can no longer land back on its dead
|
|
// predecessor's key. Without eviction the map grows for the whole session.
|
|
// Entries hold no Vulkan handles (pipeline creation copies the descriptions)
|
|
// and the draw path's entry reference never spans a frame boundary, so
|
|
// eviction here needs no GPU-idle proof. Self-gated: one counter bump and
|
|
// compare except on sweep boundaries.
|
|
void OnFrameBoundary();
|
|
static SizeT GetComponentSize(DataType type);
|
|
// Tightly-packed byte size of one vertex element for this attribute: componentSize * size for
|
|
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
|
|
// an unknown/unsupported type.
|
|
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
|
|
|
|
private:
|
|
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
|
|
Bool isLong = false);
|
|
static Bool IsScaledIntegerVertexFormat(VkFormat format);
|
|
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
|
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
|
|
|
const VulkanRendererConfig& m_config;
|
|
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
|
// Values are heap-allocated: UnorderedMap is open-addressing, so INSERT
|
|
// invalidates references to stored values - and so does ERASE, which shifts
|
|
// the rest of the probe cluster into the hole and therefore moves entries
|
|
// other than the erased one. The draw path (and the VAOs' state-pointer
|
|
// memos) hold entry pointers across both; only the unique_ptr cell moves,
|
|
// never the pointee.
|
|
UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache;
|
|
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
|
Uint64 m_frameBoundaryCounter = 0;
|
|
// Bumped whenever any cache entry is erased. VAOs memo a raw pointer to
|
|
// their heap-allocated entry (stable across map insert/rehash by
|
|
// construction); a memo is honored only while its recorded epoch
|
|
// matches, so an evicted entry can never be dereferenced through a
|
|
// stale memo.
|
|
//
|
|
// Drawn from a process-wide source, never a per-instance counter: the VAO
|
|
// memos outlive this factory (they live on pGLContext's VAOs, the renderer
|
|
// is destroyed and recreated on EGL surface release/re-create), so a fresh
|
|
// factory restarting at a dead factory's epoch value would honor its
|
|
// dangling entry pointers. The constructor takes a value strictly greater
|
|
// than anything a predecessor ever stamped, so a dead factory's memo can
|
|
// never compare equal here - the same never-reused idiom as the lifetime ids.
|
|
// Single-threaded like the rest of the factory (renderer-thread only).
|
|
static inline Uint64 s_evictionEpochSource = 0;
|
|
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
|
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
|
};
|
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|