mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
- MagmaPipeIdentityTable was a FIXED 2048/8192-entry, 2-way set-associative LRU. Above capacity it evicted LIVE objects, and every memo keyed on the handle died with them: a verbatim transcription of the previous Acquire lost 54% of uses' handles at 2500 live VAOs against 2048 entries, and 20% at 1024 live VAOs once the lifetime ids are sparse (an app that creates and destroys VAOs - the Minecraft chunk shape this exists for). - Two of the three memos it fed had NO capacity before this package: the content-hash memo and the resolved-state memo were unbounded mutable fields on VertexArrayObject. Eviction there turns one ComputeHash per VAO reconfiguration into one per DRAW; once the buffer table thrashes too, the vertex-input content hash becomes a per-draw value that inserts a fresh heap-allocated BackendVertexInputState into an unbounded map on every draw, swept only every 256 frame boundaries. That is a worse leak than the one the fixed table was introduced to avoid. - So the mint grows on demand and reclaims by AGE: a lifetime-id map with a one-entry front memo, a free list, and an OnFrameBoundary sweep on the same cadence and retirement age as the cache entries those slots key. Footprint tracks the live DRAWN working set instead of objects ever created, which is the property MG_Impl/Pipe/SlotAllocator cannot have here (nothing in P2 can call its Free). Re-run of the same workloads: handle churn is 0.0% at 512, 1024, 2048, 2500, 3000, 4000, 8192, 10000 and 16000 live objects, consecutive and sparse ids alike, at 1 and 5 acquisitions per use. - VertexInputStateFactory::m_vaoMemos follows the mint with no capacity of its own, through a chunked table whose entry addresses never move - which is what the fixed table's only real guarantee was, and D12.4's grow-on-demand ask without a relocating Vector. - VulkanRenderer::m_vaoDrawMemoTable deliberately keeps the base ref's 2048 entries and the base ref's older-frameSerial victim rule, and changes only its KEY. It is the one memo of the three that had a capacity before P2, a VaoDrawMemo is ~450 B, and losing one costs one vertex-binding re-resolve. Measured steady-state miss rate against the base ref's address-hashed table: 0.0% vs 6.4% at 512 live VAOs, 0.0% vs 24.0% at 1024, 0.0% vs 60.0% at 2048, 36.2% vs 69.5% at 2500, 63.5% vs 79.1% at 3000; both are ~100% at 4096 (2x capacity), where an LRU on a cyclic pattern cannot win. - The two tables are now a MagmaPipeIdentityTables member of VulkanRenderer, handed to its VertexInputStateFactory, instead of two function-local statics that outlived every context and shared one reclamation clock across two. - A Gen that reaches 2^32-1 retires its slot for good rather than wrapping. MOBILEGL_ASSERT is compiled out of every build P2 runs, and a DEBUG-level build of this tree does not compile at all (MG_Util/Types.h uses MOBILEGL_ASSERT before MGLOG_F is declared - untouched since the base ref, and not this package's file), so the defence has to be on the release path to exist. - The no-CSO pipeline-memo fallback stops using MGLOG_W_ONCE. MOBILEGL_LOG_ONCE_INTERNAL is an unconditional std::atomic_flag::test_and_set - a locked xchg per evaluation, not "one static bool test" - and this site is on the per-draw path in exactly the configuration that reaches it. A plain per-renderer bool replaces it, and the comment now says what the warning's absence does and does not prove (nothing at all while bit 0 is clear). - MOBILEGL_PIPE_LEGACY_MEMOS=0 with kMGPipeSubsystemRenderState clear still runs the pre-handle state hash - there is a correct answer there and bit 0 is not Track H, so it is not fatal - but it is no longer silent: the startup gate names the combination. - The D12.3 static_assert block now names D19's DynamicChunksCoverMagmasDynamicTailKey, whose ctest entry lives in package A's file, so the integrator can see which half is missing.
213 lines
12 KiB
C++
213 lines
12 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
|
|
// MG_Pipe::MGPipeHandle for the P2 D12.5 memo table below. A header of constexpr constants,
|
|
// so the pull build gains nothing from it.
|
|
#include <MG_Pipe/MGPipeHandles.h>
|
|
|
|
#include "Config.h"
|
|
#include "MagmaPipeArms.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,
|
|
// GL_DOUBLE source data narrowed to a tightly packed float32 stream: the fetch half
|
|
// of the fp64 demotion the shader side already does unconditionally.
|
|
Float64ToFloat32,
|
|
};
|
|
|
|
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
|
|
};
|
|
};
|
|
|
|
#if MOBILEGL_PIPE_PUSH
|
|
// The mint is the RENDERER's (MagmaPipeIdentityTables), not a process-global and not
|
|
// this factory's: VulkanRenderer::LookupVaoDrawMemo has to derive the same {slot, gen}
|
|
// for the same VAO, and a table that outlived the context it was minted for would share
|
|
// one reclamation clock across two live contexts (review v2 minor 4).
|
|
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice,
|
|
MagmaPipeIdentityTables& identity):
|
|
m_config(config), m_physicalDevice(physicalDevice), m_identity(&identity) {}
|
|
#else
|
|
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
|
|
m_config(config), m_physicalDevice(physicalDevice) {}
|
|
#endif
|
|
~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;
|
|
#if MOBILEGL_PIPE_PUSH
|
|
// The VAO's content hash IF it has already been memoized, without computing one.
|
|
// P2 D12.5: the three draw-path readers that used to ask the VAO object this
|
|
// question ask the factory instead, because that is where the memo lives once the
|
|
// frontend object stops carrying the backend's state.
|
|
Bool TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) const;
|
|
#endif
|
|
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;
|
|
|
|
#if MOBILEGL_PIPE_PUSH
|
|
// ---- P2 D12.5: the backend's memos, off the frontend VAO and into the backend ----
|
|
//
|
|
// The two facts that used to live as `mutable` fields on VertexArrayObject
|
|
// (Get/SetBackendHashMemo and Get/SetBackendStateMemo), kept here instead, keyed on
|
|
// the VAO's {slot, gen} and guarded by exactly the same config version. A frontend
|
|
// state object holding the backend's raw pointer is what P2 retires: under split the
|
|
// backend is in another process and its cache entry has no address a client could
|
|
// store, so the memo has to live on the side that owns the pointee.
|
|
//
|
|
// The AUX memo is not carried over: its two words moved into VaoDrawMemo::layoutHash
|
|
// and layoutAuxMasks long ago and its getter has no live reader anywhere in the tree,
|
|
// so the handle arm simply stops writing it (D12.5 says delete rather than move).
|
|
struct VaoBackendMemos {
|
|
// Whose memos these are. The identity table can recycle a slot for a different
|
|
// VAO under LRU pressure, and the handle compare - Gen included - is what says
|
|
// the contents are this object's and not its predecessor's.
|
|
MG_Pipe::MGPipeHandle Owner = MG_Pipe::kMGPipeNullHandle;
|
|
Uint64 Hash = 0;
|
|
Uint32 HashConfigVersion = ~0u;
|
|
const void* State = nullptr;
|
|
Uint64 StateEpoch = 0;
|
|
Uint32 StateConfigVersion = ~0u;
|
|
};
|
|
// Grow-on-demand (D12.4), one entry per slot the renderer's mint has ever handed
|
|
// out, and NO CAPACITY: these two memos had none before this package either - they
|
|
// were unbounded mutable fields on the VertexArrayObject itself - and re-introducing
|
|
// eviction here is what review v2 rejected. MagmaPipeSlotTable grows in chunks so an
|
|
// entry reference stays valid across the nested GetOrCreateVertexInputState call.
|
|
// 48 B per live VAO, reclaimed with the slot when the object goes idle.
|
|
mutable MagmaPipeSlotTable<VaoBackendMemos> m_vaoMemos;
|
|
// The renderer's {slot, gen} mint (see the constructor). Never null under push.
|
|
MagmaPipeIdentityTables* m_identity = nullptr;
|
|
// The entry belonging to `vao`, claimed (and cleared) if the slot currently holds
|
|
// someone else's.
|
|
VaoBackendMemos& MemosFor(const MG_State::GLState::VertexArrayObject& vao) const;
|
|
#endif
|
|
|
|
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 the frontend context'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).
|
|
//
|
|
// P2 D12.5: the process-wide source is the LEGACY arm's need. It exists because the
|
|
// memos live on the frontend VAOs and therefore outlive the factory. The handle arm's
|
|
// memo table is owned by this factory and dies with it, so a per-instance counter is
|
|
// enough there and the epoch shrinks back to what it looks like it should be.
|
|
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
|
static inline Uint64 s_evictionEpochSource = 0;
|
|
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
|
|
#else
|
|
Uint64 m_evictionEpoch = 1;
|
|
#endif
|
|
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
|
};
|
|
} // namespace MobileGL::MG_Backend::DirectVulkan
|