// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/UniformDescriptorBinder.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 "ProgramFactory.h" #include "VkBufferManager.h" #include "VkSamplerManager.h" #include "VkTextureManager.h" #include "../VkIncludes.h" #include namespace MobileGL::MG_State::GLState { class ITextureObject; class ProgramObject; class SamplerObject; } namespace MobileGL::MG_Backend::DirectVulkan { class UniformManager { public: struct SamplerBindingOverride { Uint32 binding = 0; Uint32 element = 0; MG_State::GLState::ITextureObject* texture = nullptr; const MG_State::GLState::SamplerObject* sampler = nullptr; VkImageView imageView = VK_NULL_HANDLE; VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED; Bool forceNearestFiltering = false; }; struct SamplerImageFeedbackBinding { Uint32 samplerBinding = 0; Uint32 samplerElement = 0; MG_State::GLState::ITextureObject* texture = nullptr; const MG_State::GLState::SamplerObject* sampler = nullptr; SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown; }; // `physicalDevice` is only ever asked for format properties: a placeholder descriptor for // an unbound texel-buffer binding has to be built from a format the DEVICE accepts as a // texel buffer, and there is no other route to that answer from here. Bool Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkBufferManager* bufferManager, ProgramFactory* programFactory, VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount, Uint32 maxBindings = 16, Uint32 setsPerFrame = 64, VkTextureManager* textureManager = nullptr, VkSamplerManager* samplerManager = nullptr); void Shutdown(); void BeginFrame(Uint32 frameIndex); // A command buffer (re)began recording: descriptor bindings recorded into // the previous buffer do not carry over, so drop the bind-dedup shadow. void OnCommandBufferBoundary() { m_lastBindValid = false; } // A ProgramFactory eviction just destroyed this layout: purge every frame // slot's cached descriptor sets for it, so a recycled handle value can never // stale-hit sets written for the dead layout's bindings. The sets are // vkFreeDescriptorSets'd back to their pools (created with // FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program // churn recycles pool capacity instead of abandoning it. GPU-safe: the layout // only dies after >1024 idle frame boundaries, so no in-flight command buffer // references its sets. This is the only eviction path for the per-layout // caches - a live layout's entry must never be purged (its sets would be // unreachable pool slots), so there is deliberately no age-based sweep here. void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout); // One record per visited CombinedImageSampler DESCRIPTOR (post fallback substitution, // in binding order, and within a binding in array-element order): the resolved texture // and effective sampler, as never-reused lifetime ids so a freed-and-reallocated object // at the same heap address can only MISS a comparison, never false-hit it (same ABA // rule as SamplerResolveMemo). An arrayed binding contributes one record per element - // element granularity is required, or swapping the textures of two elements of the same // array would leave the record list identical and the fast path would keep a stale set. struct SampledBindingRecord { Uint64 textureLifetimeId = 0; Uint64 samplerLifetimeId = 0; }; Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Vector& outTextures, Vector* outBindingRecords = nullptr); // Shadow-compare for the SetupDraw fast path: re-runs the CollectSampledTextures // walk and reports whether every visited binding still resolves to the recorded // (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a // redundant glBindSampler, which always bumps it) does not prove the sampled set // moved; this walk does, without rebuilding the set or falling off the fast path. Bool SampledBindingsUnchanged(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, const Vector& previousRecords) const; Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Vector& outTextures) const; Bool CollectSamplerImageFeedback( const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Vector& outBindings) const; static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel, GLint imageLevel, GLenum imageAccess); // samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that // every input of every combined-image-sampler resolution is unchanged since the // previous draw's resolve - same (texture, sampler) per binding, texture params // sum, sampling-resolution generation (sampler params + texture shape), image // epochs AND per-resource layout values - so the per-binding cached // VkDescriptorImageInfo may be reused without re-running the resolve chain. Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 frameIndex, VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, const SamplerBindingOverride* samplerBindingOverride = nullptr, Bool samplerDescriptorsUnchangedHint = false, const Vector* samplerBindingOverrides = nullptr); // Pure format-policy helper kept public for host regression tests. Formatted storage // images use their shader qualifier; transformed float images use glBindImageTexture's // format and never silently fall back to the backing image format. static VkFormat ResolveStorageImageViewFormat(VkFormat reflectedFormat, GLenum bindingFormat, VkFormat resourceFormat, Bool useBindingFormat); // True when the program reads at least one sampler and every one of them is bound to a // texture whose GL level range is a single level. Such a sampler resolves to // minLod = maxLod = 0 (see VkSamplerManager::GetOrCreateSampler), so an implicit-LOD sample // and an explicit LOD 0 sample must read the same texel - which is what makes the // ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads // only GL state, so a texture that ends up single-level for another reason (one uploaded // level under a wide level range) merely misses the rewrite. static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj); private: struct DescriptorPoolBucket { VkDescriptorPool handle = VK_NULL_HANDLE; Uint32 maxSets = 0; Uint32 allocatedSets = 0; Bool updateAfterBind = false; }; // A cached descriptor set together with the pool it was allocated from, so a // layout-destroyed purge can vkFreeDescriptorSets it back and credit the // owning bucket's accounting. struct CachedDescriptorSet { VkDescriptorSet set = VK_NULL_HANDLE; VkDescriptorPool pool = VK_NULL_HANDLE; }; struct DescriptorSetCacheEntry { Vector sets; Uint32 cursor = 0; }; struct FrameResources { Vector descriptorPools; UnorderedMap descriptorSetCacheByLayout; Vector texelBufferViews; Uint32 activeDescriptorPoolIndex = 0; Uint32 allocatedSetsThisFrame = 0; Uint32 peakAllocatedSetsThisFrame = 0; }; static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, SharedPtr& outTexture); // Shared per-binding resolution for CollectSampledTextures and // SampledBindingsUnchanged, so membership and comparison can never diverge: // texture after the fallback substitution (may still be null when no fallback // exists), effective sampler = unit override else the texture's own sampler. // False = the binding is skipped (unbound with a non-2D fallback target). // `element` indexes a sampler array inside the binding; see ResolveSamplerDescriptor. Bool ResolveSampledBinding(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, MG_State::GLState::ITextureObject*& outTexture, const MG_State::GLState::SamplerObject*& outSampler) const; // Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures): // the bound texture stays alive through the draw via GL binding state, so callers that // only need the pointer skip the SharedPtr copy's atomic refcount churn. static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw( const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element); // `numericDomain` is the sampler's class, and it matters only for the multisample arm - // see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it. SharedPtr GetFallbackTexture( TextureTarget target, SamplerNumericDomain numericDomain) const; // The multisample arm of GetFallbackTexture. One object per (target, numeric domain) and // no upload path: a multisample image cannot be written by a transfer, so its texels stay // undefined - which is what GL promises for a texelFetch on an incomplete multisample // texture - and it cannot carry MUTABLE_FORMAT, so its format has to match the sampler's // class outright rather than being reinterpreted at view time. SharedPtr GetFallbackMultisampleTexture( TextureTarget target, SamplerNumericDomain numericDomain) const; // ---- placeholders for UNBOUND image-backed descriptors ------------------------- // GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing // to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete // buffer texture, 8.26 for an image unit with no texture) - undefined VALUES, not a // dropped draw. Vulkan has no unwritten descriptor, so something valid has to sit in the // set or the whole draw or dispatch is lost, which is what these two build. Same shape as // VkBufferManager::AcquireUnboundStorageDescriptor, one level up: per FORMAT rather than // one shared object, because a descriptor whose format disagrees with the shader's // declaration is invalid Vulkan even when nothing ever reads it. // // `declaredFormat` is the format the SHADER declared (VK_FORMAT_UNDEFINED for a sampled // texel buffer, which never carries one, or for a formatless `writeonly` image); // `numericDomain` decides the format when there is no declaration and is the fallback // class when the device cannot use the declared one as a texel buffer. VkBufferView AcquireUnboundTexelBufferView(VkFormat declaredFormat, SamplerNumericDomain numericDomain, Bool storage); // A 1x1 (x1 layer, or 6 faces for a cube) texture of `format`, shaped for `target` so the // view the descriptor gets has the view type the shader's image declaration demands. // Null for a target with no single-sampled placeholder shape - multisample images, whose // descriptor needs a multisample view that this cannot stand in for. SharedPtr GetUnboundStorageImageTexture(TextureTarget target, VkFormat format) const; // The (target, format) pair a storage-image binding's placeholder is keyed by, resolved // from reflection alone. False when the binding has no placeholder shape. Bool ResolveUnboundStorageImagePlaceholder(const ProgramFactory::VkProgramObject& programObj, Uint32 binding, TextureTarget& outTarget, VkFormat& outFormat) const; // `element` indexes a sampler ARRAY inside one binding; each element carries its own // independently assigned GL texture unit, so it selects the texture, the sampler // override and the fallback separately from its neighbours. // // trustUnchangedHint: reuse this binding's cached VkDescriptorImageInfo outright // (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof // obligations the caller carries). The cache is keyed by binding alone, so it is // used ONLY for single-descriptor bindings - see m_samplerResolveMemo. Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, VkDescriptorImageInfo& outImageInfo, Bool trustUnchangedHint = false) const; Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride, VkDescriptorImageInfo& outImageInfo) const; Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex, VkBufferView& outBufferView); // GLSL `imageBuffer`: the same VkBufferView descriptor as the sampled texel buffer above, // but resolved from an IMAGE unit (glBindImageTexture) rather than a texture unit, and // made GPU-resident-writable because the shader may store to it. No `element` parameter: // an imageBuffer ARRAY is refused at program creation, so a binding is always one // descriptor (see the array gate in RemapDescriptorBindingsForVulkan). Bool ResolveStorageTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 frameIndex, VkBufferView& outBufferView); // `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary // block. Each element resolves through its own GL storage block, and so its own GL // binding point, buffer and glBindBufferRange window. Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, VkDescriptorBufferInfo& outBufferInfo) const; // `element` indexes an image ARRAY inside one binding; each element carries its own // independently assigned GL image unit. Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element, VkDescriptorImageInfo& outImageInfo) const; // Result of resolving a UBO binding: either a zero-copy direct bind to the app's resident // VkBuffer (the GLES backend's approach - no per-draw copy) or the CPU payload to upload. struct UboBindResult { Bool directBindable = false; VkBuffer buffer = VK_NULL_HANDLE; VkDeviceSize range = 0; // reflected block size; constant across draws (hashed) VkDeviceSize dynamicOffset = 0; // block range start; moves per draw (NOT hashed) const void* payload = nullptr; // fallback UploadTransient path VkDeviceSize payloadSize = 0; }; Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 arrayElement, UboBindResult& out) const; // Shared resolution of one dynamic-UBO binding element into the // (buffer, range, dynamicOffset) triple the descriptor consumes: direct // bind, global-slice reuse, or transient upload. Used by the full walk // and by the dynamic-offset-only rebind (see FastRebindMemo). Bool ResolveDynamicUboDescriptor(const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 arrayElement, Uint32 frameIndex, VkBuffer& outBuffer, VkDeviceSize& outRange, Uint32& outDynamicOffset); // The vkCmdBindDescriptorSets tail shared by the full walk and the // dynamic-offset-only rebind: skips the driver call when this exact // binding is already live on the command buffer (see the bind-dedup // shadow below), otherwise binds and refreshes the shadow. void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint, VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet, const Vector& dynamicOffsets); Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const; Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind); VkResult AllocateDescriptorSetsFromActivePool( Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet); VkResult AcquireDescriptorSet(Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet); VkDevice m_device = VK_NULL_HANDLE; VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE; VkBufferManager* m_bufferManager = nullptr; ProgramFactory* m_programFactory = nullptr; Vector m_frames; VkDeviceSize m_minDynamicOffsetAlignment = 1; Uint32 m_frameCount = 0; Uint32 m_maxBindings = 0; Uint32 m_setsPerFrame = 0; Uint32 m_peakDescriptorSetsObserved = 0; VkTextureManager* m_textureManager = nullptr; VkSamplerManager* m_samplerManager = nullptr; mutable SharedPtr m_fallbackTexture2D; // Keyed by (arrayed, numeric domain); see GetFallbackMultisampleTexture. Lazily populated, // never evicted - at most six tiny 1x1 images - and torn down with the manager. mutable UnorderedMap> m_fallbackMultisampleTextures; // See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily // populated, never evicted (a program's declared formats are a fixed, tiny set) and torn // down with the manager. The texel views are keyed by format AND by storage-vs-sampled // because the two descriptor kinds demand different format FEATURES of the device, so one // format can be usable for one and not the other. Deliberately NOT the per-frame // texelBufferViews list: those are destroyed at every frame boundary, and these must // outlive it or the placeholder would be rebuilt for every unbound binding every frame. UnorderedMap m_unboundTexelBufferViews; mutable UnorderedMap> m_unboundStorageImageTextures; // Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps // capacity) so the descriptor-write path stops allocating on every draw. Vector m_writesScratch; Vector m_bufferInfosScratch; Vector m_imageInfosScratch; Vector m_texelBufferViewsScratch; Vector m_dynamicOffsetsScratch; // Descriptor-set reuse across recent draws (see BindProgramUniformBuffers). // When a draw's resolved descriptor content is byte-identical to one memoized // earlier, reuse that VkDescriptorSet and skip AcquireDescriptorSet + // vkUpdateDescriptorSets - only the bind-time dynamic offsets differ. Four // entries with round-robin replacement rather than one: draws alternating // between two programs (MC's chunk<->entity ping-pong) would thrash a single // slot into a full re-allocate+write every draw. Reset each frame in BeginFrame // because the frame's descriptor sets are recycled there. struct DescriptorReuseEntry { Uint64 signature = 0; VkDescriptorSet set = VK_NULL_HANDLE; Bool valid = false; }; static constexpr Uint32 kDescriptorReuseMemoSize = 4; DescriptorReuseEntry m_descriptorReuseMemo[kDescriptorReuseMemoSize]; Uint32 m_descriptorReuseMemoNext = 0; // Dynamic-offset-only rebind (see BindProgramUniformBuffers): records the // descriptor set selected by the last cacheable full walk of a program // whose active bindings are exactly one dynamic UBO (single descriptor) // plus combined-image samplers. When the next call proves every sampler // descriptor input unchanged (samplerDescriptorsUnchangedHint) and the // UBO re-resolves to the SAME VkBuffer+range - only the dynamic offset // moved, the per-draw glUniform case - the walk collapses to: resolve one // offset, rebind the recorded set with new pDynamicOffsets (Vulkan allows // rebinding the same set with different dynamic offsets). // Invalidation inventory: BeginFrame clears it (the frame's sets are // recycled) and the frameIndex field guards cross-frame confusion on top; // OnDescriptorSetLayoutDestroyed clears it (the set may be freed); a // sampler-override walk clears it (mirrors m_descriptorReuseMemo); a // program relink bumps the backend state version and thus programObj.hash // so the key misses; the program lifetime id is never reused, so a // deleted-and-recreated program misses; a texture/sampler/binding change // drops the hint upstream; an arena wrap or growth resolves a different // VkBuffer and misses. AcquireDescriptorSet's per-frame cursor only // advances, so the recorded set is never re-written within its frame. struct FastRebindMemo { Bool valid = false; Uint32 frameIndex = 0; Uint64 programLifetimeId = 0; ProgramFactory::HashType programHash = 0; Uint32 uboBinding = 0; VkBuffer uboBuffer = VK_NULL_HANDLE; VkDeviceSize uboRange = 0; VkDescriptorSet set = VK_NULL_HANDLE; }; FastRebindMemo m_fastRebindMemo; // vkCmdBindDescriptorSets dedup: consecutive draws with a static uniform // block resolve to the same set AND the same dynamic offsets, so the // driver call can be skipped outright. Command-buffer-scope state; reset // via OnCommandBufferBoundary whenever a recording (re)begins. Keyed on // layout+bind point, so a pipeline-layout switch always rebinds. static constexpr Uint32 kMaxShadowedDynamicOffsets = 8; Bool m_lastBindValid = false; VkDescriptorSet m_lastBindSet = VK_NULL_HANDLE; VkPipelineLayout m_lastBindLayout = VK_NULL_HANDLE; VkPipelineBindPoint m_lastBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; Uint32 m_lastBindOffsetCount = 0; Uint32 m_lastBindOffsets[kMaxShadowedDynamicOffsets] = {}; // Global-UBO transient-slice reuse: MC leaves the default uniform block // untouched across long GUI/terrain runs, so the per-draw re-upload of // the same bytes can reuse the slice uploaded earlier THIS frame (frame // serial guards arena recycling; the content version guards writes). struct GlobalUboSliceMemo { Uint64 programLifetimeId = 0; Uint64 frameSerial = 0; Uint32 uboContentVersion = 0; VkBuffer buffer = VK_NULL_HANDLE; VkDeviceSize offset = 0; VkDeviceSize range = 0; }; static constexpr Uint32 kGlobalUboMemoSize = 4; GlobalUboSliceMemo m_globalUboMemo[kGlobalUboMemoSize]; Uint32 m_globalUboMemoNext = 0; // Per-binding fast path over VkSamplerManager's content-hashed sampler cache, which // stays the source of truth: its key hashes all sampler+texture state, so two distinct // sampler objects with identical state still resolve to one VkSampler. This memo only // skips recomputing that hash. Across a draw batch the bound sampler set is stable, so a // binding whose sampler (lifetime id + version, bumped on every setter) and texture // (lifetime id + params version, bumped on the format/border-color setters that feed the // key) are unchanged recycles the VkSampler it resolved last draw; a param change bumps // a version and forces a re-resolve. Both objects are keyed by a never-reused monotonic // lifetime id, so a freed-and-reallocated sampler or texture at the same heap address // always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a // stale guess can only miss and fall through to the hash, never resolve wrong. Still // reset each frame alongside the descriptor-set cache. Indexed by binding, but the // whole-descriptor entry is additionally keyed by program lifetime: Vulkan binding // numbers are layout-local and unrelated programs routinely reuse binding 0/1. struct SamplerResolveMemo { Uint64 infoProgramLifetimeId = 0; Uint64 samplerLifetimeId = 0; Uint64 textureLifetimeId = 0; VkSampler sampler = VK_NULL_HANDLE; Uint32 viewLevelCount = 0; Uint16 samplerVersion = 0; Uint16 textureParamsVersion = 0; Bool forceNearestFiltering = false; Bool valid = false; // ResolveSampledImageViewFormat is pure in (image format, numeric domain), but a // domain mismatch walks a ~184-entry format table. Memo the resolution per binding // so a reinterpreted sampler pays that scan once, not once per draw. VkFormat viewFormatSource = VK_FORMAT_UNDEFINED; SamplerNumericDomain viewFormatDomain = SamplerNumericDomain::Unknown; VkFormat viewFormat = VK_FORMAT_UNDEFINED; Bool viewFormatValid = false; // Whole resolved descriptor from this binding's last full resolve. Reused // ONLY under ResolveSamplerDescriptor's trustUnchangedHint, whose caller // proves every resolve input unchanged; cleared with the per-frame reset // (the cached VkSampler outlives a frame only via a fresh resolve, which // also re-stamps it against VkSamplerManager's frame-boundary sweep). // // This one field is keyed by binding but describes ONE descriptor, so it is // written and read only for single-descriptor bindings. A sampler ARRAY's // elements share the binding and would overwrite each other here - the last // element resolved would then be handed to element 0 on the next hinted draw. // Every other field above is self-validating (each compares its full key // before reuse, and the view-format entry is a pure function of format and // numeric domain), so an arrayed binding may keep using those. VkDescriptorImageInfo info{}; Bool infoValid = false; }; mutable Vector m_samplerResolveMemo; // Exclusive upper bound on the entries of m_samplerResolveMemo that any resolve // has ever written. The vector is sized to the DEVICE binding cap (256 on desktop // NVIDIA), but a program declares 1-8 bindings, so the per-frame reset below was // memsetting ~22 KB of never-touched entries every frame - a measurable slice of // the per-frame fixed cost on draw-light frames. Every site that can turn any of // an entry's *Valid flags on raises this mark first, so entries at or above it are // provably still in their constructed (all-invalid) state and clearing them is a // no-op. Never lowered except by Initialize/Shutdown, which rebuild the vector. mutable Uint32 m_samplerResolveMemoHighWater = 0; void NoteSamplerResolveMemoTouched(Uint32 binding) const { if (binding >= m_samplerResolveMemoHighWater) { m_samplerResolveMemoHighWater = binding + 1; } } }; } // namespace MobileGL::MG_Backend::DirectVulkan