// 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 #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 bindings; Vector attributes; Vector bindingBufferKeys; Vector bindingBaseOffsets; Vector bindingAttributeLocations; Vector bindingUsesClientMemory; Vector 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 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(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> 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