Files
fs2open.github.com/code/graphics/vulkan/VulkanTexture.h
T
the-e 0d64f7f237 Add load-variant render pass support for Vulkan render targets
Introduce load-variant render passes to Vulkan textures, enabling content preservation during mid-frame readbacks. Changes include creating `renderPassLoad`, flushing command buffers for readbacks, and resuming rendering into targets with `loadOp=eLoad` to avoid clearing previously rendered content.
2026-07-22 19:57:10 +02:00

451 lines
16 KiB
C++

#pragma once
#include "globalincs/pstypes.h"
#include "VulkanMemory.h"
#define BMPMAN_INTERNAL
#include "bmpman/bm_internal.h"
#include <array>
#include <vulkan/vulkan.hpp>
namespace graphics::vulkan {
/**
* @brief Vulkan-specific texture data stored in bitmap slots
*
* Extends gr_bitmap_info to store Vulkan image handles and metadata.
* This is the Vulkan equivalent of tcache_slot_opengl.
*/
class tcache_slot_vulkan : public gr_bitmap_info {
public:
vk::Image image;
vk::ImageView imageView;
VulkanAllocation allocation;
vk::Format format = vk::Format::eUndefined;
vk::ImageLayout currentLayout = vk::ImageLayout::eUndefined;
uint32_t width = 0;
uint32_t height = 0;
uint32_t mipLevels = 1;
uint32_t arrayLayers = 1;
int bpp = 0;
int bitmapHandle = -1;
uint32_t arrayIndex = 0;
bool used = false;
// For render targets
vk::Framebuffer framebuffer;
vk::ImageView framebufferView; // Single-mip view for framebuffer (when mipLevels > 1)
vk::RenderPass renderPass; // Render pass compatible with this target (color loadOp=eClear)
// Load-variant render pass (color loadOp=eLoad) used to RESUME rendering into this
// target after a mid-frame readback (gr.screenToBlob) without wiping already-drawn
// content. Only meaningful for non-cubemap targets.
vk::RenderPass renderPassLoad;
bool isRenderTarget = false;
// Optional depth/stencil attachment (BMP_FLAG_RENDER_TARGET_DEPTH_ATTACHMENT).
// Without this, models drawn into the target get no z-testing: the Vulkan spec
// ignores the pipeline's depth/stencil state when the subpass has no depth
// attachment, so depth-tested geometry renders in submission order.
vk::Image depthImage;
vk::ImageView depthImageView;
VulkanAllocation depthAllocation;
// 3D texture support
bool is3D = false;
uint32_t depth = 1;
// Cubemap support
bool isCubemap = false;
std::array<vk::ImageView, 6> cubeFaceViews = {}; // Per-face 2D views for render-to-cubemap
std::array<vk::Framebuffer, 6> cubeFaceFramebuffers = {}; // Per-face framebuffers for render-to-cubemap
// Note: the cube-sampling view (viewType=eCube, layerCount=6) lives in imageView
// for both the render-target and static-file cubemap paths — there is no separate
// cube view member.
// Texture scaling (for non-power-of-two handling)
float uScale = 1.0f;
float vScale = 1.0f;
tcache_slot_vulkan() { reset(); }
~tcache_slot_vulkan() override = default;
void reset();
};
/**
* @brief Manages Vulkan textures, samplers, and render targets
*/
class VulkanTextureManager {
public:
VulkanTextureManager();
~VulkanTextureManager();
// Non-copyable
VulkanTextureManager(const VulkanTextureManager&) = delete;
VulkanTextureManager& operator=(const VulkanTextureManager&) = delete;
/**
* @brief Initialize the texture manager
*/
bool init(vk::Device device, vk::PhysicalDevice physicalDevice,
VulkanMemoryManager* memoryManager,
vk::CommandPool commandPool, vk::Queue graphicsQueue);
/**
* @brief Shutdown and free all textures
*/
void shutdown();
/**
* @brief Flush all GPU texture resources from bitmap slots
*
* Called between missions (from vulkan_bm_page_in_start) to release
* VkImage/VMA allocations for textures that won't be needed.
* Textures are re-uploaded on demand when accessed again.
*/
void flushTextures() const;
// Bitmap management functions (implement gr_screen function pointers)
/**
* @brief Initialize a bitmap slot for Vulkan
*/
void bm_init(bitmap_slot* slot) const;
/**
* @brief Create Vulkan resources for a bitmap slot
*/
void bm_create(bitmap_slot* slot) const;
/**
* @brief Free Vulkan resources for a bitmap slot
*/
void bm_free_data(bitmap_slot* slot, bool release) const;
/**
* @brief Upload bitmap data to GPU
* @param compType Compression type (DDS_DXT1/3/5, DDS_BC7) or 0 for uncompressed
*/
bool bm_data(int handle, bitmap* bm, int compType);
/**
* @brief Create a render target
*/
int bm_make_render_target(int handle, int* width, int* height, int* bpp, int* mm_lvl, int flags);
/**
* @brief Set active render target
*/
int bm_set_render_target(int handle, int face);
/**
* @brief Update texture data
*/
void update_texture(int bitmap_handle, int bpp, const ubyte* data, int width, int height);
/**
* @brief Read texture data back to CPU
*
* Mirrors gr_opengl_get_bitmap_from_texture(): returns a newly
* vm_malloc()'d buffer of tightly packed RGB/RGBA8 pixels (caller frees
* with vm_free), sized from the texture's actual GPU dimensions written
* to width_out/height_out. Returns nullptr on failure.
*/
ubyte* get_bitmap_from_texture(int bitmap_num, int* width_out, int* height_out);
// Sampler management
/**
* @brief Get or create a sampler with specified parameters
*/
vk::Sampler getSampler(vk::Filter magFilter, vk::Filter minFilter,
vk::SamplerAddressMode addressMode,
bool enableAnisotropy, float maxAnisotropy,
bool enableMipmaps);
/**
* @brief Get a ready-to-use DescriptorImageInfo for the 2D fallback texture
*/
vk::DescriptorImageInfo getFallbackTextureInfo2D();
/**
* @brief Get a ready-to-use DescriptorImageInfo for the cubemap fallback texture
*/
vk::DescriptorImageInfo getFallbackTextureInfoCube();
/**
* @brief Get a ready-to-use DescriptorImageInfo for the 2D array fallback texture
*/
vk::DescriptorImageInfo getFallbackTextureInfo2DArray();
/**
* @brief Get a ready-to-use DescriptorImageInfo for the 3D fallback texture
*/
vk::DescriptorImageInfo getFallbackTextureInfo3D();
// Texture access
/**
* @brief Get texture slot data
*/
tcache_slot_vulkan* getTextureSlot(int handle);
/**
* @brief Bitmap handle of the render target currently bound via bm_set_render_target,
* or -1 when rendering to the default framebuffer. Used by gr.screenToBlob so a
* capture reads the active render target instead of the on-screen framebuffer.
*/
int getCurrentRenderTarget() const { return m_currentRenderTarget; }
/**
* @brief Create a static, single-mip, single-layer 2D texture from CPU pixel data
*
* For small fixed lookup tables (e.g. SMAA area/search textures) that aren't
* part of the regular bitmap/tcache system. Uploads synchronously.
*/
bool createStaticTexture2D(uint32_t width, uint32_t height, vk::Format format,
const void* pixelData, size_t dataSize, const char* debugName,
vk::Image& outImage, vk::ImageView& outView, VulkanAllocation& outAlloc);
// Utility functions
/**
* @brief Convert FSO bitmap format to Vulkan format
*/
static vk::Format bppToVkFormat(int bpp, bool compressed = false, int compressionType = 0);
/**
* @brief Transition image layout
*/
void transitionImageLayout(vk::Image image, vk::Format format,
vk::ImageLayout oldLayout, vk::ImageLayout newLayout,
uint32_t mipLevels = 1, uint32_t arrayLayers = 1);
/**
* @brief Called at start of frame
*/
void frameStart();
private:
/**
* @brief Create a Vulkan image
* @param cubemap If true, sets eCubeCompatible flag (requires arrayLayers=6)
* @param imageType Vulkan image type (e2D, e3D, etc.)
*/
bool createImage(uint32_t width, uint32_t height, uint32_t mipLevels,
vk::Format format, vk::ImageTiling tiling,
vk::ImageUsageFlags usage, MemoryUsage memUsage,
vk::Image& image, VulkanAllocation& allocation,
uint32_t arrayLayers = 1, bool cubemap = false,
uint32_t imageDepth = 1,
vk::ImageType imageType = vk::ImageType::e2D);
enum class ImageViewType { Array2D, Plain2D, Cube, Volume3D };
/**
* @brief Create an image view
* @param viewType Controls view type: Array2D=sampler2DArray, Plain2D=sampler2D, Cube=samplerCube
*/
vk::ImageView createImageView(vk::Image image, vk::Format format,
vk::ImageAspectFlags aspectFlags,
uint32_t mipLevels,
ImageViewType viewType = ImageViewType::Array2D,
uint32_t layerCount = 1,
uint32_t baseArrayLayer = 0);
/**
* @brief Create a 1x1 white fallback texture (image + view + upload)
*/
bool createFallbackTexture(vk::Image& outImage, VulkanAllocation& outAlloc,
vk::ImageView& outView, ImageViewType viewType,
uint32_t arrayLayers = 1, bool cubemap = false,
vk::ImageType imageType = vk::ImageType::e2D);
/**
* @brief Begin single-time command buffer
*/
vk::CommandBuffer beginSingleTimeCommands();
/**
* @brief End and submit single-time command buffer (synchronous, blocks on waitIdle)
*/
void endSingleTimeCommands(vk::CommandBuffer commandBuffer);
/**
* @brief Create a host-visible staging buffer (eTransferSrc) sized for an upload
*
* Creates the buffer and allocates CpuOnly memory for it. On failure the
* buffer is destroyed and false is returned (outBuffer left null). Callers
* remain responsible for any image/view they allocated beforehand.
*/
bool createStagingBuffer(size_t size, vk::Buffer& outBuffer, VulkanAllocation& outAllocation);
/**
* @brief Record layout transitions and buffer-to-image copy into a command buffer
*/
static void recordUploadCommands(vk::CommandBuffer cmd, vk::Image image, vk::Buffer stagingBuffer,
vk::Format format, uint32_t width, uint32_t height,
uint32_t mipLevels, vk::ImageLayout oldLayout,
bool generateMips = false,
const SCP_vector<vk::BufferImageCopy>& regions = {},
uint32_t arrayLayers = 1);
/**
* @brief Submit an upload command buffer asynchronously and defer resource cleanup
*
* Submits without waitIdle. Queues staging buffer and command buffer for
* deferred destruction/free after enough frames have elapsed.
*/
void submitUploadAsync(vk::CommandBuffer cmd, vk::Buffer stagingBuffer,
VulkanAllocation stagingAllocation);
/**
* @brief Free command buffers whose GPU work has completed
*/
void processPendingCommandBuffers();
/**
* @brief Calculate number of mipmap levels
*/
static uint32_t calculateMipLevels(uint32_t width, uint32_t height);
/**
* @brief Upload all frames of an animation as layers of a single texture array
*/
bool uploadAnimationFrames(int handle, bitmap* bm, int compType,
int baseFrame, int numFrames);
/**
* @brief Upload a cubemap DDS texture (6 faces) as a single cubemap image
*/
bool uploadCubemap(int handle, bitmap* bm, int compType);
/**
* @brief Upload a 3D texture (volumetric data) as a single 3D image
*/
bool upload3DTexture(int handle, bitmap* bm, int texDepth);
/**
* @brief Upload a plain 2D texture (the common case once animation/cubemap/3D are ruled out)
*/
bool uploadTexture2D(int handle, bitmap* bm, int compType);
/**
* @brief Ref-count release for a shared animation texture-array slot
*
* Shared animation frames all point at one array VkImage; the slot for the
* triggering frame must not destroy that image while other frames still
* reference it. Marks this slot unused and walks the frame range:
* - non-array slot (arrayLayers <= 1) -> returns true (caller destroys normally)
* - shared array, another frame still in use -> detaches this slot in place
* (image/view/allocation cleared + reset),
* returns false
* - shared array, this was the last live reference -> returns true (caller destroys the shared image)
*
* The caller keeps ownership of the actual destruction sequence (it differs
* between flushTextures and bm_free_data), this only makes the shared-vs-last
* decision.
*/
static bool releaseAnimationSlotRef(tcache_slot_vulkan* ts);
/**
* @brief Decode a get_bitmap_from_texture() readback buffer into data_out
*
* Handles both the BC-compressed (block-decode via bcdec) and uncompressed
* (per-format channel expand/convert) cases; data_out is tightly packed
* w*h pixels at outChannels (3 or 4) bytes each.
*/
static void decodeReadbackBuffer(const void* mapped, vk::Format format, bool isCompressed,
uint32_t w, uint32_t h, uint32_t blockW, uint32_t blockH,
int blockSize, int outChannels, void* data_out);
// Guard flag to prevent recursion when bm_lock calls bm_data during animation upload
bool m_uploadingAnimation = false;
// First-N debug-log counters: members rather than function-local statics
// so they reset when the manager is recreated on a renderer restart, and aren't
// shared across a hypothetical second instance. Purely gate nprintf spam.
int m_bmDataLogCount = 0; // bm_data entry trace
int m_uploadFmtLogCount = 0; // uploadTexture2D format trace
int m_reuploadLogCount = 0; // uploadTexture2D resident-texture re-upload notice
// Deferred command buffer free list
struct PendingCommandBuffer {
vk::CommandBuffer cb;
uint32_t framesRemaining;
};
SCP_vector<PendingCommandBuffer> m_pendingCommandBuffers;
vk::Device m_device;
vk::PhysicalDevice m_physicalDevice;
VulkanMemoryManager* m_memoryManager = nullptr;
vk::CommandPool m_commandPool;
vk::Queue m_graphicsQueue;
// Cached samplers (key: packed sampler state)
SCP_unordered_map<uint64_t, vk::Sampler> m_samplerCache;
vk::Sampler m_defaultSampler;
// Fallback 1x1 white textures for unbound texture slots
vk::Image m_fallback2DArrayTexture;
vk::ImageView m_fallback2DArrayView; // 2D_ARRAY view (for material texture arrays)
VulkanAllocation m_fallback2DArrayAllocation;
vk::Image m_fallbackTexture2D;
vk::ImageView m_fallbackTextureView2D; // 2D view (for post-processing sampler2D)
VulkanAllocation m_fallbackTexture2DAllocation;
// Fallback 1x1x6 white cubemap for unbound samplerCube slots
vk::Image m_fallbackCubeTexture;
vk::ImageView m_fallbackCubeView; // Cube view (for samplerCube)
VulkanAllocation m_fallbackCubeAllocation;
// Fallback 1x1x1 white 3D texture for unbound sampler3D slots
vk::Image m_fallback3DTexture;
vk::ImageView m_fallback3DView; // 3D view (for sampler3D)
VulkanAllocation m_fallback3DAllocation;
// Device limits
uint32_t m_maxTextureSize = 4096;
float m_maxAnisotropy = 1.0f;
// Current render target state
int m_currentRenderTarget = -1;
bool m_initialized = false;
};
// Global texture manager instance
VulkanTextureManager* getTextureManager();
void setTextureManager(VulkanTextureManager* manager);
/**
* @brief Generate mip levels 1..mipLevels-1 via blit chain from the previous level.
*
* Prerequisite: mip 0 must already be in eTransferSrcOptimal.
* Result: ALL mip levels transitioned to eShaderReadOnlyOptimal.
*/
void vulkan_generate_mipmap_chain(vk::CommandBuffer cmd, vk::Image image,
uint32_t width, uint32_t height,
uint32_t mipLevels, uint32_t arrayLayers = 1);
// ========== gr_screen function pointer implementations ==========
int vulkan_preload(int bitmap_num, int is_aabitmap);
void vulkan_bm_create(bitmap_slot* slot);
void vulkan_bm_free_data(bitmap_slot* slot, bool release);
void vulkan_bm_init(bitmap_slot* slot);
bool vulkan_bm_data(int handle, bitmap* bm);
void vulkan_bm_page_in_start();
int vulkan_bm_make_render_target(int handle, int* width, int* height, int* bpp, int* mm_lvl, int flags);
int vulkan_bm_set_render_target(int handle, int face);
void vulkan_update_texture(int bitmap_handle, int bpp, const ubyte* data, int width, int height);
ubyte* vulkan_get_bitmap_from_texture(int bitmap_num, int* width_out, int* height_out);
} // namespace graphics::vulkan