mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-09 17:52:08 -10:00
Under Vulkan a mission's $Environment Map was never uploaded: the irradiance and lighting passes only use ENVMAP when its texture slot already has an image view, and nothing created one for a bitmap that was only bm_load'ed. OpenGL uploads it the first time it binds it as a cubemap. Add VulkanTextureManager::ensureCubemapUploaded(), called from both passes. It locks the map the way OpenGL does for TCACHE_TYPE_CUBEMAP. The lock uploads through bm_data(), which now sends uncompressed DDS cubemaps (bm_is_compressed() returns 0 for them; comp_type identifies them) to uploadCubemap() as DXT ones already went. If the slot still holds no cube after the lock, because the data was already resident, it uploads it directly. A map that cannot be used is remembered by bitmap signature (handles are reused across missions) and warned about once in debug builds, and the irradiance pass is skipped instead of sampling a non-cube view. uploadCubemap() also assumed uncompressed cubemaps had one mip level and read the six faces back to back. DDS stores face0[mip0..mipN], face1[...], so faces 1-5 read the tail of face 0's mip chain. It now uses the file's mip count, expands 24-bit data over the whole chain, generates a chain for a one-level uncompressed cubemap as OpenGL does (the lighting pass samples by roughness), copies mip 0 for every array layer, cleans up the image when the staging map fails, and clears isCubemap as soon as it retires the old image. Co-authored-by: Dustinspace217 <[email protected]>
459 lines
16 KiB
C++
459 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 Upload a cubemap bitmap as a cubemap if it has not been uploaded yet
|
|
* @param[in] handle bitmap handle of a DDS cubemap (compressed or not) or a cubemap render target
|
|
* @return true if the handle now has a cubemap image view
|
|
*/
|
|
bool ensureCubemapUploaded(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 BC- and ETC2-compressed textures (block-decode via bcdec/etcdec)
|
|
* and uncompressed textures (per-format channel expand/convert); 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;
|
|
uint m_failedCubemapSignature = 0;
|
|
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
|
|
|