mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-08-01 18:29:24 -04:00
VkContext::shutdown deliberately skips vmaDestroyAllocator, because walking every allocation costs seconds with thousands of loaded textures and models, and the driver reclaims device memory anyway. The cost is that everything the caches still hold is reported one object at a time at vkDestroyDevice: ninety thousand errors in a single run, which buries any real problem the layers find — the rendering faults fixed earlier today were needles in exactly this haystack. Keep the fast path for players and destroy the allocator properly when the layers are active. A few seconds on the way out buys a validation signal that can be read, and a genuine leak now stands out instead of hiding in the noise.
316 lines
13 KiB
C++
316 lines
13 KiB
C++
#pragma once
|
|
|
|
#include "rendering/vk_utils.hpp"
|
|
#include <vulkan/vulkan.h>
|
|
#include <vk_mem_alloc.h>
|
|
#include <VkBootstrap.h>
|
|
#include <SDL2/SDL.h>
|
|
#include <vector>
|
|
#include <functional>
|
|
#include <cstdint>
|
|
#include <unordered_map>
|
|
#include <mutex>
|
|
|
|
namespace wowee {
|
|
namespace rendering {
|
|
|
|
static constexpr uint32_t MAX_FRAMES_IN_FLIGHT = 2;
|
|
|
|
struct FrameData {
|
|
VkCommandPool commandPool = VK_NULL_HANDLE;
|
|
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
|
VkFence inFlightFence = VK_NULL_HANDLE;
|
|
};
|
|
|
|
class VkContext {
|
|
public:
|
|
VkContext() = default;
|
|
~VkContext();
|
|
|
|
VkContext(const VkContext&) = delete;
|
|
VkContext& operator=(const VkContext&) = delete;
|
|
|
|
[[nodiscard]] bool initialize(SDL_Window* window);
|
|
void shutdown();
|
|
|
|
// Swapchain management
|
|
[[nodiscard]] bool recreateSwapchain(int width, int height);
|
|
|
|
// Frame operations
|
|
VkCommandBuffer beginFrame(uint32_t& imageIndex);
|
|
void endFrame(VkCommandBuffer cmd, uint32_t imageIndex);
|
|
|
|
// Single-time command buffer helpers
|
|
VkCommandBuffer beginSingleTimeCommands();
|
|
void endSingleTimeCommands(VkCommandBuffer cmd);
|
|
|
|
// Immediate submit for one-off GPU work (descriptor pool creation, etc.)
|
|
void immediateSubmit(std::function<void(VkCommandBuffer cmd)>&& function);
|
|
|
|
// Batch upload mode: records multiple upload commands into a single
|
|
// command buffer, then submits with ONE fence wait instead of one per upload.
|
|
void beginUploadBatch();
|
|
void endUploadBatch(); // Async: submits but does NOT wait for fence
|
|
void endUploadBatchSync(); // Sync: submits and waits (for load screens)
|
|
bool isInUploadBatch() const { return inUploadBatch_; }
|
|
void deferStagingCleanup(AllocatedBuffer staging);
|
|
void pollUploadBatches(); // Check completed async uploads, free staging buffers
|
|
void waitAllUploads(); // Block until all in-flight uploads complete
|
|
|
|
// Defer resource destruction until it is safe with multiple frames in flight.
|
|
//
|
|
// This queues work to run after the fence for the *current frame slot* has
|
|
// signaled the next time we enter beginFrame() for that slot (i.e. after
|
|
// MAX_FRAMES_IN_FLIGHT submissions). Use this for resources that may still
|
|
// be referenced by command buffers submitted in the previous frame(s),
|
|
// such as descriptor sets and buffers freed during streaming/unload.
|
|
void deferAfterFrameFence(std::function<void()>&& fn);
|
|
// Like deferAfterFrameFence, but waits until ALL in-flight frame slots have
|
|
// been fenced — safe for shared resources bound by multiple frames' command
|
|
// buffers (material descriptor sets, vertex/index buffers, etc.).
|
|
void deferAfterAllFrameFences(std::function<void()>&& fn);
|
|
|
|
// Accessors
|
|
VkInstance getInstance() const { return instance; }
|
|
VkPhysicalDevice getPhysicalDevice() const { return physicalDevice; }
|
|
VkDevice getDevice() const { return device; }
|
|
uint32_t getGpuVendorId() const { return gpuVendorId_; }
|
|
const char* getGpuName() const { return gpuName_; }
|
|
bool isAmdGpu() const { return gpuVendorId_ == 0x1002; }
|
|
bool isNvidiaGpu() const { return gpuVendorId_ == 0x10DE; }
|
|
VkQueue getGraphicsQueue() const { return graphicsQueue; }
|
|
uint32_t getGraphicsQueueFamily() const { return graphicsQueueFamily; }
|
|
bool hasDedicatedTransferQueue() const { return hasDedicatedTransfer_; }
|
|
VmaAllocator getAllocator() const { return allocator; }
|
|
VkSurfaceKHR getSurface() const { return surface; }
|
|
VkPipelineCache getPipelineCache() const { return pipelineCache_; }
|
|
|
|
VkSwapchainKHR getSwapchain() const { return swapchain; }
|
|
VkFormat getSwapchainFormat() const { return swapchainFormat; }
|
|
VkExtent2D getSwapchainExtent() const { return swapchainExtent; }
|
|
const std::vector<VkImageView>& getSwapchainImageViews() const { return swapchainImageViews; }
|
|
const std::vector<VkImage>& getSwapchainImages() const { return swapchainImages; }
|
|
uint32_t getSwapchainImageCount() const { return static_cast<uint32_t>(swapchainImages.size()); }
|
|
|
|
uint32_t getCurrentFrame() const { return currentFrame; }
|
|
const FrameData& getCurrentFrameData() const { return frames[currentFrame]; }
|
|
|
|
// For ImGui
|
|
VkRenderPass getImGuiRenderPass() const { return imguiRenderPass; }
|
|
// Single-sampled, colour-only pass that loads the swapchain. The UI draws
|
|
// here, after the scene has resolved and after water refraction has copied
|
|
// it, so the capture never contains the UI.
|
|
VkRenderPass getOverlayRenderPass() const { return overlayRenderPass; }
|
|
// The same pass but clearing, for screens that draw the UI with no scene
|
|
// behind it. Shares getOverlayFramebuffers().
|
|
VkRenderPass getOverlayClearRenderPass() const { return overlayClearRenderPass; }
|
|
const std::vector<VkFramebuffer>& getOverlayFramebuffers() const { return overlayFramebuffers; }
|
|
// Compatible with getImGuiRenderPass(), but loads the scene instead of
|
|
// clearing it, so drawing can continue into the same framebuffer after the
|
|
// pass has been closed for a copy. Null under MSAA.
|
|
VkRenderPass getSceneContinueRenderPass() const { return sceneContinueRenderPass; }
|
|
VkDescriptorPool getImGuiDescriptorPool() const { return imguiDescriptorPool; }
|
|
const std::vector<VkFramebuffer>& getSwapchainFramebuffers() const { return swapchainFramebuffers; }
|
|
|
|
bool isSwapchainDirty() const { return swapchainDirty; }
|
|
void markSwapchainDirty() { swapchainDirty = true; }
|
|
|
|
// VSync (present mode)
|
|
bool isVsyncEnabled() const { return vsync_; }
|
|
void setVsync(bool enabled) { vsync_ = enabled; }
|
|
|
|
bool isDeviceLost() const { return deviceLost_; }
|
|
|
|
// MSAA
|
|
VkSampleCountFlagBits getMsaaSamples() const { return msaaSamples_; }
|
|
void setMsaaSamples(VkSampleCountFlagBits samples);
|
|
VkSampleCountFlagBits getMaxUsableSampleCount() const;
|
|
VkImage getDepthImage() const { return depthImage; }
|
|
VkImage getDepthCopySourceImage() const {
|
|
return (depthResolveImage != VK_NULL_HANDLE) ? depthResolveImage : depthImage;
|
|
}
|
|
bool isDepthCopySourceMsaa() const {
|
|
return (depthResolveImage == VK_NULL_HANDLE) && (msaaSamples_ > VK_SAMPLE_COUNT_1_BIT);
|
|
}
|
|
VkFormat getDepthFormat() const { return depthFormat; }
|
|
VkImageView getDepthResolveImageView() const { return depthResolveImageView; }
|
|
VkImageView getDepthImageView() const { return depthImageView; }
|
|
|
|
// Sampler cache: returns a shared VkSampler matching the given create info.
|
|
// Callers must NOT destroy the returned sampler — it is owned by VkContext.
|
|
// Automatically clamps anisotropy if the device doesn't support it.
|
|
VkSampler getOrCreateSampler(const VkSamplerCreateInfo& info);
|
|
|
|
// Whether the physical device supports sampler anisotropy.
|
|
bool isSamplerAnisotropySupported() const { return samplerAnisotropySupported_; }
|
|
|
|
// Global sampler cache accessor (set during VkContext::initialize, cleared on shutdown).
|
|
// Used by VkTexture and other code that only has a VkDevice handle.
|
|
static VkContext* globalInstance() { return sInstance_; }
|
|
|
|
// UI texture upload: creates a Vulkan texture from RGBA data and returns
|
|
// a VkDescriptorSet suitable for use as ImTextureID.
|
|
// The caller does NOT need to free the result — resources are tracked and
|
|
// cleaned up when the VkContext is destroyed.
|
|
VkDescriptorSet uploadImGuiTexture(const uint8_t* rgba, int width, int height);
|
|
|
|
private:
|
|
bool createInstance(SDL_Window* window);
|
|
bool createSurface(SDL_Window* window);
|
|
bool selectPhysicalDevice();
|
|
bool createLogicalDevice();
|
|
bool createAllocator();
|
|
bool createSwapchain(int width, int height);
|
|
void destroySwapchain();
|
|
bool createCommandPools();
|
|
bool createSyncObjects();
|
|
bool createPipelineCache();
|
|
void savePipelineCache();
|
|
bool createImGuiResources();
|
|
void destroyImGuiResources();
|
|
|
|
// vk-bootstrap objects (kept alive for swapchain recreation etc.)
|
|
vkb::Instance vkbInstance_;
|
|
vkb::PhysicalDevice vkbPhysicalDevice_;
|
|
|
|
VkInstance instance = VK_NULL_HANDLE;
|
|
VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE;
|
|
VkSurfaceKHR surface = VK_NULL_HANDLE;
|
|
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
|
|
VkDevice device = VK_NULL_HANDLE;
|
|
VmaAllocator allocator = VK_NULL_HANDLE;
|
|
|
|
// Pipeline cache (persisted to disk for faster startup)
|
|
VkPipelineCache pipelineCache_ = VK_NULL_HANDLE;
|
|
uint32_t gpuVendorId_ = 0;
|
|
char gpuName_[256] = {};
|
|
|
|
VkQueue graphicsQueue = VK_NULL_HANDLE;
|
|
VkQueue presentQueue = VK_NULL_HANDLE;
|
|
uint32_t graphicsQueueFamily = 0;
|
|
uint32_t presentQueueFamily = 0;
|
|
|
|
// Dedicated transfer queue (second queue from same graphics family)
|
|
VkQueue transferQueue_ = VK_NULL_HANDLE;
|
|
VkCommandPool transferCommandPool_ = VK_NULL_HANDLE;
|
|
bool hasDedicatedTransfer_ = false;
|
|
uint32_t graphicsQueueFamilyQueueCount_ = 1; // queried in selectPhysicalDevice
|
|
|
|
// Swapchain
|
|
VkSwapchainKHR swapchain = VK_NULL_HANDLE;
|
|
VkFormat swapchainFormat = VK_FORMAT_UNDEFINED;
|
|
VkExtent2D swapchainExtent = {0, 0};
|
|
std::vector<VkImage> swapchainImages;
|
|
std::vector<VkImageView> swapchainImageViews;
|
|
std::vector<VkFramebuffer> swapchainFramebuffers;
|
|
bool swapchainDirty = false;
|
|
bool deviceLost_ = false;
|
|
bool vsync_ = true;
|
|
|
|
// Per-frame resources
|
|
FrameData frames[MAX_FRAMES_IN_FLIGHT];
|
|
uint32_t currentFrame = 0;
|
|
|
|
// Per-swapchain-image semaphores (avoids reuse while presentation engine holds them)
|
|
std::vector<VkSemaphore> imageAcquiredSemaphores_; // [swapchainImageCount], per-image
|
|
std::vector<VkSemaphore> renderFinishedSemaphores_; // [swapchainImageCount], per-image
|
|
VkSemaphore nextAcquireSemaphore_ = VK_NULL_HANDLE; // free semaphore for next acquire
|
|
VkSemaphore currentAcquireSemaphore_ = VK_NULL_HANDLE; // the one used for the current frame
|
|
|
|
// Immediate submit resources
|
|
VkCommandPool immCommandPool = VK_NULL_HANDLE;
|
|
VkFence immFence = VK_NULL_HANDLE;
|
|
// Cached, reusable cmd buffer for beginSingleTimeCommands. Pool was created
|
|
// with RESET_COMMAND_BUFFER_BIT so we can reset and reuse instead of
|
|
// round-tripping through vkAllocateCommandBuffers + vkFreeCommandBuffers
|
|
// every immediate submit (the M2 frustum-cull dispatch fires this per frame).
|
|
VkCommandBuffer immCmdBuf_ = VK_NULL_HANDLE;
|
|
|
|
// Batch upload state (nesting-safe via depth counter)
|
|
int uploadBatchDepth_ = 0;
|
|
bool inUploadBatch_ = false;
|
|
VkCommandBuffer batchCmd_ = VK_NULL_HANDLE;
|
|
std::vector<AllocatedBuffer> batchStagingBuffers_;
|
|
|
|
// Async upload: in-flight batches awaiting GPU completion
|
|
struct InFlightBatch {
|
|
VkFence fence = VK_NULL_HANDLE;
|
|
VkCommandBuffer cmd = VK_NULL_HANDLE;
|
|
std::vector<AllocatedBuffer> stagingBuffers;
|
|
};
|
|
std::vector<InFlightBatch> inFlightBatches_;
|
|
|
|
void runDeferredCleanup(uint32_t frameIndex);
|
|
std::vector<std::function<void()>> deferredCleanup_[MAX_FRAMES_IN_FLIGHT];
|
|
|
|
// Depth buffer (shared across all framebuffers)
|
|
VkImage depthImage = VK_NULL_HANDLE;
|
|
VkImageView depthImageView = VK_NULL_HANDLE;
|
|
VmaAllocation depthAllocation = VK_NULL_HANDLE;
|
|
VkFormat depthFormat = VK_FORMAT_D32_SFLOAT;
|
|
|
|
bool createDepthBuffer();
|
|
void destroyDepthBuffer();
|
|
|
|
// MSAA resources
|
|
VkSampleCountFlagBits msaaSamples_ = VK_SAMPLE_COUNT_1_BIT;
|
|
VkImage msaaColorImage_ = VK_NULL_HANDLE;
|
|
VkImageView msaaColorView_ = VK_NULL_HANDLE;
|
|
VmaAllocation msaaColorAllocation_ = VK_NULL_HANDLE;
|
|
|
|
bool createMsaaColorImage();
|
|
void destroyMsaaColorImage();
|
|
bool createDepthResolveImage();
|
|
void destroyDepthResolveImage();
|
|
|
|
// Actual Vulkan API version the instance was created with (gates core 1.2 calls)
|
|
uint32_t instanceApiVersion_ = VK_API_VERSION_1_1;
|
|
|
|
// MSAA depth resolve support (for sampling/copying resolved depth)
|
|
bool depthResolveSupported_ = false;
|
|
VkResolveModeFlagBits depthResolveMode_ = VK_RESOLVE_MODE_NONE;
|
|
VkImage depthResolveImage = VK_NULL_HANDLE;
|
|
VkImageView depthResolveImageView = VK_NULL_HANDLE;
|
|
VmaAllocation depthResolveAllocation = VK_NULL_HANDLE;
|
|
|
|
// ImGui resources
|
|
VkRenderPass imguiRenderPass = VK_NULL_HANDLE;
|
|
VkRenderPass overlayRenderPass = VK_NULL_HANDLE;
|
|
VkRenderPass overlayClearRenderPass = VK_NULL_HANDLE;
|
|
std::vector<VkFramebuffer> overlayFramebuffers;
|
|
VkRenderPass sceneContinueRenderPass = VK_NULL_HANDLE;
|
|
bool createOverlayRenderPass();
|
|
bool createSceneContinueRenderPass();
|
|
void destroyOverlayRenderPass();
|
|
VkDescriptorPool imguiDescriptorPool = VK_NULL_HANDLE;
|
|
|
|
// Shared sampler for UI textures (created on first uploadImGuiTexture call)
|
|
VkSampler uiTextureSampler_ = VK_NULL_HANDLE;
|
|
|
|
// Tracked UI textures for cleanup
|
|
struct UiTexture {
|
|
VkImage image;
|
|
VkDeviceMemory memory;
|
|
VkImageView view;
|
|
};
|
|
std::vector<UiTexture> uiTextures_;
|
|
|
|
// Sampler cache — deduplicates VkSamplers by configuration hash.
|
|
std::mutex samplerCacheMutex_;
|
|
std::unordered_map<uint64_t, VkSampler> samplerCache_;
|
|
bool samplerAnisotropySupported_ = false;
|
|
|
|
static VkContext* sInstance_;
|
|
|
|
#ifndef NDEBUG
|
|
bool enableValidation = true;
|
|
#else
|
|
bool enableValidation = false;
|
|
#endif
|
|
// Whether the layers actually came up this run, including via the env var.
|
|
bool validationActive_ = false;
|
|
};
|
|
|
|
} // namespace rendering
|
|
} // namespace wowee
|