// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.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 "FrameContext.h"
#include "PipelineFactory.h"
#include "ProgramFactory.h"
#include "SwapchainObject.h"
#include "UniformManager.h"
#include "VertexInputStateFactory.h"
#include "VkBufferObject.h"
#include "VkBufferManager.h"
#include "VkClearManager.h"
#include "VkRenderPassManager.h"
#include "VkSamplerManager.h"
#include "VkTextureManager.h"
#include "VkTimerQueryManager.h"
#include "MG_Util/Math/VectorTypes.h"
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include <vk_mem_alloc.h>

#include "../VkIncludes.h"

namespace MobileGL::MG_State::GLState {
    class FramebufferObject;
    class ProgramObject;
    class SamplerObject;
    class VertexArrayObject;
} // namespace MobileGL::MG_State::GLState

namespace MobileGL::MG_Backend::DirectVulkan {
    enum class DrawSetupAspect: Uint8 {
        FramebufferObject  = 1 << 0,
        VertexArrayObject  = 1 << 1,
        UniformBuffer      = 1 << 2,
        VertexBuffer       = 1 << 3,
        IndexBuffer        = 1 << 4,
        IndirectDrawBuffer = 1 << 5,
        Viewport           = 1 << 6,
        Scissor            = 1 << 7,
    };

    struct DrawCmdParam {
        Uint32 vertexCount = 0;
        Uint32 instanceCount = 1;
        Uint32 firstVertex = 0;
        Uint32 firstInstance = 0;
        // Indexed-draw metadata for bounding vertex-stream conversion. baseVertex is the
        // draw's base-vertex offset; indexRangeIsExactView is true only when the draw
        // fetches exactly the indices its IndexBufferView describes (direct DrawElements;
        // multi/indirect forms leave it false because the CPU cannot bound their ranges).
        Int32 baseVertex = 0;
        Bool indexRangeIsExactView = false;
    };

    struct DrawIndexedCmdParam {
        Uint32 indexCount = 0;
        Uint32 instanceCount = 1;
        Uint32 firstIndex = 0;
        Int32 vertexOffset = 0;
        Int32 firstInstance = 0;
    };

    struct DrawCmd {
        GLenum mode = GL_TRIANGLES;
        DrawCmdParam params;
    };

    struct IndexBufferView {
        GLenum indexType = GL_UNSIGNED_SHORT;
        SizeT indexByteOffset = 0;
        SizeT indexByteSize = 0;
        // Interpret indexByteOffset as a raw client pointer even when an element
        // array buffer is bound (backend-synthesized index lists, e.g. the
        // GL_LINE_LOOP -> LINE_STRIP rewrite).
        Bool forceClientMemory = false;
    };

    struct DrawIndexedCmd {
        GLenum mode = GL_TRIANGLES;
        IndexBufferView indexBufferView;

        DrawIndexedCmdParam params;
    };

    struct MultiDrawIndexedCmd {
        GLenum mode = GL_TRIANGLES;
        IndexBufferView indexBufferView;

        Uint32 drawCount = 0;
        DrawIndexedCmdParam* pParams = nullptr;
    };

    struct MultiDrawCmd {
        GLenum mode = GL_TRIANGLES;
        Uint32 drawCount = 0;
        DrawCmdParam* pParams = nullptr;
    };

    struct QueueFamilyIndices {
        Int32 graphicsFamily = -1;
        Int32 presentFamily = -1;
    };

    struct PhysicalDevice {
        QueueFamilyIndices queueFamilies;
        VkPhysicalDeviceProperties properties;
        VkPhysicalDevice handle = VK_NULL_HANDLE;

        Bool IsComplete() const {
            return handle != VK_NULL_HANDLE && queueFamilies.graphicsFamily != -1 && queueFamilies.presentFamily != -1;
        }
    };

    class VulkanRenderer : public IBufferCopyCommandProvider,
                           public FrameContext::IRecordingObserver,
                           public VkRenderPassManager::IEvictionObserver,
                           public ProgramFactory::IEvictionObserver {
    public:
        VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg = {});
        ~VulkanRenderer();

        void Initialize();
        void Shutdown();

        // IBufferCopyCommandProvider: recording command buffer, outside any
        // render pass, for immediate staged buffer copies.
        VkCommandBuffer AcquireBufferCopyCommandBuffer() override;

        // FrameContext::IRecordingObserver: prepares the frame's timer-query
        // pool (harvest + reset) right after the frame command buffer begins
        // recording, before any render pass.
        void OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) override;

        // VkRenderPassManager::IEvictionObserver: the render-pass aging sweep just
        // destroyed these VkRenderPasses; evict every graphics pipeline hashed on a
        // dying handle (they share its >1024-boundary idleness, so immediate
        // destruction is safe) and drop the last-pipeline memo if any went.
        void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) override;

        // ProgramFactory::IEvictionObserver: an aged-out program entry was
        // destroyed; evict its compute pipeline and graphics pipelines (same
        // idleness guarantee - they are only bound through draws/dispatches that
        // stamp the program entry) and purge the descriptor-set cache entries
        // keyed by its now-recyclable VkDescriptorSetLayout handle.
        void OnProgramEvicted(ProgramFactory::HashType programHash,
                              VkDescriptorSetLayout descriptorSetLayout) override;

        Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
                       const DrawCmdParam& drawParams,
                       const IndexBufferView* pIndexBufferView = nullptr);
        // ANGLE-style consecutive-draw fast path: SetupDraw snapshots the fully
        // resolved draw configuration; the next draw whose cheap version/identity
        // checks all match skips the resolution half (LOD probe, sampled-set
        // walk, render-pass and pipeline resolution) and jumps straight to the
        // per-draw tail. Returns false (leaving no side effects that the full
        // path cannot redo idempotently) whenever anything might have changed.
        Bool TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
                                  const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView);
        void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer,
                                                const RenderPassEntry& compatibleRenderPassEntry);

        enum class ScissoredClearPrep {
            NotNeeded,  // scissor covers the whole target — take the deferred whole-surface path instead
            NoOp,       // nothing to clear (degenerate target or empty scissor rect)
            Ready,      // a render pass is active; record vkCmdClearAttachments with the returned rect
        };
        ScissoredClearPrep PrepareScissoredClear(const MG_State::GLState::FramebufferObject& framebuffer,
                                                 VkClearRect& outClearRect);

        void Clear(GLbitfield mask);
        void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
        void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
        void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
        void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
        void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
                                     GLenum buffer, GLint drawbuffer, const GLfloat* value);
        void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
                                     GLenum buffer, GLint drawbuffer, const GLint* value);
        void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
                                      GLenum buffer, GLint drawbuffer, const GLuint* value);
        void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
                                     GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
        void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
                             GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
                             GLbitfield mask, GLenum filter);
        void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFbo,
                                  const SharedPtr<MG_State::GLState::FramebufferObject>& drawFbo,
                                  GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
                                  GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
                                  GLbitfield mask, GLenum filter);
        void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset,
                       GLint x, GLint y, GLsizei width, GLsizei height);
        void CopyImageSubData(const CopyImageEndpoint& srcEndpoint,
                              GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
                              const CopyImageEndpoint& dstEndpoint,
                              GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
                              GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
        void GenerateMipmap(GLenum target);
        void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
        // GL_DEPTH_COMPONENT / GL_DEPTH_STENCIL / GL_STENCIL_INDEX readback from the
        // read framebuffer's depth/stencil attachment (per-aspect buffer copies with
        // CPU repacking into the requested client layout).
        void ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, GLsizei width,
                                    GLsizei height, GLenum format, GLenum type, void* pixels);
        // Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
        // expects command recording to be active and any render pass already ended.
        //
        // `defaultFramebufferOrientation` is set only when the source is the swapchain's
        // depth/stencil image, which this renderer stores display-side-up: the copy rect then
        // has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on
        // the way back, exactly as the colour ReadPixels path does.
        // `sourceLayerCount` above 1 says the `height` rows the client is owed are stored as that
        // many ARRAY LAYERS of a one-row image rather than as rows of one layer - the shape a GL
        // 1D array has in Vulkan. The two produce byte-identical tightly-packed readbacks, so
        // only the copy region differs; everything after it is written against `height`.
        void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
                                           VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
                                           GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type,
                                           void* pixels, Bool defaultFramebufferOrientation = false,
                                           Uint32 sourceLayerCount = 1);
        // Same-extent depth blit between images of different depth formats: host
        // round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
        Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
                                    VkImageLayout* srcTrackedLayout, Uint32 srcMipLevel, Uint32 srcBaseArrayLayer,
                                    VkImage dstImage, VkFormat dstFormat, VkImageLayout* dstTrackedLayout,
                                    Uint32 dstMipLevel, Uint32 dstBaseArrayLayer, GLint srcX, GLint srcY, GLint dstX,
                                    GLint dstY, GLint width, GLint height, VkImageLayout srcRestoreLayout,
                                    VkImageLayout dstRestoreLayout, Bool stencilAspect);
        static SizeT GetReadbackTexelSize(VkFormat sourceFormat);
        // Map a GL bottom-left-origin rectangle into the display-oriented swapchain image.
        // Quarter-turn surface transforms swap the copy extent's axes.
        static Bool MapDefaultFramebufferReadbackRect(GLint x, GLint y, GLsizei width, GLsizei height,
                                                      VkExtent2D imageExtent,
                                                      VkSurfaceTransformFlagBitsKHR preTransform,
                                                      VkOffset2D* imageOffset, VkExtent2D* imageCopyExtent);
        // Reorder a tightly packed block copied with MapDefaultFramebufferReadbackRect back into
        // GL row order. The input block has swapped dimensions for 90/270 degree transforms.
        static Bool RemapDefaultFramebufferReadback(const Uint8* rawPixels, Uint32 logicalWidth,
                                                    Uint32 logicalHeight,
                                                    VkSurfaceTransformFlagBitsKHR preTransform,
                                                    SizeT texelSize, Uint8* outPixels);
        static Bool ConvertReadbackPixels(const Uint8* sourcePixels, VkFormat sourceFormat,
                                          GLsizei width, GLsizei height, GLenum destinationFormat,
                                          GLenum destinationType, SizeT destinationRowStride,
                                          Uint8* destinationPixels);
        void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
        void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
                             TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
                             GLsizei bufSize, GLvoid* pixels);
        void DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ);
        void DispatchComputeIndirect(GLintptr indirect);
        void MemoryBarrier(GLbitfield barriers);
        static VkMemoryBarrier BuildMemoryBarrierForGlBarriers(GLbitfield barriers);
        void DrawArrays(const DrawCmd& payload);
        void DrawElements(const DrawIndexedCmd& payload);
        void MultiDrawArrays(const MultiDrawCmd& payload);
        void MultiDrawElements(const MultiDrawIndexedCmd& payloads);
        void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount,
                                       GLsizei stride);
        void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
        void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
                                            GLsizei maxdrawcount, GLsizei stride);
        void Present();

        const PhysicalDevice& GetPhysicalDevice() const;
        VkInstance GetInstance() const;
        Bool IsDrawIndirectCountExtensionEnabled() const;

        // GL fence support, expressed in queue-submission indices backed by
        // real VkFences. A GL fence captures GetSyncPointSubmitIndex() at
        // creation: the index of the submission that will carry the commands
        // recorded so far (m_submitCounter + 1 while work is pending, or
        // m_submitCounter when nothing has been recorded since the last
        // submit). It is signaled once that submission's fence is observed
        // signaled - unlike the frame-serial heuristic, this makes fences
        // signal as soon as the GPU actually finishes, which MC 1.21.5's
        // fence-paced ring buffers rely on to recycle their space.
        Uint64 GetSyncPointSubmitIndex() const;
        // Non-blocking: polls outstanding submission fences and reports
        // whether every submission up to `submitIndex` has completed.
        Bool IsSubmitIndexComplete(Uint64 submitIndex);
        // Submits the commands recorded so far without waiting (GL flush).
        // Recording restarts lazily on a fresh command buffer; the submitted
        // one is retired until the frame slot's fence is next waited. Returns
        // true when a submission was made.
        Bool FlushPendingCommands();
        // Flush gated on usefulness: only flushes when `submitIndex` is still
        // unsubmitted, so poll loops on already-submitted fences do not split
        // the frame's render pass (a full tile load/store on TBDR GPUs).
        Bool FlushForSyncPoint(Uint64 submitIndex);
        // Blocking wait for a submission index with a nanosecond timeout.
        // When the index is still unsubmitted and flushIfPending is set, the
        // pending commands are flushed first so the wait can make progress.
        Bool WaitForSubmitIndex(Uint64 submitIndex, Uint64 timeoutNs, Bool flushIfPending);

        // Frame-serial completion, still used by the timer-query paths (their
        // records are bucketed per frame slot).
        Bool IsFrameSerialComplete(Uint64 serial) const;
        // Blocking wait for a submitted serial. Returns false when the serial
        // cannot complete without further submissions (it belongs to the
        // current, not-yet-presented frame) or when the wait failed.
        Bool WaitForFrameSerial(Uint64 serial, Uint64 timeoutNs);

        // GPU timer queries, backing the GL_TIME_ELAPSED / GL_TIMESTAMP
        // frontend. Timestamp support (queue timestampValidBits > 0 and a
        // non-zero timestampPeriod) is cached at device creation.
        Bool IsTimerQuerySupported() const;
        // The samplerAnisotropy device feature was granted, so GL_TEXTURE_MAX_ANISOTROPY_EXT is
        // honored rather than accepted-and-ignored.
        Bool IsSamplerAnisotropySupported() const { return m_samplerAnisotropyFeatureEnabled; }
        // ARB_base_instance extends indirect command records with a non-zero firstInstance and
        // requires gl_InstanceID to remain zero-based. Vulkan needs both features to honor that
        // complete contract: one legalizes the command word, the other enables the shader rebase.
        Bool IsNonZeroIndirectBaseInstanceSupported() const {
            return m_drawIndirectFirstInstanceFeatureEnabled && m_shaderDrawParametersFeatureEnabled;
        }
        // Ensures the frame command buffer is recording (same lazy pattern as
        // SetupDraw) and writes a bottom-of-pipe timestamp into the current
        // frame's pool. Null when unsupported or the pool is exhausted.
        SharedPtr<VkTimerQueryManager::TimestampRecord> WriteTimerQueryTimestamp();
        // Non-blocking: true once the record's raw ticks are on the CPU
        // (harvests the slot once its frame serial has completed).
        Bool IsTimerQueryResultReady(VkTimerQueryManager::TimestampRecord& record);
        // Blocking wait, mirroring ClientWaitSync's caveat: a record written
        // this frame cannot complete until Present submits the commands, so
        // this returns false (result reads as 0) instead of deadlocking.
        Bool WaitForTimerQueryResult(VkTimerQueryManager::TimestampRecord& record);
        Uint64 GetTimerQueryElapsedNs(const VkTimerQueryManager::TimestampRecord& begin,
                                      const VkTimerQueryManager::TimestampRecord& end) const;
        Uint64 GetTimerQueryTimestampNs(const VkTimerQueryManager::TimestampRecord& record) const;

        // GL_SAMPLES_PASSED occlusion queries: every app draw between Start and Stop is
        // wrapped in a Vulkan occlusion query slot; the result is the slot sum. Requires
        // hostQueryReset for slot recycling - Start fails (frontend keeps the query
        // unsupported) when the device lacks it.
        Bool StartOcclusionQueryCapture();
        void StopOcclusionQueryCapture(Vector<Uint32>& outSlots);
        // Flushes pending commands, waits, sums the slots, and recycles them.
        Bool ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples);

        void RequestSwapchainResize(Uint32 width, Uint32 height);
        // Re-query the surface and report whether the live swapchain no longer matches it
        // (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
        // rebuild, so a surface the driver merely considers suboptimal cannot thrash.
        Bool SwapchainIsOutOfDate();
        // Returns false when the surface is zero-area (minimized/hidden window):
        // no new swapchain is installed and presentation must stay suspended.
        Bool RecreateSwapchain();

    private:
        // Tiered emission for an already-set-up multi-draw batch (state bound, index
        // buffer bound for the indexed form). Tier 1: VK_EXT_multi_draw. Tier 2: one
        // vkCmdDraw(Indexed)Indirect over a transient command array. Tier 3: unrolled
        // vkCmdDraw(Indexed) loop. Tier eligibility is per-batch (uniform instance
        // state for tier 1, firstInstance/feature legality for tier 2); every tier
        // consumes the same param span, so contiguous-run merging done by the caller
        // benefits all of them.
        void EmitMultiDrawIndexed(VkCommandBuffer commandBuffer, const DrawIndexedCmdParam* pParams, Uint32 drawCount);
        void EmitMultiDraw(VkCommandBuffer commandBuffer, const DrawCmdParam* pParams, Uint32 drawCount);

        struct BlitUniformData {
            float srcRect[4] = {0.f, 0.f, 1.f, 1.f};
            float dstRect[4] = {0.f, 0.f, 1.f, 1.f};
            Int surfaceTransform = 0;
            Int padding[3] = {0, 0, 0};
        };

        struct BlitResources {
            SharedPtr<MG_State::GLState::ProgramObject> program;
            SharedPtr<MG_State::GLState::SamplerObject> nearestSampler;
            SharedPtr<MG_State::GLState::SamplerObject> linearSampler;
            Int srcRectLocation = -1;
            Int dstRectLocation = -1;
            Int surfaceTransformLocation = -1;
            Uint32 samplerBinding = 0;
        };

        struct DepthMipmapResources {
            SharedPtr<MG_State::GLState::ProgramObject> program;
            Int srcRectLocation = -1;
            Int dstRectLocation = -1;
            Int surfaceTransformLocation = -1;
            Int srcTexelSizeLocation = -1;
            Uint32 samplerBinding = 0;
        };

        // A single-sample staging image for multisample-resolve blits that also have to change
        // orientation. vkCmdResolveImage cannot flip (it takes one offset per side, not the
        // invertible pair vkCmdBlitImage takes), so a resolve into or out of the default
        // framebuffer used to land the mirrored band. Resolving here first and then blitting from
        // here separates the two operations, and each one then does only what it can express.
        //
        // Pooled rather than created per blit: the CTS runs hundreds of these back to back, and
        // create-destroy per call would both cost allocations and, worse, need per-call deferred
        // destruction to outlive the recording. It grows to the largest extent asked for and is
        // reused; format changes recreate it.
        struct MultisampleResolveScratchImage {
            VkImage image = VK_NULL_HANDLE;
            VmaAllocation allocation = VK_NULL_HANDLE;
            VkFormat format = VK_FORMAT_UNDEFINED;
            VkExtent2D extent = {0, 0};
            VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
        };
        MultisampleResolveScratchImage m_msResolveScratch;
        // Returns a scratch image at least `extent` in size with exactly `format`, transitioned to
        // TRANSFER_DST and ready to be resolved into. Null image on failure (the caller then falls
        // back to the direct resolve).
        Bool AcquireMultisampleResolveScratchImage(VkCommandBuffer commandBuffer, VkFormat format,
                                                   VkExtent2D extent);
        void DestroyMultisampleResolveScratchImage();

        struct DeferredDepthMipmapCleanup {
            Vector<VkImageView> imageViews;
            Vector<VkFramebuffer> framebuffers;
            Vector<VkRenderPass> renderPasses;
            Vector<VkPipeline> pipelines;
        };

        void QueueClearBufferPayload(GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload);
        void QueueClearBufferPayloadForFramebuffer(const MG_State::GLState::FramebufferObject& framebuffer,
                                                  GLenum buffer, GLint drawbuffer,
                                                  const ClearAttachmentPayload& clearPayload);
        void RecordScissoredClearBuffer(const MG_State::GLState::FramebufferObject& framebuffer,
                                        GLenum buffer, GLint drawbuffer,
                                        const ClearAttachmentPayload& clearPayload,
                                        const VkClearRect& clearRect);

        // ---- Submission fence tracking (GL sync objects) ----
        // One record per vkQueueSubmit still in flight, in ascending submit
        // order. Present/readback submissions reference the frame slot's
        // fence (not pool-owned); mid-frame flushes use pooled fences that are
        // recycled once their submission is observed complete.
        // Not thread-safe: like the rest of the renderer, the tracker relies
        // on GL calls being serialized (launchers migrate the context across
        // threads, but calls never run concurrently), so sync-object polls
        // may mutate it without locking.
        struct SubmitRecord {
            Uint64 submitIndex = 0;
            // Buffer-manager frame serial the submission was made under; its
            // completion raises the completed-serial floor (timer queries and
            // buffer busy-tracking live in frame-serial space).
            Uint64 frameSerial = 0;
            VkFence fence = VK_NULL_HANDLE;
            Bool pooledFence = false;
        };
        // Registers a submission that vkQueueSubmit just made with `fence`.
        // Invariant: every graphics-queue submission that outlives its call
        // site must be registered so GL fences observe it. Exempt are the
        // texture-upload/preserve submits in VkTextureManager, which
        // vkWaitForFences inline before returning.
        void RegisterSubmit(VkFence fence, Bool pooledFence);
        // Builds the submit packet for the frame's pending command buffer
        // (consuming the acquire semaphore on the slot's first submission),
        // submits it with `fence`, and registers the submission. On failure
        // the frame state is left untouched. Shared by the mid-frame flush
        // and the readback path so the semaphore-consumption invariant lives
        // in one place.
        Bool SubmitPendingCommandBuffer(FrameContext::FrameData& frame, VkFence fence, Bool pooledFence);
        // Polls in-flight submission fences (prefix order) and advances the
        // completed counter past every fence observed signaled.
        void RefreshCompletedSubmits();
        // All submissions up to `submitIndex` are known complete (their fence
        // was waited or the device was idled); drops their records and
        // recycles pooled fences.
        void OnSubmitsCompletedUpTo(Uint64 submitIndex);
        VkFence AcquirePooledSubmitFence();
        void DestroySubmitFencePool();
        Bool HasPendingRecordedWork() const;
        // Frame-boundary housekeeping for paths that never reach Present's
        // tail (present-less readback loops, suspended presentation, blocking
        // sync waits): runs the same per-frame drains Present performs, but
        // only when every queue submission has been observed complete AND no
        // recorded-but-unsubmitted commands exist - i.e. when CPU-GPU overlap
        // is provably already zero. Never blocks (non-blocking fence poll
        // only), so the presenting path's frames-in-flight pipelining is
        // untouched. Returns true when the drain ran.
        Bool TryDrainFrameTransients();

        Vector<SubmitRecord> m_inFlightSubmits;
        Vector<VkFence> m_freeSubmitFences;
        Uint64 m_submitCounter = 0;
        Uint64 m_completedSubmitCounter = 0;
        // Drains since the last Present, gating the drain's frame-boundary-equivalent
        // work (arena rewind + cache aging): a presenting app's mid-frame
        // readbacks/waits must neither churn the transient caches nor accelerate the
        // aging clocks, while present-less loops still cross a boundary every few
        // iterations. Reset in Present.
        Uint32 m_drainsSinceLastPresent = 0;

        NativeWindowType m_window = 0;
        void* m_platformDisplay = nullptr;
        void* m_platformLibrary = nullptr;
        void* m_platformCloseDisplay = nullptr;
        // Whether the loader exposes VK_EXT_headless_surface, detected once in
        // CreateInstance() from the enumerated instance extensions. On desktop an
        // offscreen surface REQUIRES it: false is a clean, loud bring-up failure, never
        // a substituted window. (Android is the one exception and has its own path -
        // no Mali/Adreno driver seen so far exposes the extension, so a windowless
        // context is given an AImageReader ANativeWindow that is never displayed.)
        Bool m_headlessSurfaceSupported = true;
        // Android has the same shortfall: no Mali/Adreno driver seen so far exposes
        // VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an
        // AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever
        // displayed - the reader's images are simply never acquired. Owned here, so
        // Shutdown() deletes it.
        void* m_fallbackImageReader = nullptr;
        VulkanRendererConfig m_config;
        Bool m_swapchainResizeRequested = false;
        // Presentation is suspended while the window is zero-area (minimized): the
        // swapchain is unusable/out of date, so Present drops frames instead of
        // submitting on a signaled fence / presenting never-acquired images.
        Bool m_presentSuspended = false;

        // Vulkan objects
        Bool m_validationLayersEnabled = false;
        Vector<VkExtensionProperties> m_extensions;
        VkInstance m_instance = VK_NULL_HANDLE;
        VkDebugUtilsMessengerEXT m_debugMessenger = VK_NULL_HANDLE;
        // Fallback reporting channel for drivers that ship the validation layers but
        // only expose the older VK_EXT_debug_report (Adreno 650 / Vulkan 1.1.128).
        VkDebugReportCallbackEXT m_debugReportCallback = VK_NULL_HANDLE;
        PhysicalDevice m_physicalDevice;
        VkDevice m_device = VK_NULL_HANDLE;
        VmaAllocator m_allocator = nullptr;
        VkSurfaceKHR m_surface = VK_NULL_HANDLE;
        SwapchainObject m_swapchainObject;

        VkQueue m_graphicsQueue = VK_NULL_HANDLE;
        VkQueue m_presentQueue = VK_NULL_HANDLE;
        Bool m_drawIndirectCountExtensionEnabled = false;
        Bool m_indexTypeUint8ExtensionEnabled = false;
        Bool m_logicOpFeatureEnabled = false;
        Bool m_multiDrawIndirectFeatureEnabled = false;
        // drawIndirectFirstInstance gates indirect commands whose firstInstance != 0;
        // cached at device creation because the tier-2 multi-draw path (a transient
        // VkDrawIndexedIndirectCommand array) is illegal for such a sub-draw without it.
        Bool m_drawIndirectFirstInstanceFeatureEnabled = false;
        // VK_EXT_multi_draw: native batched submission for the CPU-side glMultiDraw*
        // families (tier 1 of the multi-draw dispatch).
        Bool m_multiDrawExtensionEnabled = false;
        Uint32 m_maxMultiDrawCount = 0;
        // Multi-draw dispatch tiers, resolved once at device creation from device support
        // clamped by MOBILEGL_MAGMA_MULTIDRAW_MODE (a preference, never a demand):
        //   tier 1 (ext):      one vkCmdDrawMulti(Indexed)EXT           - m_multiDrawAllowExt
        //   tier 2 (indirect): one vkCmdDraw(Indexed)Indirect batch     - m_multiDrawAllowIndirect
        //   tier 3 (unroll):   one vkCmdDraw(Indexed) per sub-draw      - always available
        // m_multiDrawForceUnrollIndirect additionally forces the GPU-parameter
        // glMultiDraw*Indirect paths onto their per-command loop (mode=unroll only).
        Bool m_multiDrawAllowExt = false;
        Bool m_multiDrawAllowIndirect = false;
        Bool m_multiDrawForceUnrollIndirect = false;
        Bool m_samplerAnisotropyFeatureEnabled = false;
        Bool m_shaderDrawParametersExtensionEnabled = false;
        Bool m_shaderDrawParametersFeatureEnabled = false;
        // Native subgroup topology, queried at device creation for the compute-module
        // subgroup repairs (SubgroupSupportPolicy.h) and the REQUIRE_FULL_SUBGROUPS
        // stage flag; 0 / false when the device has no usable compute subgroups or
        // MOBILEGL_DISABLE_SUBGROUP forced them off.
        Uint32 m_nativeSubgroupSize = 0;
        Bool m_nativeSubgroupSupported = false;
        Bool m_computeFullSubgroupsFeatureEnabled = false;
        // VkPhysicalDeviceSubgroupSizeControlProperties::maxComputeWorkgroupSubgroups;
        // 0 when the extension (and therefore the full-subgroups flag) is unavailable.
        Uint32 m_maxComputeWorkgroupSubgroups = 0;
        Bool m_unformattedFloatStorageImagesEnabled = false;
        // Set only after descriptor-indexing feature AND property queries prove that
        // update-after-bind is legal for every descriptor category this renderer emits.
        ProgramFactory::UpdateAfterBindLimits m_updateAfterBindLimits{};
        // fillModeNonSolid gates VK_POLYGON_MODE_LINE/_POINT (glPolygonMode); independentBlend gates
        // per-draw-buffer color write masks (glColorMaski). Both are cached at device creation and
        // drive a runtime fallback when the device lacks them.
        Bool m_fillModeNonSolidFeatureEnabled = false;
        Bool m_independentBlendFeatureEnabled = false;
        // dualSrcBlend gates GL_SRC1_* blend factors (glBindFragDataLocationIndexed dual-source blend);
        // primitiveTopologyListRestart gates primitive restart on *list* topologies (strip/fan restart
        // needs no feature). Both cached at device creation and drive a hard-fail-at-draw when absent.
        Bool m_dualSrcBlendFeatureEnabled = false;
        Bool m_primitiveTopologyListRestartFeatureEnabled = false;
        // shaderTessellationAndGeometryPointSize gates the PointSize built-in in a tessellation
        // or geometry stage, which desktop GL treats as an ordinary per-vertex output (writable,
        // and capturable by name through transform feedback). Cached at device creation and
        // handed to ProgramFactory, which refuses a program whose tessellation or geometry module
        // declares the matching SPIR-V capability while this is false - SetupDraw then skips its
        // draws (VkProgramObject::pointSizeCapabilityUnsupported) rather than building a pipeline
        // that is invalid usage.
        Bool m_tessellationAndGeometryPointSizeFeatureEnabled = false;
        // VK_EXT_custom_border_color. Vulkan's four predefined VkBorderColor values cover only
        // transparent/opaque black and opaque white; GL_TEXTURE_BORDER_COLOR is an arbitrary vec4 (or
        // an arbitrary ivec4/uvec4 through the "I" entry points). Without this extension a border
        // colour outside the palette has to be snapped to the nearest predefined one. Both features
        // are required together: customBorderColorWithoutFormat is what lets a sampler carry a custom
        // colour without naming the image format it will be paired with, which GL's sampler objects
        // cannot know. maxCustomBorderColorSamplers is a real device limit, so the sampler cache has
        // to be able to fall back to the snapped value once it is reached.
        Bool m_customBorderColorFeatureEnabled = false;
        Uint32 m_maxCustomBorderColorSamplers = 0;
        // sampleRateShading gates VkPipelineMultisampleStateCreateInfo::sampleShadingEnable, i.e.
        // glEnable(GL_SAMPLE_SHADING) + glMinSampleShading. Unlike dualSrcBlend this does NOT
        // hard-fail the draw when absent: sample shading is a rate hint, and every sample-rate
        // pipeline is still correct (just not per-sample) at the default rate - so the enable is
        // dropped and the draw proceeds, which is what a GL implementation with SAMPLES=1 does too.
        Bool m_sampleRateShadingFeatureEnabled = false;
        // multiViewport gates rasterizing into more than one of ARB_viewport_array's 16 viewports
        // (gl_ViewportIndex). m_maxRasterizableViewports is min(MAX_VIEWPORTS, device limit), or 1
        // when the feature is off, and is the viewportCount a gl_ViewportIndex-writing pipeline
        // declares - it is NOT what GL_MAX_VIEWPORTS reports, which is the frontend state width.
        Bool m_multiViewportFeatureEnabled = false;
        Uint32 m_maxRasterizableViewports = 1;
        // Union of shader stages sampled-read barriers may name; built at device creation
        // because geometry/tessellation stage bits are invalid in a barrier when their
        // feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and
        // ALL_GRAPHICS would also serialize against non-shader stages.
        VkPipelineStageFlags m_sampledReadStageMask = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
                                                      VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
                                                      VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
        // Cached at device creation from the graphics queue family properties
        // and device limits; drives timer-query support.
        Uint32 m_timestampValidBits = 0;
        Float m_timestampPeriodNs = 0.0f;
        Bool m_timerQuerySupported = false;
        using PFNDrawIndexedIndirectCountFunc = void(VKAPI_PTR*)(VkCommandBuffer commandBuffer, VkBuffer buffer,
                                                                 VkDeviceSize offset, VkBuffer countBuffer,
                                                                 VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
                                                                 Uint32 stride);
        static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
        // VK_EXT_multi_draw entry points, loaded at device creation when the extension
        // (and its multiDraw feature) is enabled; null otherwise.
        static inline PFN_vkCmdDrawMultiEXT s_vkCmdDrawMultiEXT = nullptr;
        static inline PFN_vkCmdDrawMultiIndexedEXT s_vkCmdDrawMultiIndexedEXT = nullptr;

        // VK_EXT_transform_feedback (GL transform feedback capture)
        Bool m_transformFeedbackFeatureEnabled = false;
        // VK_EXT_provoking_vertex. Vulkan's built-in convention is "provoking vertex first"; GL's
        // default is LAST_VERTEX_CONVENTION, and GL derives BOTH flat shading and the transform
        // feedback vertex order from it. provokingVertexLast alone fixes flat shading and the
        // input-assembler capture order and has no dependency on transform feedback; only
        // transformFeedbackPreservesProvokingVertex does.
        Bool m_provokingVertexLastEnabled = false;
        // transformFeedbackPreservesProvokingVertex was actually enabled at device creation. Kept
        // separate because it is the only thing that arms
        // VUID-VkGraphicsPipelineCreateInfo-topology-04884, the rule that forbids a TRIANGLE_FAN
        // pipeline from asking for LAST on a device that cannot preserve a fan's provoking vertex.
        Bool m_provokingVertexXfbPreserveEnabled = false;
        // provokingVertexModePerPipeline: when VK_FALSE every pipeline in one render pass instance
        // must agree on the mode, so glProvokingVertex(GL_FIRST_VERTEX_CONVENTION) cannot be honoured
        // per draw and every pipeline takes GL's default (LAST) instead.
        Bool m_provokingVertexModePerPipeline = false;
        // transformFeedbackPreservesTriangleFanProvokingVertex.
        Bool m_provokingVertexFanPreserved = false;
        // Per-pipeline provoking-vertex mode. capturesXfbFromGeometryStage must be a LINK-TIME
        // property of the program, never the dynamic "is transform feedback active" flag: the
        // 8-entry m_pipelineMemo and the SetupDrawSnapshot fast path key on programObj.hash and
        // the pipeline-state value hash, neither of which moves when glBeginTransformFeedback is
        // called, so a dynamic input here would hand back a stale VkPipeline.
        VkProvokingVertexModeEXT SelectProvokingVertexMode(VkPrimitiveTopology topology,
                                                          Bool capturesXfbFromGeometryStage) const;
        // VK_EXT_vertex_attribute_divisor: without it every non-zero glVertexAttribDivisor
        // behaves as 1, because that is all Vulkan's instance input rate can express.
        Bool m_vertexAttributeDivisorEnabled = false;
        static inline PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr;
        static inline PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
        static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
        // Counter buffers (one 4-byte slot per capture binding) let consecutive
        // draws within one glBeginTransformFeedback append GL-style. Transform feedback
        // objects can each hold an open, paused span at the same time, so the counters are
        // per object: one group of four slots each, handed out on first use.
        static constexpr SizeT kXfbCounterObjectSlots = 16;
        VkBufferObject m_xfbCounterBuffer;
        UnorderedMap<Uint, Uint32> m_xfbCounterSlotByObject;
        Uint32 m_xfbNextCounterSlot = 0;
        // Set for a slot once a captured draw has been recorded into its span; selects
        // counter-buffer resume on the next captured draw of the same span.
        Array<Bool, kXfbCounterObjectSlots> m_xfbCountersValid{};
        Array<Uint64, kXfbCounterObjectSlots> m_xfbLastSeenGeneration{};
        // Counter slot group of the bound transform feedback object.
        Uint32 CurrentXfbCounterSlot();
        // Wraps a recorded draw with BeginTransformFeedbackEXT/EndTransformFeedbackEXT
        // when GL transform feedback is active; binds capture buffers on demand.
        Bool BeginXfbCaptureForDraw(FrameContext::FrameData& frame);
        void EndXfbCaptureForDraw(FrameContext::FrameData& frame, Bool began);
        // Makes the captured bytes visible to whatever reads them next. Deferred rather than
        // recorded next to the capture, because the capturing draw runs inside a render pass
        // that declares no self-dependency.
        void MakeXfbWritesVisible();
        Bool m_xfbWritesPendingVisibility = false;
        // Wrap one app draw in an occlusion-query slot while a GL_SAMPLES_PASSED
        // query is active. Returns whether a slot was begun (End must mirror it).
        Bool BeginOcclusionForDraw(VkCommandBuffer commandBuffer);
        void EndOcclusionForDraw(VkCommandBuffer commandBuffer, Bool began);
        Bool m_occlusionQueryPreciseEnabled = false;
        Bool m_hostQueryResetEnabled = false;
        PFN_vkResetQueryPool s_vkResetQueryPool = nullptr;
        VkQueryPool m_occlusionQueryPool = VK_NULL_HANDLE;
        static constexpr Uint32 kOcclusionQuerySlots = 8192;
        Uint32 m_occlusionSlotCursor = 0;
        Bool m_occlusionCaptureActive = false;
        Vector<Uint32> m_occlusionActiveSlots;
        // Transform feedback primitive queries: one pool slot per captured draw yields
        // the (written, needed) pair; GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN sums the
        // first, GL_PRIMITIVES_GENERATED the second - exact with geometry shaders,
        // unlike the CPU fallback accounting.
        Bool m_xfbQueriesSupported = false;
        PFN_vkCmdBeginQueryIndexedEXT s_vkCmdBeginQueryIndexedEXT = nullptr;
        PFN_vkCmdEndQueryIndexedEXT s_vkCmdEndQueryIndexedEXT = nullptr;
        VkQueryPool m_xfbQueryPool = VK_NULL_HANDLE;
        static constexpr Uint32 kXfbQuerySlots = 8192;
        Uint32 m_xfbQuerySlotCursor = 0;
        Bool m_xfbQueryCaptureActive[2] = {false, false}; // [0]=written, [1]=generated
        Vector<Uint32> m_xfbQueryActiveSlots[2];
        Bool m_xfbQuerySlotOpen = false;
        Uint32 m_xfbQueryOpenSlot = 0;

    public:
        // kind: 0 = PRIMITIVES_WRITTEN, 1 = PRIMITIVES_GENERATED.
        Bool StartXfbQueryCapture(Uint32 kind);
        void StopXfbQueryCapture(Uint32 kind, Vector<Uint32>& outSlots);
        Bool ResolveXfbQueryResult(const Vector<Uint32>& slots, Bool wantGenerated, Uint64& outPrimitives);

    private:
        void BeginXfbQueryForDraw(VkCommandBuffer commandBuffer);
        void EndXfbQueryForDraw(VkCommandBuffer commandBuffer);

        VkCommandPool m_commandPool = VK_NULL_HANDLE;

        VkBufferManager m_bufferManager;

        Uint m_imageIndexAcquired = 0;
        FrameContext m_frameContext;

        UniquePtr<PipelineFactory> m_pipelineFactory;
        // Single-slot "last pipeline" memo: skip the per-draw GetOrCreatePipeline work (state
        // gather + synthetic vertex-input rebuild + payload hash + lookup) when the full pipeline
        // state is unchanged from the previous draw. The key provably covers every pipeline field.
        // Reset per-frame and on pipeline destruction so the cached handle can never dangle.
        // Small N-way pipeline-resolution memo (round-robin replacement). A
        // single-entry memo thrashed on draw sequences that alternate a few
        // pipelines (GUI text/quad program ping-pong), paying the full
        // payload-hash lookup per draw; eight entries cover such working sets
        // while keeping the hit path a trivial linear scan.
        struct PipelineMemoEntry {
            GLenum mode = 0;
            Uint64 programHash = 0;
            Uint64 vertexInputHash = 0;
            Uint64 renderPassHash = 0;
            // VALUE hash of the pipeline-relevant fixed-function state (see
            // ComputePipelineStateHash), not the monotonic pipeline-state version:
            // the version never repeats, so a per-draw GL_BLEND toggle would miss
            // all entries forever even though the state alternates between two
            // values the memo already holds.
            Uint64 pipelineStateHash = 0;
            ProgramFactory::CompileOptionFlags transformFlags = {};
            // Baked into the pipeline (PipelineFactory::ComputeHash mixes it), and NOT derivable
            // from anything else in this key: it depends on whether the draw is indexed and on the
            // index type, neither of which the mode/program/state hashes carry. Without it an
            // indexed and a non-indexed draw over the same program and state collide on one entry
            // and the second one gets the first one's restart setting.
            Bool primitiveRestartEnable = false;
            VkPipeline pipeline = VK_NULL_HANDLE;
        };
        static constexpr Uint32 kPipelineMemoSize = 8;
        PipelineMemoEntry m_pipelineMemo[kPipelineMemoSize];
        Uint32 m_pipelineMemoCount = 0;
        Uint32 m_pipelineMemoNext = 0;
        // Hash of every fixed-function GL state the pipeline payload reads that the
        // memo key's other fields (mode / program / vertex input / render pass /
        // transform flags) do not already pin down. Equal hash under an equal rest
        // of key => byte-identical PipelineCreatePayload. Cached per pipeline-state
        // version: the version is monotonic and bumps on every pipeline-state
        // change, so an unchanged (version, colorAttachmentCount) proves the state
        // bytes are unchanged and the hash can be reused without re-reading them.
        Uint64 ComputePipelineStateHash(Uint32 colorAttachmentCount,
                                        VkSampleCountFlagBits rasterizationSamples) const;
        // The effective GL_SAMPLE_MASK word for a draw at this rasterization sample count; see
        // the definition for the GL-vs-Vulkan rule it reconciles. Shared by the pipeline payload
        // and the pipeline-state memo word so the two cannot disagree.
        Uint32 ResolveEffectiveSampleMask(VkSampleCountFlagBits rasterizationSamples) const;
        Uint m_pipelineStateHashVersion = 0;
        Uint32 m_pipelineStateHashColorCount = 0;
        // The sample count the cached hash was computed at. A pipeline-state input now depends on
        // it (the effective sample mask), so a draw that changes only the target's sample count
        // has to recompute rather than reuse.
        VkSampleCountFlagBits m_pipelineStateHashSampleCount = VK_SAMPLE_COUNT_1_BIT;
        Uint64 m_pipelineStateHash = 0;
        Bool m_pipelineStateHashValid = false;
        // GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the
        // function also reads whether the bound DRAW framebuffer is the default one
        // (only the default framebuffer gets the Y-flip and rotation bits - an FBO
        // pass renders unflipped). Keyed on BOTH inputs; missing the FBO bit shipped
        // an upside-down default-framebuffer pass after any render-to-texture
        // (minecraft-1.17-main-menu retrace, whole frame flipped).
        VkSurfaceTransformFlagBitsKHR m_baseTransformFlagsPreTransform =
            VK_SURFACE_TRANSFORM_FLAG_BITS_MAX_ENUM_KHR;
        Bool m_baseTransformFlagsIsDefaultFbo = false;
        Bool m_baseTransformFlagsKeyValid = false;
        Uint32 m_baseTransformFlagsCache = 0;
        // isDefaultFbo must be the default-ness of the CURRENTLY bound draw framebuffer;
        // every caller already has it in hand from its own guards.
        Uint32 GetBaseTransformFlagsRaw(Bool isDefaultFbo);
        // Drops every memoized pipeline handle. Required at command-buffer
        // boundaries and whenever any pipeline may have been destroyed. Also drops
        // the cached pipeline-state hash: the same boundaries can retire the GL
        // context whose monotonic version the cache is keyed on.
        void InvalidatePipelineMemo() {
            m_pipelineMemoCount = 0;
            m_pipelineMemoNext = 0;
            m_pipelineStateHashValid = false;
        }
        UnorderedMap<ProgramFactory::HashType, VkPipeline> m_computePipelines;
        UniquePtr<ProgramFactory> m_programFactory;
        UniquePtr<UniformManager> m_uniformManager;
        UniquePtr<VertexInputStateFactory> m_vertexInputStateFactory;
        UniquePtr<VkClearManager> m_clearManager;
        UniquePtr<VkRenderPassManager> m_renderPassManager;
        UniquePtr<VkTextureManager> m_textureManager;
        UniquePtr<VkSamplerManager> m_samplerManager;
        UniquePtr<VkTimerQueryManager> m_timerQueryManager;
        BlitResources m_blitResources;
        DepthMipmapResources m_depthMipmapResources;
        Vector<DeferredDepthMipmapCleanup> m_deferredDepthMipmapCleanup;

        // Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled
        // texture SET is provably unchanged from the previous draw: same program (lifetime id +
        // backend-state version, which covers sampler-uniform reassignment / relink) and transform
        // flags, no texture bind/unbind/delete since (GetTextureBindGeneration), and nothing that
        // moves a texture's shape or a sampler's parameters since (GetSamplingResolutionGeneration
        // - membership depends on mipmap-completeness, which both of those decide). On a hit,
        // m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback /
        // layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL
        // walk is skipped. The program lifetime id (never reused, unlike the GL name) and the
        // monotonic bind generation make the key ABA-proof; the per-command-buffer reset is a cheap
        // belt-and-suspenders.
        Bool m_lastSampledSetValid = false;
        Uint64 m_lastSampledSetProgramLifetimeId = 0;
        Uint32 m_lastSampledSetProgramVersion = 0;
        ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {};
        Uint64 m_lastSampledSetBindGeneration = 0;
        Uint64 m_lastSampledSetSamplingGeneration = 0;
        // Set from the draw's resolved VkProgramObject on both the full and the fast setup paths;
        // read by BeginXfbCaptureForDraw, which has only GL state otherwise. See
        // VkProgramObject::xfbCaptureDeclined.
        Bool m_currentDrawXfbCaptureDeclined = false;

        // Memo for the per-draw explicit-LOD-0 eligibility probe
        // (ProgramSamplesOnlySingleLevelTextures): same key family as the
        // sampled-set memo, plus the sampled textures' params-version sum so a
        // level-range or filter change re-probes. On a hit the resolved
        // transform flags are reused, which also collapses the two
        // GetOrCreateProgram lookups into one.
        Bool m_lastLodDecisionValid = false;
        Uint64 m_lastLodProgramLifetimeId = 0;
        Uint32 m_lastLodProgramVersion = 0;
        Uint64 m_lastLodBindGeneration = 0;
        Uint64 m_lastLodParamsSum = 0;
        // Sampling-resolution generation at probe time. The probe reads the effective
        // sampler's filters/aniso/LOD range, whose setters bump only this counter -
        // the params-version sum above never moves for them.
        Uint64 m_lastLodSamplingGeneration = 0;
        ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {};
        ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {};

        // Does the current program's vertex stage declare the BaseVertex builtin? A property
        // of the program's SPIR-V, so (lifetime id, backend-state version) is the whole key.
        //
        // Memoized rather than re-asked because asking means resolving the UN-zeroed program
        // variant, and a program that only ever draws non-indexed would then compile a variant
        // no draw uses AND re-stamp its use every draw, so the idle sweep could never retire
        // it. With the memo the answer is known before the first lookup and only the variant
        // the draw actually needs is resolved.
        Bool m_lastBaseVertexQueryValid = false;
        Uint64 m_lastBaseVertexProgramLifetimeId = 0;
        Uint32 m_lastBaseVertexProgramVersion = 0;
        Bool m_lastBaseVertexReads = false;

        // Snapshot behind TrySetupDrawFastPath. Values only: the program and
        // render-pass caches are open-addressing maps whose entries move on
        // insert, so no pointers into them are cached; the pipeline handle is
        // protected by the command-buffer-boundary reset plus the mid-frame
        // pipeline-destruction resets, and monotonic epochs guard everything
        // that can be destroyed or recreated between draws.
        struct SetupDrawSnapshot {
            Bool valid = false;
            Uint8 aspects = 0;
            GLenum mode = 0;
            Uint64 programLifetimeId = 0;
            Uint32 programVersion = 0;
            const void* vao = nullptr;
            // Same rule as VaoDrawMemo::vaoLifetimeId: (address, config version) is not an
            // identity, because a recycled address can arrive carrying a config version
            // the dead VAO also had (two mutations to configure one attribute is the
            // common shape), and "the VAO did not move" would then skip the layout
            // re-resolve for a different VAO.
            Uint64 vaoLifetimeId = 0;
            Uint32 vaoConfigVersion = 0;
            const void* drawFbo = nullptr;
            // Never-reused lifetime id beside the raw pointer + Uint16 version: a
            // deleted FBO recycled at the same address with the same fresh version
            // count would otherwise compare equal (same ABA as the render-pass
            // manager's fast-path memo).
            Uint64 drawFboLifetimeId = 0;
            Uint16 fboVersion = 0;
            Bool drawFboIsDefault = false;
            Uint renderStateVersion = 0;
            Uint64 bindGeneration = 0;
            Uint32 baseTransformFlags = 0;
            Uint32 resolvedTransformFlags = 0;
            // What ResolvePrimitiveRestartEnable answered for the draw this snapshot was taken
            // from, i.e. what its pipeline's primitiveRestartEnable was built with. `aspects`
            // already separates indexed from non-indexed draws, but not one index TYPE from
            // another, and a restart index that fits GL_UNSIGNED_INT but not GL_UNSIGNED_SHORT
            // makes those two draws want different pipelines.
            Bool primitiveRestartEnable = false;
            Uint64 renderPassHash = 0;
            Uint32 imageIndex = 0;
            Uint64 textureEraseEpoch = 0;
            Uint64 textureImageEpoch = 0;
            Uint64 renderbufferImageEpoch = 0;
            Uint64 sampledContentSum = 0;
            Uint64 sampledParamsSum = 0;
            // Guards the sampler-descriptor reuse hint: bumped by any sampler-object
            // parameter or texture shape change (see GetSamplingResolutionGeneration),
            // none of which the sums above cover.
            Uint64 samplingResolutionGeneration = 0;
            // Render-pass flavor input (DepthTest || StencilTest at snapshot time).
            // A pipeline-state change that leaves this equal cannot change which
            // render pass GetOrCreateRenderPass would pick, so the fast path may
            // re-resolve just the pipeline against the active pass; a change that
            // flips it must fall back to the full path's pass selection.
            Bool drawUsesDepthStencil = false;
            // The snapshotting draw's pipeline viewportCount. A pure function of the PROGRAM
            // (writesViewportIndexBuiltin) and of a device feature fixed at renderer init, both
            // of which the programLifetimeId/programVersion guards above already pin - carried
            // here so the fast path does not re-fetch the program object to re-derive it.
            Uint32 viewportCount = 1;
            IntVec2 renderPassExtent = {0, 0};
            // colorAttachmentCount of the snapshotting draw's render pass: the
            // pipeline-state hash input, so the fast path can refresh that hash and
            // probe the pipeline memo after a state change without re-fetching the
            // render-pass entry (the pass itself is pinned by renderPassHash above).
            Uint32 renderPassColorCount = 0;
            // Pinned with the colour count and for the same reason: the fast path recomputes the
            // pipeline-state value hash from the snapshot, and that hash reads the sample count.
            VkSampleCountFlagBits renderPassSampleCount = VK_SAMPLE_COUNT_1_BIT;
            VkPipeline pipeline = VK_NULL_HANDLE;
            // layoutHash of the snapshotting draw's vertex-input state. The pipeline and
            // the vertex-input pre-flight depend on the VAO only through this (plus the
            // program, pinned separately), so a changed VAO whose aux memo carries the
            // same layoutHash re-uses the snapshot's pipeline and pre-flight verdict
            // outright - the VAO-cycling case Minecraft chunk rendering hits every draw.
            Uint64 vaoLayoutHash = 0;
            // Memoised ProgramFactory entry of the snapshotting draw, valid while
            // (programLifetimeId, programVersion, resolvedTransformFlags) match - all
            // checked above - AND the factory's cache structure epoch is unchanged (the
            // cache is open-addressing and holds entries by value, so any insert/erase
            // moves them). The fast path must re-stamp use through StampProgramUse when
            // it bypasses GetOrCreateProgram, or the idle sweep could evict a live entry.
            const ProgramFactory::VkProgramObject* programObj = nullptr;
            Uint64 programFactoryEpoch = 0;
            // Per-entry copies of the snapshotting draw's sampled set (the scratch
            // vectors below hold only the LAST full-path draw's set, which with more
            // than one snapshot entry is not necessarily this entry's program).
            // sampledTextures/sampledResources carry the same epoch-guarded pointer
            // lifetime rules as the scratch originals: textureEraseEpoch (checked
            // every probe) declines the entry before any erased resource pointer
            // could be dereferenced. sampledLayouts is the layout VALUE each
            // resource held when this entry's descriptors were built (the
            // descriptor-reuse hint needs the SAME layout, not just a sampleable
            // one), and sampledBindingRecords feeds SampledBindingsUnchanged when
            // the bind generation moved.
            Vector<MG_State::GLState::ITextureObject*> sampledTextures;
            Vector<VkTextureManager::TextureResource*> sampledResources;
            Vector<VkImageLayout> sampledLayouts;
            Vector<UniformManager::SampledBindingRecord> sampledBindingRecords;
        };
        // Program-keyed snapshot entries: program ping-pong (Sodium switches programs
        // mid-frame every few draws) would otherwise evict the single snapshot on
        // every switch and send every draw through the full path. Entries are found
        // by programLifetimeId (MRU-first probe); every other guard stays per-probe,
        // so a stale entry declines itself exactly like the old single snapshot did.
        static constexpr Uint32 kSetupDrawSnapshotCount = 4;
        SetupDrawSnapshot m_setupDrawSnapshots[kSetupDrawSnapshotCount];
        Uint32 m_setupDrawSnapshotMru = 0;    // last entry that hit or was filled
        Uint32 m_setupDrawSnapshotVictim = 0; // round-robin fill cursor when all entries are live
        void InvalidateSetupDrawSnapshots() {
            for (auto& snapshot : m_setupDrawSnapshots) {
                snapshot.valid = false;
            }
        }

        // Per-draw scratch buffers (clear keeps capacity) — these paths run for every
        // draw call and must not allocate.
        Vector<MG_State::GLState::ITextureObject*> m_sampledTexturesScratch;
        // Per-binding (texture, effective sampler) lifetime-id records from the same
        // CollectSampledTextures walk that filled m_sampledTexturesScratch. The fast
        // path shadow-compares against them (SampledBindingsUnchanged) when the
        // texture bind generation moved, so a redundant glBindSampler/glBindTexture
        // storm that resolves to the same bindings keeps the fast path.
        Vector<UniformManager::SampledBindingRecord> m_sampledBindingRecordsScratch;
        // Parallel to m_sampledTexturesScratch, refilled by every SetupDraw's
        // first sampled-texture loop: the resolved backend resources, so the
        // post-transition loop can skip re-resolving textures whose layout is
        // already sampleable.
        Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
        Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
        Vector<UniformManager::SamplerImageFeedbackBinding> m_samplerImageFeedbackScratch;
        Vector<UniformManager::SamplerBindingOverride> m_samplerImageBindingOverridesScratch;
        Vector<VkBuffer> m_vertexBuffersScratch;
        Vector<VkDeviceSize> m_vertexOffsetsScratch;
        Vector<VkVertexInputAttributeDescription> m_patchedAttributesScratch;
        Vector<Float> m_vertexConversionScratch;
        Vector<Uint8> m_vertexRepackScratch;

        struct ConvertedVertexStreamKey {
            const MG_State::GLState::BufferObject* buffer = nullptr;
            Uint64 changeSerial = 0;
            SizeT baseOffset = 0;
            Uint32 sourceStride = 0;
            DataType type = DataType::Float32;
            Int size = 0;
            Bool normalized = false;
            Bool isInteger = false;
            VertexInputStateFactory::VertexStreamConversion conversion =
                VertexInputStateFactory::VertexStreamConversion::None;

            Bool operator==(const ConvertedVertexStreamKey& other) const {
                return buffer == other.buffer && changeSerial == other.changeSerial &&
                       baseOffset == other.baseOffset && sourceStride == other.sourceStride &&
                       type == other.type && size == other.size && normalized == other.normalized &&
                       isInteger == other.isInteger && conversion == other.conversion;
            }
        };

        struct ConvertedVertexStreamKeyHash {
            SizeT operator()(const ConvertedVertexStreamKey& key) const {
                SizeT hash = std::hash<const void*>{}(key.buffer);
                auto combine = [&hash](SizeT value) {
                    hash ^= value + static_cast<SizeT>(0x9e3779b97f4a7c15ull) + (hash << 6) + (hash >> 2);
                };
                combine(std::hash<Uint64>{}(key.changeSerial));
                combine(std::hash<SizeT>{}(key.baseOffset));
                combine(std::hash<Uint32>{}(key.sourceStride));
                combine(std::hash<Uint32>{}(static_cast<Uint32>(key.type)));
                combine(std::hash<Int>{}(key.size));
                combine(std::hash<Bool>{}(key.normalized));
                combine(std::hash<Bool>{}(key.isInteger));
                combine(std::hash<Uint32>{}(static_cast<Uint32>(key.conversion)));
                return hash;
            }
        };

        struct ConvertedVertexStream {
            BufferSlice slice;
            // Number of source elements the cached slice covers. A draw needing a prefix of
            // this range reuses the slice (converted streams are tightly packed); a draw
            // needing more reconverts and replaces the entry, so per (buffer, layout) a
            // frame converts at most the largest range any draw asked for.
            SizeT elementCount = 0;
            // Pins the source buffer for the frame so its heap address cannot be reused by
            // a new BufferObject while this pointer-keyed entry is alive.
            SharedPtr<const MG_State::GLState::BufferObject> sourcePin;
        };
        UnorderedMap<ConvertedVertexStreamKey, ConvertedVertexStream, ConvertedVertexStreamKeyHash>
            m_convertedVertexStreams;

        // One VAO's resolved vkCmdBindVertexBuffers arguments, reusable by a later draw
        // that would resolve them to the same thing. Consecutive draws in a chunk-renderer
        // frame keep the program and the vertex layout and only swap the VAO, so a
        // per-VAO memo turns the second and later draws through each VAO into a validate
        // plus (usually skipped) rebind.
        //
        // Only whole-buffer bindings are memoised. Client-memory and format-converted
        // streams re-upload from a range that depends on the draw's own vertex/index
        // range, and synthetic bindings carry glVertexAttrib* values that are not part
        // of any key here; a layout using any of them is never stored.
        // Field order is hit-path cache locality, hot to cold: the per-draw validate
        // reads the scalars and the EBO memo head, then only the first bindingCount
        // elements of vkBuffers/vkOffsets; the per-binding revalidation arrays at the
        // tail are touched once per frame at most.
        struct ResolvedVertexBindings {
            // Must equal DynamicStateShadow::kMaxShadowedVertexBindings (static_assert in
            // the .cpp): past that width the bind shadow cannot skip a redundant bind
            // either, so a wider layout resolves per draw. Minecraft-shaped layouts use four.
            static constexpr Uint32 kMaxBindings = 8;

            // Frame serial of the last completed resolve OR cross-frame revalidation.
            // Zero until a resolve completes, and reset to zero before one starts, so a
            // resolve that bails out midway cannot leave a half-filled entry matchable.
            // Unlike the original frame-scoped memo, an entry whose buffers are all
            // resident and unmapped is revalidated across frames (per-binding slice
            // epoch compares) instead of re-resolved - see TryBindResolvedVertexBindings.
            Uint64 frameSerial = 0;
            // Identity of the resolved Vulkan layout: the VAO's content hash
            // (VertexInputStateFactory::GetOrComputeHash - the same value the factory
            // keys its entries on) fixes bindings.size(), each binding's base offset,
            // which bindings are client/converted, and (through the mixed-in buffer
            // addresses) which buffer each binding reads. Compared against the VAO's
            // own hash memo on the hit path, so a hit never touches the factory entry.
            VertexInputStateFactory::HashType vertexInputHash = 0;
            // The program's vertex input layout: decides the synthetic-binding set and
            // hence the total binding count.
            Uint32 activeAttribMask = 0;
            Uint32 bindingCount = 0;
            // VkBufferManager::GetSliceEpochCounter() at resolve time. Still equal means
            // no buffer anywhere changed its slice or was persistently mapped since, which
            // settles every per-binding question below in one compare.
            Uint64 sliceEpochCounter = 0;
            // Any bound buffer already carrying a host map when the slice was resolved.
            // Such a buffer can mutate its shadow with no API call, so it has to be
            // re-pushed per draw and the one-compare path above cannot apply.
            Bool anyBufferMapped = true;

            // Resident element-buffer slice memo (skips the per-draw AcquireResidentSlice
            // for the VAO's EBO, which cold-chases 500+ distinct resources in a
            // chunk-cycling frame). Self-validating exactly like the bindings above: a hit
            // requires the LIVE bound EBO pointer to equal indexBuffer AND either an
            // unmoved manager-wide slice-epoch counter (nothing anywhere changed slices
            // or gained a host map, the same one-compare rescue the vertex half uses) or
            // that buffer's resource still carrying indexSliceEpoch (epochs are minted
            // from a process-lifetime counter, so a recycled address can never
            // revalidate). Restart-substituted and streamed EBOs are never stored.
            // indexFrameSerial tracks the last frame the resource's GPU-use serial was
            // stamped through this memo; 0 means no index memo. Independent of the
            // vertex half: both are (pointer, epoch)-validated, so neither can serve
            // stale state for the other.
            const MG_State::GLState::BufferObject* indexBuffer = nullptr;
            Uint64 indexSliceEpoch = 0;
            // GetSliceEpochCounter() when the resource's epoch was last verified; only
            // meaningful while indexFrameSerial matches the current frame serial.
            Uint64 indexSliceEpochCounter = 0;
            VkBuffer indexVkBuffer = VK_NULL_HANDLE;
            VkDeviceSize indexSliceOffset = 0;
            Uint64 indexFrameSerial = 0;
            // The EBO carried a host map when the slice was recorded - the mirror of
            // anyBufferMapped on the vertex half. A shadow-backed (non-adopted)
            // persistent map mutates its shadow with no API call and no epoch bump, so
            // the one-compare rescue must decline and re-run the acquire, whose
            // SyncPersistentMappedRange is the push-down. A map taken AFTER the record
            // is already covered: AcquirePersistentMap bumps the slice epoch for the
            // request itself, adopted or declined.
            Bool indexBufferMapped = false;

            // Bound per draw (first bindingCount elements).
            VkBuffer vkBuffers[kMaxBindings] = {};
            VkDeviceSize vkOffsets[kMaxBindings] = {};
            // Per binding: the VAO attribute location its buffer comes from, that buffer,
            // and the buffer's VkBufferManager slice epoch when the slice was resolved.
            // Only read by the per-frame revalidation and the something-moved fallback.
            Uint8 attributeLocations[kMaxBindings] = {};
            const MG_State::GLState::BufferObject* buffers[kMaxBindings] = {};
            Uint64 sliceEpochs[kMaxBindings] = {};
        };
        // One direct-mapped slot of the per-VAO draw-memo table below. A slot belongs to
        // the object whose (vaoKey, vaoLifetimeId) pair it carries: the address alone
        // only picks the slot, and the never-reused lifetime id is what proves the slot
        // is THIS VAO's, so the successor allocated onto a destroyed VAO's address
        // always misses. That identity check is load-bearing and the content-hash
        // validations below do NOT stand in for it - a recycled address under a
        // byte-identical configuration reproduces the content hash exactly, which is
        // how a destroyed VAO's resolved bindings were once handed to its successor's
        // draw. The slot is still never dereferenced through vaoKey, and every fact it
        // carries is still validated against live state before use:
        //  - layoutHash/layoutAuxMasks are valid only while contentHash equals the LIVE
        //    VAO's own hash memo (which the VAO's config version guards), so a config
        //    change or a buffer rebind misses even for the same object.
        //  - bindings revalidates per draw exactly as before (frame serial, content
        //    hash, per-binding live buffer pointers and slice epochs).
        struct alignas(64) VaoDrawMemo {
            const MG_State::GLState::VertexArrayObject* vaoKey = nullptr;
            // The VAO's never-reused lifetime id, checked alongside vaoKey. The pointer
            // ALONE is not an identity: a deleted VAO's heap address is handed straight
            // back by the next glGenVertexArrays-shaped allocation, and the successor then
            // matched this slot and inherited the dead object's memos. Both stated
            // defences failed with it, because both reduce to the content hash and the
            // content hash's buffer-identity component was itself a recycled heap address.
            Uint64 vaoLifetimeId = 0;
            // The VAO content hash (VertexInputStateFactory::GetOrComputeHash) the two
            // layout facts below were derived from; 0 while nothing valid is stored.
            Uint64 contentHash = 0;
            Bool layoutFactsValid = false;
            // The resolved layout identity + packed (unsupported, location) masks -
            // the exact values GetBackendAuxMemo used to serve, moved here so the
            // per-draw probe stays inside this table's one hot line instead of
            // touching a second cold line of every cycled VAO object.
            Uint64 layoutHash = 0;
            Uint64 layoutAuxMasks = 0;
            ResolvedVertexBindings bindings;
        };
        // Fixed-size, allocated on first use, never rehashed or swept: entries are
        // recycled in place on slot collisions (two-slot probe, older frame serial
        // evicted), and stale entries self-invalidate through the compares above. A
        // fixed table also makes every VaoDrawMemo/ResolvedVertexBindings pointer
        // stable for the duration of a draw, which the EBO memo handoff
        // (m_currentDrawResolvedEntry) relies on.
        static constexpr Uint32 kVaoDrawMemoSlotCount = 2048; // power of two
        Vector<VaoDrawMemo> m_vaoDrawMemoTable;
        // Finds the slot holding `vao`, or recycles the older of its two candidate
        // slots into an empty memo keyed on `vao`. Never returns null.
        VaoDrawMemo* LookupVaoDrawMemo(const MG_State::GLState::VertexArrayObject* vao);
        // The current draw's memo entry, set by UploadAndBindVertexBuffers and consumed
        // by the same draw's UploadAndBindIndexBuffer (the EBO memo lives in the same
        // entry). Valid ONLY within that window: the next draw's lookup can recycle the
        // slot. Null when the draw's layout is not memoisable.
        ResolvedVertexBindings* m_currentDrawResolvedEntry = nullptr;

        void CreateInstance();
        VkResult SetupDebugMessenger();
        VkResult DestroyDebugMessenger();
        VkResult SetupDebugReportCallback();
        void DestroyDebugReportCallback();
        VkDebugUtilsMessengerCreateInfoEXT PopulateDebugMessengerCreateInfo();
        void CreateSurface();
        void PickPhysicalDevice();
        void CreateLogicalDeviceAndQueues();
        void CreateAllocator();
        void DestroyAllocator();
        void CreateSwapchain();
        void CreateCommandPool();

        // Whether THIS draw's primitive stream restarts, and therefore what
        // VkPipelineInputAssemblyStateCreateInfo::primitiveRestartEnable must be. Resolved by the
        // caller because it needs two facts a pipeline cannot see: whether the draw is indexed at
        // all (GL primitive restart acts on the index stream, so it is a no-op for glDrawArrays),
        // and the index TYPE (an application restart index that does not fit the type matches no
        // index, so that draw restarts nowhere - see UploadAndBindIndexBuffer).
        Bool ResolvePrimitiveRestartEnable(Flags<DrawSetupAspect> aspects,
                                           const IndexBufferView* pIndexBufferView) const;

        VkPipeline GetOrCreatePipeline(
            GLenum mode,
            const MG_State::GLState::ProgramObject& program,
            const ProgramFactory::VkProgramObject& programObj,
            ProgramFactory::CompileOptionFlags transformFlags,
            const MG_State::GLState::VertexArrayObject& vao,
            const RenderPassEntry& renderPassEntry,
            Bool primitiveRestartEnable);
        VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj);
        void DestroyComputePipelines();
        // Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to
        // flush the pending recording (see the body), which retires the current command buffer.
        Bool PrepareStorageImageTextures(
            FrameContext::FrameData& frame,
            const MG_State::GLState::ProgramObject& program,
            const ProgramFactory::VkProgramObject& programObj);
        // Vulkan forbids a sampled descriptor and writable storage descriptor from naming the
        // same image subresource in one shader operation. Snapshot only the sampler side; the
        // storage descriptor continues to name the application texture.
        Bool PrepareSamplerImageFeedbackSnapshots(
            FrameContext::FrameData& frame,
            const MG_State::GLState::ProgramObject& program,
            const ProgramFactory::VkProgramObject& programObj,
            VkPipelineStageFlags consumerShaderStageMask);

        // The per-draw dynamic-state tail (viewport, scissor, blend constants, depth
        // bias, line width, stencil), gated behind one render-state-parameters-version
        // compare per command buffer - see the gate fields in DynamicStateShadow.
        // viewportCount is the bound pipeline's declared viewport count: 1 for every program that
        // does not write gl_ViewportIndex (the memoized fast path), otherwise the renderer's
        // rasterizable viewport count, which takes the unmemoized array path.
        void ApplyDynamicDrawStateTail(FrameContext::FrameData& frame, const IntVec2& extent, Bool isDefaultFbo,
                                       Uint32 viewportCount = 1);
        void ApplyMultiViewportDynamicState(VkCommandBuffer commandBuffer, Uint32 viewportCount, const IntVec2& extent,
                                            VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo);
        VkRect2D ComputeGLScissorRect(Uint32 index, const IntVec2& extent,
                                      VkSurfaceTransformFlagBitsKHR preTransform, Bool isDefaultFbo) const;
        // How many viewports a draw with this program rasterizes into: 1 unless the program
        // assigns gl_ViewportIndex AND the device enabled multiViewport. Both the pipeline's
        // baked viewportCount and the dynamic arrays come from this one answer, so they cannot
        // disagree.
        Uint32 ResolveDrawViewportCount(Bool programWritesViewportIndex) const {
            return programWritesViewportIndex && m_multiViewportFeatureEnabled ? m_maxRasterizableViewports : 1u;
        }

        Bool UploadAndBindVertexBuffers(VkCommandBuffer commandBuffer, const MG_State::GLState::VertexArrayObject& vao,
                                        const ProgramFactory::VkProgramObject& programObj,
                                        const DrawCmdParam& drawParams,
                                        const IndexBufferView* pIndexBufferView);
        // Binds `entry`'s memoised buffers when every input it was resolved from is
        // still live and unchanged, else returns false and leaves nothing bound.
        // vaoContentHash is the VAO's memoised content hash (GetBackendHashMemo), which
        // pins the layout AND the bound buffers without resolving the factory entry.
        // Non-const entry: a cross-frame revalidation refreshes its serial/epoch stamps.
        Bool TryBindResolvedVertexBindings(VkCommandBuffer commandBuffer,
                                           const MG_State::GLState::VertexArrayObject& vao,
                                           ResolvedVertexBindings& entry,
                                           Uint64 vaoContentHash,
                                           Uint32 activeAttribMask, Uint64 frameSerial);
        Bool UploadAndBindIndexBuffer(FrameContext::FrameData& frame,
                                     const MG_State::GLState::VertexArrayObject& vao,
                                      const IndexBufferView* pIndexBufferView = nullptr);
        Bool InitializeBlitResources();
        Bool InitializeDepthMipmapResources();
        void ShutdownBlitResources();
        void ShutdownDepthMipmapResources();
        void CollectDeferredDepthMipmapCleanup(Uint32 frameIndex);
        void DestroyDeferredDepthMipmapCleanup();
        Bool TryBlitToDefaultFramebufferWithShader(FrameContext::FrameData& frame,
                                                   MG_State::GLState::FramebufferObject& readFbo,
                                                   MG_State::GLState::FramebufferObject& drawFbo,
                                                   GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
                                                   GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
                                                   GLenum filter);
        // Clears one layer of a colour image through a throwaway render pass whose entire content
        // is its LOAD_OP_CLEAR. Two callers, both of which a transfer clear cannot serve: a z
        // slice of a VK_IMAGE_TYPE_3D image (vkCmdClearColorImage cannot name one), and a
        // MULTISAMPLE image (which carries no TRANSFER_DST usage at all). `finalLayout` is the
        // layout the caller already tracks for the whole image, so this never has to touch
        // resource->layout.
        Bool ClearDepthSliceWithRenderPass(VkCommandBuffer commandBuffer,
                                           MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
                                           Uint32 depthSlice, const VkClearValue& clearValue,
                                           VkImageLayout finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
        Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
                                               MG_State::GLState::ITextureObject& texture);
        // The multisample arm of the above. Split out rather than branched inline because it
        // shares none of the transfer path: a multisample image carries no TRANSFER_DST usage, so
        // neither the TRANSFER_DST transition nor vkCmdClearColorImage is legal on one.
        Bool MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
                                                MG_State::GLState::ITextureObject& texture,
                                                VkTextureManager::TextureResource& resource,
                                                const Vector<PendingClearEntry>& pendingClears);
        Bool MaterializePendingClearForRenderbuffer(
            VkCommandBuffer commandBuffer,
            const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
        // The default framebuffer's twin of the two above. It cannot go through
        // MaterializePendingClearForTexture: the default FBO's colour attachment is a
        // placeholder texture object, and syncing THAT would clear a texture image nobody
        // presents instead of the acquired swapchain image.
        Bool MaterializePendingClearForDefaultFramebuffer(VkCommandBuffer commandBuffer,
                                                          MG_State::GLState::FramebufferObject& fbo,
                                                          FramebufferAttachmentType attachmentType);
        // Its depth/stencil half: a different image (the swapchain's depth/stencil twin), a
        // different clear command and per-aspect masking.
        Bool MaterializePendingDepthStencilClearForDefaultFramebuffer(
            VkCommandBuffer commandBuffer, const MG_State::GLState::FramebufferAttachmentObject& attachment,
            const ClearAttachmentPayload& payload);
        VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
        Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
                                           MG_State::GLState::ITextureObject& texture,
                                           VkTextureManager::TextureResource& resource,
                                           Uint32 baseMipLevel,
                                           Uint32 generateMipLevelCount,
                                           const IntVec3& storageBaseTexelSize,
                                           VkImageLayout originalLayout,
                                           VkImageLayout finalLayout);
        Bool SubmitReadbackCommandsAndWait(FrameContext::FrameData& frame);

    public:
        // Submits whatever is recorded and waits for it. The CPU is about to read memory
        // a shader wrote (a mapped shader storage buffer), and coherent host-visible
        // storage only guarantees visibility once the work that produced it has retired.
        Bool FinishPendingGpuWork();

    private:

        void ShutdownSwapchain();

        // Static functions
        static Int GetPresentQueueFamilyIndex(const PhysicalDevice& physicalDevice, VkSurfaceKHR surface,
                                              const Vector<VkQueueFamilyProperties>& queueFamilies,
                                              Int preferredFamilyIndex = -1);
        static Vector<VkQueueFamilyProperties> GetQueueFamilyFromPhysicalDevice(VkPhysicalDevice device);
        static Int GetQueueFamilyIndex(const Vector<VkQueueFamilyProperties>& queueFamilies, VkQueueFlagBits flag);
        static Vector<VkExtensionProperties> EnumerateInstanceExtensions();
        static Vector<VkExtensionProperties> EnumerateDeviceExtensions(VkPhysicalDevice device);
        static Bool IsExtensionSupported(const Vector<VkExtensionProperties>& availableExtensions,
                                         const char* extensionName);
        static Bool IsExtensionAlreadyEnabled(const Vector<const char*>& enabledExtensions, const char* extensionName);
        static Bool EnableOptionalDeviceExtension(const Vector<VkExtensionProperties>& availableExtensions,
                                                  Vector<const char*>& inOutEnabledExtensions,
                                                  const char* extensionName);
        void ResolveOptionalDeviceExtensions(const Vector<VkExtensionProperties>& availableExtensions,
                                             Vector<const char*>& inOutEnabledExtensions);
        static Bool IsNecessaryDeviceExtensionSupported(VkPhysicalDevice device);
        static Bool GetMoreCapablePhysicalDevice(VkPhysicalDevice newVkDevice, VkSurfaceKHR surface,
                                                 const PhysicalDevice& compareWithDevice,
                                                 PhysicalDevice& outBetterDevice);
        static constexpr const char* s_validationLayerNames[] = {"VK_LAYER_KHRONOS_validation"};
        // VK_KHR_image_format_list: lets MUTABLE_FORMAT images declare their exact view-format
        // set so the driver can keep bandwidth compression (see CreateLogicalDeviceAndQueues).
        Bool m_imageFormatListExtensionEnabled = false;

        static constexpr const char* s_deviceExtensionNames[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
        static Bool CheckValidationLayerSupport();

        static VKAPI_ATTR VkBool32 VKAPI_CALL DebugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
                                                            VkDebugUtilsMessageTypeFlagsEXT messageType,
                                                            const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
                                                            void* pUserData);
    };
} // namespace MobileGL::MG_Backend::DirectVulkan
