mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-09 04:49:39 -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]>
2914 lines
101 KiB
C++
2914 lines
101 KiB
C++
#include "VulkanTexture.h"
|
||
|
||
#include <algorithm>
|
||
#include "VulkanBarrier.h"
|
||
#include "VulkanBuffer.h"
|
||
#include "VulkanDeletionQueue.h"
|
||
#include "VulkanRenderer.h"
|
||
#include "gr_vulkan.h"
|
||
|
||
#include "graphics/util/pixel_swizzle.h"
|
||
#include "bmpman/bm_internal.h"
|
||
#include "bmpman/bmpman.h"
|
||
#include "ddsutils/ddsutils.h"
|
||
#include "ddsutils/bcdec.h"
|
||
#include "globalincs/systemvars.h"
|
||
#include "ktxutils/ktxutils.h"
|
||
#include "ktxutils/etcdec.h"
|
||
|
||
namespace graphics::vulkan {
|
||
|
||
namespace {
|
||
VulkanTextureManager* g_textureManager = nullptr;
|
||
|
||
// Geometry/format description shared by every texture-upload path (single 2D,
|
||
// animation array, cubemap). Captures exactly what the staging-size, copy-region
|
||
// and per-mip math need, so those calculations live in one place instead of
|
||
// being copy-pasted into each upload function.
|
||
struct TextureUploadLayout {
|
||
uint32_t width = 0;
|
||
uint32_t height = 0;
|
||
uint32_t mipLevels = 1;
|
||
bool isCompressed = false;
|
||
size_t blockSize = 0; // compressed block size (DXT1=8, others=16)
|
||
size_t dstBytesPerPixel = 0; // uncompressed destination bpp (24bpp stored as 4)
|
||
};
|
||
|
||
// Byte size of a single mip level for one layer.
|
||
size_t mipLevelSize(const TextureUploadLayout& l, uint32_t mipW, uint32_t mipH)
|
||
{
|
||
if (l.isCompressed) {
|
||
// Works for DDS and KTX
|
||
return dds_compressed_mip_size(static_cast<int>(mipW), static_cast<int>(mipH),
|
||
static_cast<int>(l.blockSize));
|
||
}
|
||
return static_cast<size_t>(mipW) * mipH * l.dstBytesPerPixel;
|
||
}
|
||
|
||
// Get compressed block size for DDS and KTX textures
|
||
size_t get_compressed_block_size(int compType)
|
||
{
|
||
switch (compType) {
|
||
// DDS
|
||
case DDS_DXT1:
|
||
case DDS_CUBEMAP_DXT1:
|
||
case DDS_DXT3:
|
||
case DDS_CUBEMAP_DXT3:
|
||
case DDS_DXT5:
|
||
case DDS_CUBEMAP_DXT5:
|
||
case DDS_BC7:
|
||
return dds_block_size(compType);
|
||
// KTX
|
||
case KTX_ETC2_RGB:
|
||
case KTX_ETC2_SRGB:
|
||
case KTX_ETC2_RGB_A1:
|
||
case KTX_ETC2_SRGB_A1:
|
||
case KTX_ETC2_RGBA_EAC:
|
||
case KTX_ETC2_SRGBA_EAC:
|
||
return ktx_etc_block_size(ktx_map_ktx_format_to_gl_internal(compType));
|
||
|
||
default:
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
// Total bytes occupied by one layer (all mip levels). Matches the staging
|
||
// layout produced by appendLayerCopyRegions().
|
||
size_t layerByteSize(const TextureUploadLayout& l)
|
||
{
|
||
size_t total = 0;
|
||
uint32_t mipW = l.width;
|
||
uint32_t mipH = l.height;
|
||
for (uint32_t m = 0; m < l.mipLevels; ++m) {
|
||
total += mipLevelSize(l, mipW, mipH);
|
||
mipW = std::max(1u, mipW / 2);
|
||
mipH = std::max(1u, mipH / 2);
|
||
}
|
||
return total;
|
||
}
|
||
|
||
// Append one vk::BufferImageCopy per mip level for a single array layer/face,
|
||
// starting at layerBufferOffset. Returns the number of bytes the layer occupies
|
||
// (so callers can advance their staging offset). The regions depend only on the
|
||
// layout, not on the pixel data, so this is safe to call even when a frame's
|
||
// data could not be locked.
|
||
size_t appendLayerCopyRegions(SCP_vector<vk::BufferImageCopy>& regions,
|
||
const TextureUploadLayout& l, uint32_t layerIndex, size_t layerBufferOffset)
|
||
{
|
||
uint32_t mipW = l.width;
|
||
uint32_t mipH = l.height;
|
||
size_t offset = layerBufferOffset;
|
||
for (uint32_t m = 0; m < l.mipLevels; ++m) {
|
||
vk::BufferImageCopy region;
|
||
region.bufferOffset = static_cast<vk::DeviceSize>(offset);
|
||
region.bufferRowLength = 0;
|
||
region.bufferImageHeight = 0;
|
||
region.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
|
||
region.imageSubresource.mipLevel = m;
|
||
region.imageSubresource.baseArrayLayer = layerIndex;
|
||
region.imageSubresource.layerCount = 1;
|
||
region.imageOffset = vk::Offset3D(0, 0, 0);
|
||
region.imageExtent = vk::Extent3D(mipW, mipH, 1);
|
||
regions.push_back(region);
|
||
|
||
offset += mipLevelSize(l, mipW, mipH);
|
||
mipW = std::max(1u, mipW / 2);
|
||
mipH = std::max(1u, mipH / 2);
|
||
}
|
||
return offset - layerBufferOffset;
|
||
}
|
||
|
||
} // namespace
|
||
|
||
VulkanTextureManager* getTextureManager()
|
||
{
|
||
Assertion(g_textureManager != nullptr, "Vulkan TextureManager not initialized!");
|
||
return g_textureManager;
|
||
}
|
||
|
||
void setTextureManager(VulkanTextureManager* manager)
|
||
{
|
||
g_textureManager = manager;
|
||
}
|
||
|
||
// tcache_slot_vulkan implementation
|
||
|
||
void tcache_slot_vulkan::reset()
|
||
{
|
||
image = nullptr;
|
||
imageView = nullptr;
|
||
allocation = VulkanAllocation();
|
||
format = vk::Format::eUndefined;
|
||
currentLayout = vk::ImageLayout::eUndefined;
|
||
width = 0;
|
||
height = 0;
|
||
mipLevels = 1;
|
||
arrayLayers = 1;
|
||
bpp = 0;
|
||
bitmapHandle = -1;
|
||
arrayIndex = 0;
|
||
used = false;
|
||
framebuffer = nullptr;
|
||
framebufferView = nullptr;
|
||
renderPass = nullptr;
|
||
renderPassLoad = nullptr;
|
||
isRenderTarget = false;
|
||
depthImage = nullptr;
|
||
depthImageView = nullptr;
|
||
depthAllocation = VulkanAllocation();
|
||
is3D = false;
|
||
depth = 1;
|
||
isCubemap = false;
|
||
for (auto& v : cubeFaceViews) v = nullptr;
|
||
for (auto& fb : cubeFaceFramebuffers) fb = nullptr;
|
||
uScale = 1.0f;
|
||
vScale = 1.0f;
|
||
}
|
||
|
||
// VulkanTextureManager implementation
|
||
|
||
VulkanTextureManager::VulkanTextureManager() = default;
|
||
|
||
VulkanTextureManager::~VulkanTextureManager()
|
||
{
|
||
if (m_initialized) {
|
||
shutdown();
|
||
}
|
||
}
|
||
|
||
bool VulkanTextureManager::init(vk::Device device, vk::PhysicalDevice physicalDevice,
|
||
VulkanMemoryManager* memoryManager,
|
||
vk::CommandPool commandPool, vk::Queue graphicsQueue)
|
||
{
|
||
if (m_initialized) {
|
||
nprintf(("vulkan", "VulkanTextureManager::init called when already initialized!\n"));
|
||
return false;
|
||
}
|
||
|
||
m_device = device;
|
||
m_physicalDevice = physicalDevice;
|
||
m_memoryManager = memoryManager;
|
||
m_commandPool = commandPool;
|
||
m_graphicsQueue = graphicsQueue;
|
||
|
||
// Query device limits
|
||
auto properties = physicalDevice.getProperties();
|
||
m_maxTextureSize = properties.limits.maxImageDimension2D;
|
||
m_maxAnisotropy = properties.limits.maxSamplerAnisotropy;
|
||
|
||
nprintf(("vulkan", "Vulkan Texture Manager initialized\n"));
|
||
nprintf(("vulkan", " Max texture size: %u\n", m_maxTextureSize));
|
||
nprintf(("vulkan", " Max anisotropy: %.1f\n", m_maxAnisotropy));
|
||
|
||
// Create default sampler
|
||
vk::SamplerCreateInfo samplerInfo;
|
||
samplerInfo.magFilter = vk::Filter::eLinear;
|
||
samplerInfo.minFilter = vk::Filter::eLinear;
|
||
// Use ClampToEdge by default to match OpenGL's behavior for UI/interface textures.
|
||
// OpenGL creates all textures with GL_CLAMP_TO_EDGE and only switches to GL_REPEAT
|
||
// for 3D model textures at bind time (excluding AABITMAP, INTERFACE, CUBEMAP types).
|
||
// Using eRepeat here causes visible 1-pixel seams on UI bitmaps where edge texels
|
||
// blend with the opposite edge via linear filtering.
|
||
samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge;
|
||
samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge;
|
||
samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge;
|
||
samplerInfo.anisotropyEnable = (m_maxAnisotropy > 1.0f);
|
||
samplerInfo.maxAnisotropy = m_maxAnisotropy;
|
||
samplerInfo.borderColor = vk::BorderColor::eIntOpaqueBlack;
|
||
samplerInfo.unnormalizedCoordinates = false;
|
||
samplerInfo.compareEnable = false;
|
||
samplerInfo.compareOp = vk::CompareOp::eAlways;
|
||
samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear;
|
||
samplerInfo.mipLodBias = 0.0f;
|
||
samplerInfo.minLod = 0.0f;
|
||
samplerInfo.maxLod = VK_LOD_CLAMP_NONE;
|
||
|
||
try {
|
||
m_defaultSampler = m_device.createSampler(samplerInfo);
|
||
} catch (const vk::SystemError& e) {
|
||
nprintf(("vulkan", "Failed to create default sampler: %s\n", e.what()));
|
||
return false;
|
||
}
|
||
|
||
// Create 1x1 white fallback textures for unbound descriptor slots
|
||
if (!createFallbackTexture(m_fallback2DArrayTexture, m_fallback2DArrayAllocation,
|
||
m_fallback2DArrayView, ImageViewType::Array2D)) {
|
||
return false;
|
||
}
|
||
if (!createFallbackTexture(m_fallbackTexture2D, m_fallbackTexture2DAllocation,
|
||
m_fallbackTextureView2D, ImageViewType::Plain2D)) {
|
||
return false;
|
||
}
|
||
if (!createFallbackTexture(m_fallbackCubeTexture, m_fallbackCubeAllocation,
|
||
m_fallbackCubeView, ImageViewType::Cube, 6, true)) {
|
||
return false;
|
||
}
|
||
if (!createFallbackTexture(m_fallback3DTexture, m_fallback3DAllocation,
|
||
m_fallback3DView, ImageViewType::Volume3D, 1, false, vk::ImageType::e3D)) {
|
||
return false;
|
||
}
|
||
|
||
// Check BCx and ETC2 Support
|
||
auto features = m_physicalDevice.getFeatures();
|
||
bool supportsETC2 = features.textureCompressionETC2;
|
||
bool supportsBC = features.textureCompressionBC;
|
||
|
||
if (!supportsETC2) {
|
||
std::array<vk::Format, 6> etcFormats = {vk::Format::eEtc2R8G8B8UnormBlock,
|
||
vk::Format::eEtc2R8G8B8SrgbBlock,
|
||
vk::Format::eEtc2R8G8B8A1UnormBlock,
|
||
vk::Format::eEtc2R8G8B8A1SrgbBlock,
|
||
vk::Format::eEtc2R8G8B8A8UnormBlock,
|
||
vk::Format::eEtc2R8G8B8A8SrgbBlock};
|
||
|
||
for (auto fmt : etcFormats) {
|
||
auto props = m_physicalDevice.getFormatProperties(fmt);
|
||
if (props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImage) {
|
||
supportsETC2 = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!supportsBC) {
|
||
std::array<vk::Format, 4> bcFormats = {
|
||
vk::Format::eBc1RgbaUnormBlock, // DXT1
|
||
vk::Format::eBc2UnormBlock, // DXT3
|
||
vk::Format::eBc3UnormBlock, // DXT5
|
||
vk::Format::eBc7UnormBlock // BC7
|
||
};
|
||
|
||
for (auto fmt : bcFormats) {
|
||
auto props = m_physicalDevice.getFormatProperties(fmt);
|
||
if (props.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImage) {
|
||
supportsBC = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
mprintf(("VulkanTextureManager: ETC2 Texture Support = %s\n", supportsETC2 ? "YES" : "NO"));
|
||
mprintf(("VulkanTextureManager: BCn Texture Support = %s\n", supportsBC ? "YES" : "NO"));
|
||
|
||
m_initialized = true;
|
||
return true;
|
||
}
|
||
|
||
void VulkanTextureManager::shutdown()
|
||
{
|
||
if (!m_initialized) {
|
||
return;
|
||
}
|
||
|
||
// Destroy fallback 3D texture
|
||
if (m_fallback3DView) {
|
||
m_device.destroyImageView(m_fallback3DView);
|
||
m_fallback3DView = nullptr;
|
||
}
|
||
if (m_fallback3DTexture) {
|
||
m_device.destroyImage(m_fallback3DTexture);
|
||
m_fallback3DTexture = nullptr;
|
||
}
|
||
if (m_fallback3DAllocation.isValid()) {
|
||
m_memoryManager->freeAllocation(m_fallback3DAllocation);
|
||
}
|
||
|
||
// Destroy fallback cubemap
|
||
if (m_fallbackCubeView) {
|
||
m_device.destroyImageView(m_fallbackCubeView);
|
||
m_fallbackCubeView = nullptr;
|
||
}
|
||
if (m_fallbackCubeTexture) {
|
||
m_device.destroyImage(m_fallbackCubeTexture);
|
||
m_fallbackCubeTexture = nullptr;
|
||
}
|
||
if (m_fallbackCubeAllocation.isValid()) {
|
||
m_memoryManager->freeAllocation(m_fallbackCubeAllocation);
|
||
}
|
||
|
||
// Destroy fallback textures
|
||
if (m_fallbackTextureView2D) {
|
||
m_device.destroyImageView(m_fallbackTextureView2D);
|
||
m_fallbackTextureView2D = nullptr;
|
||
}
|
||
if (m_fallbackTexture2D) {
|
||
m_device.destroyImage(m_fallbackTexture2D);
|
||
m_fallbackTexture2D = nullptr;
|
||
}
|
||
if (m_fallbackTexture2DAllocation.isValid()) {
|
||
m_memoryManager->freeAllocation(m_fallbackTexture2DAllocation);
|
||
}
|
||
if (m_fallback2DArrayView) {
|
||
m_device.destroyImageView(m_fallback2DArrayView);
|
||
m_fallback2DArrayView = nullptr;
|
||
}
|
||
if (m_fallback2DArrayTexture) {
|
||
m_device.destroyImage(m_fallback2DArrayTexture);
|
||
m_fallback2DArrayTexture = nullptr;
|
||
}
|
||
if (m_fallback2DArrayAllocation.isValid()) {
|
||
m_memoryManager->freeAllocation(m_fallback2DArrayAllocation);
|
||
}
|
||
|
||
// Destroy samplers
|
||
if (m_defaultSampler) {
|
||
m_device.destroySampler(m_defaultSampler);
|
||
m_defaultSampler = nullptr;
|
||
}
|
||
|
||
for (auto& pair : m_samplerCache) {
|
||
m_device.destroySampler(pair.second);
|
||
}
|
||
m_samplerCache.clear();
|
||
|
||
m_initialized = false;
|
||
nprintf(("vulkan", "Vulkan Texture Manager shutdown\n"));
|
||
}
|
||
|
||
bool VulkanTextureManager::releaseAnimationSlotRef(tcache_slot_vulkan* ts)
|
||
{
|
||
// Non-array textures own their image outright — nothing to ref-count.
|
||
if (ts->arrayLayers <= 1 || ts->bitmapHandle < 0) {
|
||
return true;
|
||
}
|
||
|
||
// For shared animation texture arrays: mark this frame as unused, then check
|
||
// whether any other frame still references the shared image. We compute the
|
||
// base frame from slot data (bitmapHandle - arrayIndex) rather than calling
|
||
// bm_get_base_frame(), because during shutdown/mission-unload the bitmap
|
||
// entries may already be cleaned up, causing bm_get_base_frame() to return -1.
|
||
// That would skip ref-counting and every frame slot would independently queue
|
||
// the same shared resources for destruction (double-free).
|
||
ts->used = false;
|
||
|
||
int baseFrame = ts->bitmapHandle - static_cast<int>(ts->arrayIndex);
|
||
int numFrames = static_cast<int>(ts->arrayLayers);
|
||
vk::Image sharedImage = ts->image;
|
||
|
||
bool anyInUse = false;
|
||
for (int f = baseFrame; f < baseFrame + numFrames; f++) {
|
||
if (f == ts->bitmapHandle) {
|
||
continue; // skip self (already marked unused)
|
||
}
|
||
auto* fSlot = bm_get_slot(f, true);
|
||
if (fSlot && fSlot->gr_info) {
|
||
auto* fTs = static_cast<tcache_slot_vulkan*>(fSlot->gr_info);
|
||
if (fTs->used && fTs->image == sharedImage) {
|
||
anyInUse = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (anyInUse) {
|
||
// Other frames still reference the shared image — just detach this slot.
|
||
ts->image = nullptr;
|
||
ts->imageView = nullptr;
|
||
ts->allocation = VulkanAllocation{};
|
||
ts->reset();
|
||
return false;
|
||
}
|
||
|
||
// Last live reference — caller destroys the shared image.
|
||
return true;
|
||
}
|
||
|
||
void VulkanTextureManager::flushTextures() const
|
||
{
|
||
if (!m_initialized) {
|
||
return;
|
||
}
|
||
|
||
int flushed = 0;
|
||
auto* deletionQueue = getDeletionQueue();
|
||
|
||
for (auto& block : bm_blocks) {
|
||
for (auto& slot : block) {
|
||
if (!slot.gr_info) {
|
||
continue;
|
||
}
|
||
|
||
auto* ts = static_cast<tcache_slot_vulkan*>(slot.gr_info);
|
||
|
||
if (!ts->image) {
|
||
continue;
|
||
}
|
||
|
||
// Skip render targets — they are managed by the post-processor
|
||
// and scene texture system, not by the bitmap paging system
|
||
if (ts->isRenderTarget) {
|
||
continue;
|
||
}
|
||
|
||
// For shared animation texture arrays: only destroy the actual image
|
||
// when no other frame references it (detaches this slot in place and
|
||
// returns false otherwise).
|
||
if (!releaseAnimationSlotRef(ts)) {
|
||
continue;
|
||
}
|
||
|
||
// Queue deferred destruction of GPU resources
|
||
if (ts->imageView) {
|
||
deletionQueue->queueImageView(ts->imageView);
|
||
}
|
||
if (ts->image) {
|
||
deletionQueue->queueImage(ts->image, ts->allocation);
|
||
}
|
||
|
||
ts->reset();
|
||
flushed++;
|
||
}
|
||
}
|
||
|
||
nprintf(("vulkan", "VulkanTextureManager: Flushed %d textures for level transition\n", flushed));
|
||
}
|
||
|
||
void VulkanTextureManager::bm_init(bitmap_slot* slot) const
|
||
{
|
||
if (!m_initialized || !slot) {
|
||
return;
|
||
}
|
||
|
||
// Allocate Vulkan-specific data
|
||
if (slot->gr_info == nullptr) {
|
||
slot->gr_info = new tcache_slot_vulkan();
|
||
} else {
|
||
static_cast<tcache_slot_vulkan*>(slot->gr_info)->reset();
|
||
}
|
||
}
|
||
|
||
void VulkanTextureManager::bm_create(bitmap_slot* slot) const
|
||
{
|
||
if (!m_initialized || !slot) {
|
||
return;
|
||
}
|
||
|
||
// Ensure gr_info is allocated
|
||
if (slot->gr_info == nullptr) {
|
||
slot->gr_info = new tcache_slot_vulkan();
|
||
}
|
||
}
|
||
|
||
void VulkanTextureManager::bm_free_data(bitmap_slot* slot, bool release) const
|
||
{
|
||
if (!m_initialized || !slot || !slot->gr_info) {
|
||
return;
|
||
}
|
||
|
||
auto* ts = static_cast<tcache_slot_vulkan*>(slot->gr_info);
|
||
auto* deletionQueue = getDeletionQueue();
|
||
|
||
// For shared animation texture arrays: if another frame still references the
|
||
// shared image, detach this slot only (releaseAnimationSlotRef returns false);
|
||
// otherwise fall through to destroy the shared image.
|
||
if (!releaseAnimationSlotRef(ts)) {
|
||
if (release) {
|
||
delete ts;
|
||
slot->gr_info = nullptr;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Queue resources for deferred destruction to avoid destroying
|
||
// resources that may still be referenced by in-flight command buffers
|
||
|
||
// Cubemap per-face framebuffers and views (must be before ts->framebuffer
|
||
// since framebuffer may alias cubeFaceFramebuffers[0])
|
||
for (auto& fb : ts->cubeFaceFramebuffers) {
|
||
if (fb) {
|
||
deletionQueue->queueFramebuffer(fb);
|
||
fb = nullptr;
|
||
}
|
||
}
|
||
for (auto& v : ts->cubeFaceViews) {
|
||
if (v) {
|
||
deletionQueue->queueImageView(v);
|
||
v = nullptr;
|
||
}
|
||
}
|
||
// If framebuffer was aliased to cubeFaceFramebuffers[0], it's already cleaned up
|
||
if (ts->isCubemap) {
|
||
ts->framebuffer = nullptr;
|
||
}
|
||
|
||
if (ts->framebuffer) {
|
||
deletionQueue->queueFramebuffer(ts->framebuffer);
|
||
ts->framebuffer = nullptr;
|
||
}
|
||
|
||
if (ts->renderPass) {
|
||
deletionQueue->queueRenderPass(ts->renderPass);
|
||
ts->renderPass = nullptr;
|
||
}
|
||
|
||
if (ts->renderPassLoad) {
|
||
deletionQueue->queueRenderPass(ts->renderPassLoad);
|
||
ts->renderPassLoad = nullptr;
|
||
}
|
||
|
||
if (ts->imageView) {
|
||
deletionQueue->queueImageView(ts->imageView);
|
||
ts->imageView = nullptr;
|
||
}
|
||
|
||
if (ts->framebufferView) {
|
||
deletionQueue->queueImageView(ts->framebufferView);
|
||
ts->framebufferView = nullptr;
|
||
}
|
||
|
||
if (ts->depthImageView) {
|
||
deletionQueue->queueImageView(ts->depthImageView);
|
||
ts->depthImageView = nullptr;
|
||
}
|
||
|
||
if (ts->depthImage) {
|
||
deletionQueue->queueImage(ts->depthImage, ts->depthAllocation);
|
||
ts->depthImage = nullptr;
|
||
ts->depthAllocation = VulkanAllocation{}; // Clear to prevent double-free
|
||
}
|
||
|
||
if (ts->image) {
|
||
deletionQueue->queueImage(ts->image, ts->allocation);
|
||
ts->image = nullptr;
|
||
ts->allocation = VulkanAllocation{}; // Clear to prevent double-free
|
||
}
|
||
|
||
ts->reset();
|
||
|
||
if (release) {
|
||
delete ts;
|
||
slot->gr_info = nullptr;
|
||
}
|
||
}
|
||
|
||
bool VulkanTextureManager::uploadAnimationFrames(int handle, bitmap* bm, int compType,
|
||
int baseFrame, int numFrames)
|
||
{
|
||
nprintf(("vulkan", "VulkanTexture: Uploading animation array: base=%d numFrames=%d triggered by handle=%d\n",
|
||
baseFrame, numFrames, handle));
|
||
|
||
// Get dimensions and format from the triggering frame's bitmap
|
||
auto width = static_cast<uint32_t>(bm->w);
|
||
auto height = static_cast<uint32_t>(bm->h);
|
||
auto arrayLayerCount = static_cast<uint32_t>(numFrames);
|
||
|
||
bool isCompressed = (compType == DDS_DXT1 || compType == DDS_DXT3 || compType == DDS_DXT5 || compType == DDS_BC7 ||
|
||
compType == KTX_ETC2_RGB || compType == KTX_ETC2_SRGB || compType == KTX_ETC2_RGBA_EAC ||
|
||
compType == KTX_ETC2_SRGBA_EAC || compType == KTX_ETC2_RGB_A1 || compType == KTX_ETC2_SRGB_A1);
|
||
|
||
// Determine format
|
||
vk::Format format;
|
||
if (isCompressed) {
|
||
format = bppToVkFormat(bm->bpp, true, compType);
|
||
} else {
|
||
format = bppToVkFormat(bm->bpp);
|
||
}
|
||
if (format == vk::Format::eUndefined) {
|
||
nprintf(("vulkan", "VulkanTexture: uploadAnimationFrames: unsupported format bpp=%d compType=%d\n",
|
||
bm->bpp, compType));
|
||
return false;
|
||
}
|
||
|
||
// Each animation frame is one array layer; they all share this layout.
|
||
uint32_t mipLevels = 1;
|
||
if (isCompressed) {
|
||
mipLevels = static_cast<uint32_t>(bm_get_num_mipmaps(handle));
|
||
mipLevels = std::max<uint32_t>(mipLevels, 1);
|
||
}
|
||
|
||
TextureUploadLayout layout;
|
||
layout.width = width;
|
||
layout.height = height;
|
||
layout.mipLevels = mipLevels;
|
||
layout.isCompressed = isCompressed;
|
||
layout.blockSize = isCompressed ? get_compressed_block_size(compType) : 0;
|
||
layout.dstBytesPerPixel = (bm->bpp == 24) ? 4 : (bm->bpp / 8);
|
||
|
||
size_t layerDataSize = layerByteSize(layout);
|
||
size_t totalDataSize = layerDataSize * arrayLayerCount;
|
||
|
||
// Create multi-layer image
|
||
// eTransferSrc: animation frames can also be hit by the
|
||
// get_bitmap_from_texture() readback (it copies a single array layer out
|
||
// via eTransferSrcOptimal).
|
||
vk::ImageUsageFlags usage = vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled |
|
||
vk::ImageUsageFlagBits::eTransferSrc;
|
||
vk::Image image;
|
||
VulkanAllocation allocation;
|
||
|
||
if (!createImage(width, height, mipLevels, format, vk::ImageTiling::eOptimal,
|
||
usage, MemoryUsage::GpuOnly, image, allocation, arrayLayerCount)) {
|
||
nprintf(("vulkan", "VulkanTexture: uploadAnimationFrames: failed to create %ux%u x%d array image\n",
|
||
width, height, numFrames));
|
||
return false;
|
||
}
|
||
|
||
// Create multi-layer image view
|
||
vk::ImageView imageView = createImageView(image, format,
|
||
vk::ImageAspectFlagBits::eColor, mipLevels, ImageViewType::Array2D, arrayLayerCount);
|
||
if (!imageView) {
|
||
nprintf(("vulkan", "VulkanTexture: uploadAnimationFrames: failed to create image view\n"));
|
||
m_device.destroyImage(image);
|
||
m_memoryManager->freeAllocation(allocation);
|
||
return false;
|
||
}
|
||
|
||
// Create staging buffer for all layers
|
||
vk::Buffer stagingBuffer;
|
||
VulkanAllocation stagingAllocation;
|
||
if (!createStagingBuffer(totalDataSize, stagingBuffer, stagingAllocation)) {
|
||
m_device.destroyImageView(imageView);
|
||
m_device.destroyImage(image);
|
||
m_memoryManager->freeAllocation(allocation);
|
||
return false;
|
||
}
|
||
|
||
void* mapped = m_memoryManager->mapMemory(stagingAllocation);
|
||
if (!mapped) {
|
||
m_memoryManager->freeAllocation(stagingAllocation);
|
||
m_device.destroyBuffer(stagingBuffer);
|
||
m_device.destroyImageView(imageView);
|
||
m_device.destroyImage(image);
|
||
m_memoryManager->freeAllocation(allocation);
|
||
return false;
|
||
}
|
||
|
||
// Build per-layer copy regions and upload each frame's data
|
||
SCP_vector<vk::BufferImageCopy> copyRegions;
|
||
|
||
// Use the same lock parameters that were used for the triggering frame.
|
||
// bm->flags contains the lock flags (BMP_AABITMAP, BMP_TEX_OTHER, BMP_TEX_DXT*, etc.)
|
||
// bm->bpp contains the requested bpp. Using these ensures all frames are locked
|
||
// consistently (e.g., 8bpp for aabitmaps, 32bpp for RGBA textures).
|
||
auto lockBpp = bm->bpp;
|
||
auto lockFlags = bm->flags;
|
||
|
||
// Set guard flag to make recursive bm_data calls no-ops
|
||
m_uploadingAnimation = true;
|
||
|
||
for (int frame = baseFrame; frame < baseFrame + numFrames; frame++) {
|
||
auto layerIndex = static_cast<uint32_t>(frame - baseFrame);
|
||
size_t layerOffset = layerIndex * layerDataSize;
|
||
uint8_t* dst = static_cast<uint8_t*>(mapped) + layerOffset;
|
||
|
||
// Copy regions depend only on the layout, not the pixel data, so build
|
||
// them up front — even a frame that fails to lock still occupies its layer.
|
||
appendLayerCopyRegions(copyRegions, layout, layerIndex, layerOffset);
|
||
|
||
bitmap* frameBm;
|
||
bool needUnlock = false;
|
||
|
||
if (frame == handle) {
|
||
// This is the frame that triggered us — use the passed bitmap directly
|
||
frameBm = bm;
|
||
} else {
|
||
// Lock this frame to get its data
|
||
frameBm = bm_lock(frame, lockBpp, lockFlags);
|
||
if (!frameBm) {
|
||
nprintf(("vulkan", "VulkanTexture: uploadAnimationFrames: failed to lock frame %d\n", frame));
|
||
// Fill with zeros to avoid undefined data
|
||
memset(dst, 0, layerDataSize);
|
||
continue;
|
||
}
|
||
needUnlock = true;
|
||
}
|
||
|
||
// Copy frame data to staging buffer
|
||
if (!isCompressed && frameBm->bpp == 24) {
|
||
graphics::util::expand_BGR_to_BGRA(reinterpret_cast<const uint8_t*>(frameBm->data), dst,
|
||
static_cast<size_t>(width) * height);
|
||
} else {
|
||
memcpy(dst, reinterpret_cast<const void*>(frameBm->data), layerDataSize);
|
||
}
|
||
|
||
if (needUnlock) {
|
||
bm_unlock(frame);
|
||
}
|
||
}
|
||
|
||
m_uploadingAnimation = false;
|
||
|
||
// Flush staging buffer
|
||
m_memoryManager->flushMemory(stagingAllocation, 0, totalDataSize);
|
||
m_memoryManager->unmapMemory(stagingAllocation);
|
||
|
||
// Record transitions + copy and submit async
|
||
vk::CommandBuffer cmd = beginSingleTimeCommands();
|
||
recordUploadCommands(cmd, image, stagingBuffer, format, width, height,
|
||
mipLevels, vk::ImageLayout::eUndefined, false, copyRegions,
|
||
arrayLayerCount);
|
||
submitUploadAsync(cmd, stagingBuffer, stagingAllocation);
|
||
|
||
// Store shared image in ALL frame slots
|
||
for (int frame = baseFrame; frame < baseFrame + numFrames; frame++) {
|
||
int layerIndex = frame - baseFrame;
|
||
auto* frameSlot = bm_get_slot(frame, true);
|
||
if (!frameSlot) {
|
||
continue;
|
||
}
|
||
if (!frameSlot->gr_info) {
|
||
bm_init(frameSlot);
|
||
}
|
||
auto* ts = static_cast<tcache_slot_vulkan*>(frameSlot->gr_info);
|
||
|
||
// Defer destruction of any existing image in this slot
|
||
if (ts->image && ts->arrayLayers <= 1) {
|
||
auto* deletionQueue = getDeletionQueue();
|
||
if (ts->imageView) {
|
||
deletionQueue->queueImageView(ts->imageView);
|
||
}
|
||
deletionQueue->queueImage(ts->image, ts->allocation);
|
||
}
|
||
|
||
ts->image = image;
|
||
ts->imageView = imageView;
|
||
ts->allocation = allocation;
|
||
ts->width = width;
|
||
ts->height = height;
|
||
ts->format = format;
|
||
ts->mipLevels = mipLevels;
|
||
ts->bpp = bm->bpp;
|
||
ts->arrayLayers = arrayLayerCount;
|
||
ts->arrayIndex = static_cast<uint32_t>(layerIndex);
|
||
ts->bitmapHandle = frame;
|
||
ts->currentLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||
ts->used = true;
|
||
ts->uScale = 1.0f;
|
||
ts->vScale = 1.0f;
|
||
}
|
||
|
||
nprintf(("vulkan", "VulkanTexture: Animation array uploaded: %ux%u x%d layers, %zu bytes total\n",
|
||
width, height, numFrames, totalDataSize));
|
||
return true;
|
||
}
|
||
|
||
bool VulkanTextureManager::uploadCubemap(int handle, bitmap* bm, int compType)
|
||
{
|
||
nprintf(("vulkan", "VulkanTexture: Uploading cubemap: handle=%d w=%d h=%d compType=%d\n",
|
||
handle, bm->w, bm->h, compType));
|
||
|
||
auto* slot = bm_get_slot(handle, true);
|
||
if (!slot) {
|
||
return false;
|
||
}
|
||
if (!slot->gr_info) {
|
||
bm_init(slot);
|
||
}
|
||
auto* ts = static_cast<tcache_slot_vulkan*>(slot->gr_info);
|
||
|
||
auto faceW = static_cast<uint32_t>(bm->w);
|
||
auto faceH = static_cast<uint32_t>(bm->h);
|
||
|
||
// Map cubemap DDS compression types to base types
|
||
int baseCompType = compType;
|
||
if (compType == DDS_CUBEMAP_DXT1) baseCompType = DDS_DXT1;
|
||
else if (compType == DDS_CUBEMAP_DXT3) baseCompType = DDS_DXT3;
|
||
else if (compType == DDS_CUBEMAP_DXT5) baseCompType = DDS_DXT5;
|
||
|
||
bool isCompressed = (baseCompType == DDS_DXT1 || baseCompType == DDS_DXT3 ||
|
||
baseCompType == DDS_DXT5 || baseCompType == DDS_BC7 ||
|
||
baseCompType == KTX_ETC2_RGB || baseCompType == KTX_ETC2_SRGB ||
|
||
baseCompType == KTX_ETC2_RGBA_EAC || baseCompType == KTX_ETC2_SRGBA_EAC ||
|
||
baseCompType == KTX_ETC2_RGB_A1 || baseCompType == KTX_ETC2_SRGB_A1);
|
||
|
||
vk::Format format;
|
||
if (isCompressed) {
|
||
format = bppToVkFormat(bm->bpp, true, baseCompType);
|
||
} else {
|
||
format = bppToVkFormat(bm->bpp);
|
||
}
|
||
if (format == vk::Format::eUndefined) {
|
||
nprintf(("vulkan", "VulkanTexture: uploadCubemap: unsupported format\n"));
|
||
return false;
|
||
}
|
||
|
||
size_t blockSize = 0;
|
||
if (isCompressed) {
|
||
blockSize = get_compressed_block_size(baseCompType);
|
||
}
|
||
|
||
// uncompressed DDS cubemaps also store every face's mip chain (face0[mips], face1[mips], ...)
|
||
auto mipLevels = static_cast<uint32_t>(bm_get_num_mipmaps(handle));
|
||
mipLevels = std::max<uint32_t>(mipLevels, 1);
|
||
|
||
// A cubemap is six array layers; each face has the same layout.
|
||
TextureUploadLayout layout;
|
||
layout.width = faceW;
|
||
layout.height = faceH;
|
||
layout.mipLevels = mipLevels;
|
||
layout.isCompressed = isCompressed;
|
||
layout.blockSize = blockSize;
|
||
layout.dstBytesPerPixel = (bm->bpp == 24) ? 4 : (bm->bpp / 8);
|
||
|
||
size_t perFaceSize = layerByteSize(layout);
|
||
size_t totalDataSize = perFaceSize * 6;
|
||
|
||
// OpenGL builds the chain for one-level cubemaps, and the env map is sampled at a roughness-based mip
|
||
uint32_t imageMipLevels = mipLevels;
|
||
bool generateMips = false;
|
||
if (!isCompressed && mipLevels == 1) {
|
||
const auto fmtProps = m_physicalDevice.getFormatProperties(format);
|
||
if ((fmtProps.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImageFilterLinear) &&
|
||
(fmtProps.optimalTilingFeatures & vk::FormatFeatureFlagBits::eBlitSrc) &&
|
||
(fmtProps.optimalTilingFeatures & vk::FormatFeatureFlagBits::eBlitDst)) {
|
||
imageMipLevels = calculateMipLevels(faceW, faceH);
|
||
generateMips = imageMipLevels > 1;
|
||
}
|
||
}
|
||
|
||
// Defer destruction of existing resources
|
||
if (ts->image) {
|
||
auto* deletionQueue = getDeletionQueue();
|
||
if (ts->imageView) {
|
||
deletionQueue->queueImageView(ts->imageView);
|
||
ts->imageView = nullptr;
|
||
}
|
||
deletionQueue->queueImage(ts->image, ts->allocation);
|
||
ts->image = nullptr;
|
||
ts->allocation = VulkanAllocation{};
|
||
}
|
||
ts->isCubemap = false;
|
||
|
||
// Create cubemap image (6 layers, eCubeCompatible)
|
||
vk::ImageUsageFlags usage = vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled;
|
||
if (generateMips) {
|
||
usage |= vk::ImageUsageFlagBits::eTransferSrc;
|
||
}
|
||
if (!createImage(faceW, faceH, imageMipLevels, format, vk::ImageTiling::eOptimal,
|
||
usage, MemoryUsage::GpuOnly, ts->image, ts->allocation, 6, true)) {
|
||
nprintf(("vulkan", "VulkanTexture: uploadCubemap: failed to create cubemap image\n"));
|
||
return false;
|
||
}
|
||
|
||
// Create cubemap image view (samplerCube)
|
||
ts->imageView = createImageView(ts->image, format, vk::ImageAspectFlagBits::eColor,
|
||
imageMipLevels, ImageViewType::Cube, 6);
|
||
if (!ts->imageView) {
|
||
nprintf(("vulkan", "VulkanTexture: uploadCubemap: failed to create cube image view\n"));
|
||
m_device.destroyImage(ts->image);
|
||
ts->image = nullptr;
|
||
m_memoryManager->freeAllocation(ts->allocation);
|
||
return false;
|
||
}
|
||
|
||
// Create staging buffer
|
||
vk::Buffer stagingBuffer;
|
||
VulkanAllocation stagingAllocation;
|
||
if (!createStagingBuffer(totalDataSize, stagingBuffer, stagingAllocation)) {
|
||
m_device.destroyImageView(ts->imageView);
|
||
ts->imageView = nullptr;
|
||
m_device.destroyImage(ts->image);
|
||
ts->image = nullptr;
|
||
m_memoryManager->freeAllocation(ts->allocation);
|
||
return false;
|
||
}
|
||
|
||
void* mapped = m_memoryManager->mapMemory(stagingAllocation);
|
||
if (!mapped) {
|
||
m_memoryManager->freeAllocation(stagingAllocation);
|
||
m_device.destroyBuffer(stagingBuffer);
|
||
m_device.destroyImageView(ts->imageView);
|
||
ts->imageView = nullptr;
|
||
m_device.destroyImage(ts->image);
|
||
ts->image = nullptr;
|
||
m_memoryManager->freeAllocation(ts->allocation);
|
||
return false;
|
||
}
|
||
|
||
// Copy data to staging buffer
|
||
// DDS cubemap data layout: face0[mip0..mipN], face1[mip0..mipN], ..., face5[mip0..mipN]
|
||
if (!isCompressed && bm->bpp == 24) {
|
||
// Convert BGR to BGRA for all 6 faces and their mips
|
||
graphics::util::expand_BGR_to_BGRA(reinterpret_cast<const uint8_t*>(bm->data),
|
||
static_cast<uint8_t*>(mapped), (perFaceSize / 4) * 6);
|
||
} else {
|
||
memcpy(mapped, reinterpret_cast<const void*>(bm->data), totalDataSize);
|
||
}
|
||
|
||
// Build per-face, per-mip copy regions (each face is one array layer)
|
||
SCP_vector<vk::BufferImageCopy> copyRegions;
|
||
size_t bufferOffset = 0;
|
||
for (uint32_t face = 0; face < 6; face++) {
|
||
bufferOffset += appendLayerCopyRegions(copyRegions, layout, face, bufferOffset);
|
||
}
|
||
|
||
m_memoryManager->flushMemory(stagingAllocation, 0, totalDataSize);
|
||
m_memoryManager->unmapMemory(stagingAllocation);
|
||
|
||
// Record transitions + copy and submit async
|
||
vk::CommandBuffer cmd = beginSingleTimeCommands();
|
||
if (generateMips) {
|
||
copyRegions.clear();
|
||
}
|
||
recordUploadCommands(cmd, ts->image, stagingBuffer, format, faceW, faceH,
|
||
imageMipLevels, vk::ImageLayout::eUndefined, generateMips, copyRegions, 6);
|
||
submitUploadAsync(cmd, stagingBuffer, stagingAllocation);
|
||
|
||
// Update slot info
|
||
ts->width = faceW;
|
||
ts->height = faceH;
|
||
ts->format = format;
|
||
ts->mipLevels = imageMipLevels;
|
||
ts->bpp = bm->bpp;
|
||
ts->arrayLayers = 6;
|
||
ts->bitmapHandle = handle;
|
||
ts->currentLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||
ts->used = true;
|
||
ts->isCubemap = true;
|
||
ts->uScale = 1.0f;
|
||
ts->vScale = 1.0f;
|
||
|
||
nprintf(("vulkan", "VulkanTexture: Cubemap uploaded: %ux%u, %u mips, format=%d\n",
|
||
faceW, faceH, imageMipLevels, static_cast<int>(format)));
|
||
return true;
|
||
}
|
||
|
||
bool VulkanTextureManager::upload3DTexture(int handle, bitmap* bm, int texDepth)
|
||
{
|
||
auto* slot = bm_get_slot(handle, true);
|
||
if (!slot) {
|
||
return false;
|
||
}
|
||
|
||
if (!slot->gr_info) {
|
||
bm_init(slot);
|
||
}
|
||
|
||
auto* ts = static_cast<tcache_slot_vulkan*>(slot->gr_info);
|
||
|
||
auto width = static_cast<uint32_t>(bm->w);
|
||
auto height = static_cast<uint32_t>(bm->h);
|
||
auto depth3D = static_cast<uint32_t>(texDepth);
|
||
|
||
// 3D textures are always 32bpp RGBA uncompressed, single mip
|
||
vk::Format format = vk::Format::eR8G8B8A8Unorm;
|
||
size_t dataSize = width * height * depth3D * 4;
|
||
|
||
// Defer destruction of existing resources
|
||
if (ts->image) {
|
||
auto* deletionQueue = getDeletionQueue();
|
||
if (ts->imageView) {
|
||
deletionQueue->queueImageView(ts->imageView);
|
||
ts->imageView = nullptr;
|
||
}
|
||
deletionQueue->queueImage(ts->image, ts->allocation);
|
||
ts->image = nullptr;
|
||
ts->allocation = VulkanAllocation{};
|
||
}
|
||
|
||
// Create 3D image
|
||
if (!createImage(width, height, 1, format, vk::ImageTiling::eOptimal,
|
||
vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled,
|
||
MemoryUsage::GpuOnly, ts->image, ts->allocation,
|
||
1, false, depth3D, vk::ImageType::e3D)) {
|
||
nprintf(("vulkan", "Failed to create 3D texture image!\n"));
|
||
return false;
|
||
}
|
||
|
||
// Create 3D image view
|
||
ts->imageView = createImageView(ts->image, format, vk::ImageAspectFlagBits::eColor, 1, ImageViewType::Volume3D);
|
||
if (!ts->imageView) {
|
||
nprintf(("vulkan", "Failed to create 3D texture image view!\n"));
|
||
m_device.destroyImage(ts->image);
|
||
ts->image = nullptr;
|
||
m_memoryManager->freeAllocation(ts->allocation);
|
||
return false;
|
||
}
|
||
|
||
// Create staging buffer
|
||
vk::Buffer stagingBuffer;
|
||
VulkanAllocation stagingAllocation;
|
||
if (!createStagingBuffer(dataSize, stagingBuffer, stagingAllocation)) {
|
||
nprintf(("vulkan", "Failed to create staging buffer for 3D texture!\n"));
|
||
return false;
|
||
}
|
||
|
||
// Copy data to staging buffer
|
||
void* mapped = m_memoryManager->mapMemory(stagingAllocation);
|
||
Assert(mapped);
|
||
memcpy(mapped, reinterpret_cast<const void*>(bm->data), dataSize);
|
||
m_memoryManager->flushMemory(stagingAllocation, 0, dataSize);
|
||
m_memoryManager->unmapMemory(stagingAllocation);
|
||
|
||
// Record transitions + copy and submit. Route through the shared
|
||
// recordUploadCommands path with a single copy region carrying the full 3D
|
||
// extent (depth = depth3D); the barrier sequence is identical to the 2D path.
|
||
vk::BufferImageCopy region;
|
||
region.bufferOffset = 0;
|
||
region.bufferRowLength = 0;
|
||
region.bufferImageHeight = 0;
|
||
region.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
|
||
region.imageSubresource.mipLevel = 0;
|
||
region.imageSubresource.baseArrayLayer = 0;
|
||
region.imageSubresource.layerCount = 1;
|
||
region.imageOffset = vk::Offset3D(0, 0, 0);
|
||
region.imageExtent = vk::Extent3D(width, height, depth3D);
|
||
|
||
vk::CommandBuffer cmd = beginSingleTimeCommands();
|
||
recordUploadCommands(cmd, ts->image, stagingBuffer, format, width, height,
|
||
1, vk::ImageLayout::eUndefined, false, {region});
|
||
submitUploadAsync(cmd, stagingBuffer, stagingAllocation);
|
||
|
||
// Update slot info
|
||
ts->width = width;
|
||
ts->height = height;
|
||
ts->depth = depth3D;
|
||
ts->is3D = true;
|
||
ts->format = format;
|
||
ts->mipLevels = 1;
|
||
ts->bpp = 32;
|
||
ts->bitmapHandle = handle;
|
||
ts->currentLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||
ts->used = true;
|
||
ts->uScale = 1.0f;
|
||
ts->vScale = 1.0f;
|
||
|
||
nprintf(("vulkan", "VulkanTexture: 3D texture uploaded: %ux%ux%u, format=%d\n",
|
||
width, height, depth3D, static_cast<int>(format)));
|
||
return true;
|
||
}
|
||
|
||
bool VulkanTextureManager::bm_data(int handle, bitmap* bm, int compType)
|
||
{
|
||
if (m_bmDataLogCount < 20) {
|
||
nprintf(("vulkan", "VulkanTextureManager::bm_data #%d: handle=%d bm=%p bm->data=%p compType=%d\n",
|
||
m_bmDataLogCount++, handle, bm, bm ? reinterpret_cast<void*>(bm->data) : nullptr, compType));
|
||
}
|
||
|
||
if (!m_initialized || !bm || !bm->data) {
|
||
return false;
|
||
}
|
||
|
||
// Guard: nested bm_lock→bm_data calls during animation upload are no-ops
|
||
if (m_uploadingAnimation) {
|
||
return true;
|
||
}
|
||
|
||
// Detect animated texture arrays
|
||
int numFrames = 0;
|
||
int baseFrame = bm_get_base_frame(handle, &numFrames);
|
||
if (baseFrame < 0) {
|
||
return false;
|
||
}
|
||
|
||
if (numFrames > 1) {
|
||
// Check if the shared image already exists (earlier frame created it)
|
||
auto* baseSlot = bm_get_slot(baseFrame, true);
|
||
if (baseSlot) {
|
||
if (!baseSlot->gr_info) {
|
||
bm_init(baseSlot);
|
||
}
|
||
auto* baseTs = static_cast<tcache_slot_vulkan*>(baseSlot->gr_info);
|
||
if (baseTs->image && baseTs->arrayLayers == static_cast<uint32_t>(numFrames)) {
|
||
// Share existing image with this frame's slot
|
||
auto* slot = bm_get_slot(handle, true);
|
||
if (!slot->gr_info) {
|
||
bm_init(slot);
|
||
}
|
||
auto* ts = static_cast<tcache_slot_vulkan*>(slot->gr_info);
|
||
ts->image = baseTs->image;
|
||
ts->imageView = baseTs->imageView;
|
||
ts->allocation = baseTs->allocation;
|
||
ts->width = baseTs->width;
|
||
ts->height = baseTs->height;
|
||
ts->format = baseTs->format;
|
||
ts->mipLevels = baseTs->mipLevels;
|
||
ts->bpp = baseTs->bpp;
|
||
ts->arrayLayers = baseTs->arrayLayers;
|
||
ts->arrayIndex = static_cast<uint32_t>(handle - baseFrame);
|
||
ts->bitmapHandle = handle;
|
||
ts->currentLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||
ts->used = true;
|
||
return true;
|
||
}
|
||
}
|
||
// First frame requested — create array and upload all frames
|
||
return uploadAnimationFrames(handle, bm, compType, baseFrame, numFrames);
|
||
}
|
||
|
||
// Detect cubemap textures
|
||
bool isCubemapUpload = (bm->flags & BMP_TEX_CUBEMAP) != 0;
|
||
if (!isCubemapUpload) {
|
||
// Also check compression type for cubemap DDS variants
|
||
isCubemapUpload = (compType == DDS_CUBEMAP_DXT1 || compType == DDS_CUBEMAP_DXT3 ||
|
||
compType == DDS_CUBEMAP_DXT5);
|
||
}
|
||
// bm_is_compressed() returns 0 for uncompressed DDS cubemaps; only comp_type identifies them
|
||
if (!isCubemapUpload) {
|
||
isCubemapUpload = bm_get_entry(handle)->comp_type == BM_TYPE_CUBEMAP_DDS;
|
||
}
|
||
|
||
if (isCubemapUpload) {
|
||
return uploadCubemap(handle, bm, compType);
|
||
}
|
||
|
||
// Detect 3D textures (volumetric data)
|
||
if (bm->d > 1) {
|
||
return upload3DTexture(handle, bm, bm->d);
|
||
}
|
||
|
||
return uploadTexture2D(handle, bm, compType);
|
||
}
|
||
|
||
bool VulkanTextureManager::uploadTexture2D(int handle, bitmap* bm, int compType)
|
||
{
|
||
auto* slot = bm_get_slot(handle, true);
|
||
if (!slot) {
|
||
return false;
|
||
}
|
||
|
||
// Ensure slot is initialized
|
||
if (!slot->gr_info) {
|
||
bm_init(slot);
|
||
}
|
||
|
||
auto* ts = static_cast<tcache_slot_vulkan*>(slot->gr_info);
|
||
|
||
auto width = static_cast<uint32_t>(bm->w);
|
||
auto height = static_cast<uint32_t>(bm->h);
|
||
uint32_t mipLevels = 1;
|
||
bool autoGenerateMips = false;
|
||
bool isCompressed = (compType == DDS_DXT1 || compType == DDS_DXT3 || compType == DDS_DXT5 || compType == DDS_BC7 ||
|
||
compType == KTX_ETC2_RGB || compType == KTX_ETC2_SRGB || compType == KTX_ETC2_RGBA_EAC ||
|
||
compType == KTX_ETC2_SRGBA_EAC || compType == KTX_ETC2_RGB_A1 || compType == KTX_ETC2_SRGB_A1);
|
||
|
||
if (m_uploadFmtLogCount < 30) {
|
||
nprintf(("vulkan", "VulkanTextureManager::bm_data: handle=%d w=%d h=%d bpp=%d true_bpp=%d flags=0x%x compType=%d\n",
|
||
handle, bm->w, bm->h, bm->bpp, bm->true_bpp, bm->flags, compType));
|
||
m_uploadFmtLogCount++;
|
||
}
|
||
|
||
// Determine format and data size
|
||
vk::Format format;
|
||
size_t dataSize;
|
||
size_t blockSize = 0;
|
||
SCP_vector<vk::BufferImageCopy> copyRegions;
|
||
|
||
if (isCompressed) {
|
||
format = bppToVkFormat(bm->bpp, true, compType);
|
||
if (format == vk::Format::eUndefined) {
|
||
nprintf(("vulkan", "VulkanTextureManager::bm_data: Unsupported compression type %d\n", compType));
|
||
return false;
|
||
}
|
||
|
||
blockSize = get_compressed_block_size(compType);
|
||
|
||
// Get pre-baked mipmap count from DDS file
|
||
mipLevels = static_cast<uint32_t>(bm_get_num_mipmaps(handle));
|
||
mipLevels = std::max<uint32_t>(mipLevels, 1);
|
||
|
||
// Calculate total data size for all mip levels and build copy regions
|
||
TextureUploadLayout layout;
|
||
layout.width = width;
|
||
layout.height = height;
|
||
layout.mipLevels = mipLevels;
|
||
layout.isCompressed = true;
|
||
layout.blockSize = blockSize;
|
||
dataSize = appendLayerCopyRegions(copyRegions, layout, 0, 0);
|
||
} else {
|
||
format = bppToVkFormat(bm->bpp);
|
||
if (format == vk::Format::eUndefined) {
|
||
nprintf(("vulkan", "VulkanTextureManager::bm_data: Unsupported bpp %d\n", bm->bpp));
|
||
return false;
|
||
}
|
||
|
||
// 24bpp textures uploaded as 32bpp (Vulkan doesn't support 24bpp optimal tiling)
|
||
size_t dstBytesPerPixel = (bm->bpp == 24) ? 4 : (bm->bpp / 8);
|
||
dataSize = width * height * dstBytesPerPixel;
|
||
|
||
// Auto-generate mipmaps for textures whose files originally had them.
|
||
// This only triggers for uncompressed textures that were originally DDS
|
||
// with mipmaps but got decompressed by a non-DDS lock path.
|
||
if (width > 4 && height > 4) {
|
||
int numMipmaps = bm_get_num_mipmaps(handle);
|
||
if (numMipmaps > 1) {
|
||
vk::FormatProperties fmtProps = m_physicalDevice.getFormatProperties(format);
|
||
if ((fmtProps.optimalTilingFeatures & vk::FormatFeatureFlagBits::eSampledImageFilterLinear) &&
|
||
(fmtProps.optimalTilingFeatures & vk::FormatFeatureFlagBits::eBlitSrc) &&
|
||
(fmtProps.optimalTilingFeatures & vk::FormatFeatureFlagBits::eBlitDst)) {
|
||
mipLevels = calculateMipLevels(width, height);
|
||
autoGenerateMips = true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// If the texture already exists with identical extent+format we could update
|
||
// it in place (transition from ts->currentLayout, copy, transition back — the
|
||
// exact pattern update_texture already uses) instead of orphaning the old
|
||
// image and creating a fresh one. That in-place path is a behavior change with
|
||
// a cross-frame write into a still-referenced image, so it's gated on evidence
|
||
// that this condition actually fires often enough to matter. Log the first few
|
||
// hits so a test run reveals the real trigger frequency; behavior is unchanged
|
||
// (fall through to recreate).
|
||
if (ts->image && ts->width == width && ts->height == height && ts->format == format &&
|
||
ts->arrayLayers <= 1) {
|
||
if (m_reuploadLogCount < 20) {
|
||
nprintf(("vulkan", "VulkanTextureManager::uploadTexture2D: re-upload of resident texture "
|
||
"handle=%d (%ux%u) — recreating (in-place update not yet implemented)\n",
|
||
handle, width, height));
|
||
m_reuploadLogCount++;
|
||
}
|
||
}
|
||
|
||
// Defer destruction of existing resources — they may still be referenced
|
||
// by in-flight render or upload command buffers
|
||
if (ts->image) {
|
||
if (ts->arrayLayers > 1) {
|
||
// Shared animation image — just clear references, don't destroy
|
||
// (the image is shared with other frame slots)
|
||
ts->imageView = nullptr;
|
||
ts->image = nullptr;
|
||
ts->allocation = VulkanAllocation{};
|
||
} else {
|
||
auto* deletionQueue = getDeletionQueue();
|
||
if (ts->imageView) {
|
||
deletionQueue->queueImageView(ts->imageView);
|
||
ts->imageView = nullptr;
|
||
}
|
||
deletionQueue->queueImage(ts->image, ts->allocation);
|
||
ts->image = nullptr;
|
||
ts->allocation = VulkanAllocation{};
|
||
}
|
||
}
|
||
|
||
// Create image
|
||
// eTransferSrc is always set: needed for vkCmdBlitImage mipmap generation
|
||
// (autoGenerateMips) and for the get_bitmap_from_texture() readback path
|
||
// (bitmap_lookup alpha queries during model load), which copies the image
|
||
// back to a buffer via eTransferSrcOptimal.
|
||
vk::ImageUsageFlags usage = vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled |
|
||
vk::ImageUsageFlagBits::eTransferSrc;
|
||
|
||
if (!createImage(width, height, mipLevels, format, vk::ImageTiling::eOptimal,
|
||
usage, MemoryUsage::GpuOnly, ts->image, ts->allocation)) {
|
||
nprintf(("vulkan", "Failed to create texture image!\n"));
|
||
return false;
|
||
}
|
||
|
||
// Create image view (sampler2DArray for regular textures)
|
||
ts->imageView = createImageView(ts->image, format, vk::ImageAspectFlagBits::eColor, mipLevels, ImageViewType::Array2D);
|
||
if (!ts->imageView) {
|
||
nprintf(("vulkan", "Failed to create texture image view!\n"));
|
||
m_device.destroyImage(ts->image);
|
||
ts->image = nullptr;
|
||
m_memoryManager->freeAllocation(ts->allocation);
|
||
return false;
|
||
}
|
||
|
||
// Create staging buffer
|
||
vk::Buffer stagingBuffer;
|
||
VulkanAllocation stagingAllocation;
|
||
if (!createStagingBuffer(dataSize, stagingBuffer, stagingAllocation)) {
|
||
m_device.destroyImageView(ts->imageView);
|
||
ts->imageView = nullptr;
|
||
m_device.destroyImage(ts->image);
|
||
ts->image = nullptr;
|
||
m_memoryManager->freeAllocation(ts->allocation);
|
||
return false;
|
||
}
|
||
|
||
// Copy data to staging buffer
|
||
void* mapped = m_memoryManager->mapMemory(stagingAllocation);
|
||
Assert(mapped);
|
||
if (isCompressed) {
|
||
// Compressed data: copy raw block data directly (includes all mip levels)
|
||
memcpy(mapped, reinterpret_cast<const void*>(bm->data), dataSize);
|
||
} else if (bm->bpp == 24) {
|
||
graphics::util::expand_BGR_to_BGRA(reinterpret_cast<const uint8_t*>(bm->data),
|
||
static_cast<uint8_t*>(mapped), static_cast<size_t>(width) * height);
|
||
} else {
|
||
memcpy(mapped, reinterpret_cast<const void*>(bm->data), dataSize);
|
||
}
|
||
m_memoryManager->flushMemory(stagingAllocation, 0, dataSize);
|
||
m_memoryManager->unmapMemory(stagingAllocation);
|
||
|
||
// Record transitions + copy (+ optional mipmap generation) and submit async
|
||
vk::CommandBuffer cmd = beginSingleTimeCommands();
|
||
recordUploadCommands(cmd, ts->image, stagingBuffer, format, width, height,
|
||
mipLevels, vk::ImageLayout::eUndefined, autoGenerateMips, copyRegions);
|
||
submitUploadAsync(cmd, stagingBuffer, stagingAllocation);
|
||
|
||
// Update slot info
|
||
ts->width = width;
|
||
ts->height = height;
|
||
ts->format = format;
|
||
ts->mipLevels = mipLevels;
|
||
ts->bpp = bm->bpp;
|
||
ts->bitmapHandle = handle;
|
||
ts->currentLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||
ts->used = true;
|
||
ts->uScale = 1.0f;
|
||
ts->vScale = 1.0f;
|
||
|
||
return true;
|
||
}
|
||
|
||
int VulkanTextureManager::bm_make_render_target(int handle, int* width, int* height,
|
||
int* bpp, int* mm_lvl, int flags)
|
||
{
|
||
if (!m_initialized || !width || !height) {
|
||
return 0;
|
||
}
|
||
|
||
// Clamp to max size
|
||
if (static_cast<uint32_t>(*width) > m_maxTextureSize) {
|
||
*width = static_cast<int>(m_maxTextureSize);
|
||
}
|
||
if (static_cast<uint32_t>(*height) > m_maxTextureSize) {
|
||
*height = static_cast<int>(m_maxTextureSize);
|
||
}
|
||
|
||
auto* slot = bm_get_slot(handle, true);
|
||
if (!slot) {
|
||
return 0;
|
||
}
|
||
|
||
if (!slot->gr_info) {
|
||
bm_init(slot);
|
||
}
|
||
|
||
auto* ts = static_cast<tcache_slot_vulkan*>(slot->gr_info);
|
||
|
||
// Free any existing resources
|
||
bm_free_data(slot, false);
|
||
|
||
auto w = static_cast<uint32_t>(*width);
|
||
auto h = static_cast<uint32_t>(*height);
|
||
uint32_t mipLevels = 1;
|
||
|
||
if (flags & BMP_FLAG_RENDER_TARGET_MIPMAP) {
|
||
mipLevels = calculateMipLevels(w, h);
|
||
}
|
||
|
||
bool isCubemapRT = (flags & BMP_FLAG_CUBEMAP) != 0;
|
||
uint32_t arrayLayers = isCubemapRT ? 6 : 1;
|
||
vk::Format format = LDR_COLOR_FORMAT;
|
||
|
||
// Create image for render target
|
||
vk::ImageUsageFlags usage = vk::ImageUsageFlagBits::eColorAttachment |
|
||
vk::ImageUsageFlagBits::eSampled |
|
||
vk::ImageUsageFlagBits::eTransferSrc;
|
||
|
||
if (flags & BMP_FLAG_RENDER_TARGET_MIPMAP) {
|
||
usage |= vk::ImageUsageFlagBits::eTransferDst; // For mipmap generation
|
||
}
|
||
|
||
if (!createImage(w, h, mipLevels, format, vk::ImageTiling::eOptimal,
|
||
usage, MemoryUsage::GpuOnly, ts->image, ts->allocation, arrayLayers, isCubemapRT)) {
|
||
nprintf(("vulkan", "Failed to create render target image!\n"));
|
||
return 0;
|
||
}
|
||
|
||
if (isCubemapRT) {
|
||
// Cubemap render target: create cube view for sampling + per-face 2D views for framebuffer
|
||
ts->imageView = createImageView(ts->image, format, vk::ImageAspectFlagBits::eColor,
|
||
mipLevels, ImageViewType::Cube, 6);
|
||
if (!ts->imageView) {
|
||
m_device.destroyImage(ts->image);
|
||
ts->image = nullptr;
|
||
m_memoryManager->freeAllocation(ts->allocation);
|
||
return 0;
|
||
}
|
||
|
||
// Create per-face 2D views for framebuffer attachments
|
||
for (size_t face = 0; face < ts->cubeFaceViews.size(); face++) {
|
||
ts->cubeFaceViews[face] = createImageView(ts->image, format, vk::ImageAspectFlagBits::eColor,
|
||
1, ImageViewType::Plain2D, 1, static_cast<uint32_t>(face));
|
||
if (!ts->cubeFaceViews[face]) {
|
||
nprintf(("vulkan", "Failed to create cubemap face %zu view!\n", face));
|
||
// Clean up previously created views
|
||
for (size_t j = 0; j < face; j++) {
|
||
m_device.destroyImageView(ts->cubeFaceViews[j]);
|
||
ts->cubeFaceViews[j] = nullptr;
|
||
}
|
||
m_device.destroyImageView(ts->imageView);
|
||
m_device.destroyImage(ts->image);
|
||
ts->image = nullptr;
|
||
ts->imageView = nullptr;
|
||
m_memoryManager->freeAllocation(ts->allocation);
|
||
return 0;
|
||
}
|
||
}
|
||
} else {
|
||
// Regular render target: array view for shader compatibility
|
||
ts->imageView = createImageView(ts->image, format, vk::ImageAspectFlagBits::eColor, mipLevels, ImageViewType::Array2D);
|
||
if (!ts->imageView) {
|
||
m_device.destroyImage(ts->image);
|
||
ts->image = nullptr;
|
||
m_memoryManager->freeAllocation(ts->allocation);
|
||
return 0;
|
||
}
|
||
|
||
// For mipmapped render targets, create a single-mip view for framebuffer use
|
||
// (framebuffer attachments must have levelCount == 1)
|
||
if (mipLevels > 1) {
|
||
ts->framebufferView = createImageView(ts->image, format, vk::ImageAspectFlagBits::eColor, 1, ImageViewType::Array2D);
|
||
if (!ts->framebufferView) {
|
||
m_device.destroyImageView(ts->imageView);
|
||
m_device.destroyImage(ts->image);
|
||
ts->image = nullptr;
|
||
ts->imageView = nullptr;
|
||
m_memoryManager->freeAllocation(ts->allocation);
|
||
return 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Optional depth/stencil attachment. Without it, models rendered into the target
|
||
// get no z-testing at all: the Vulkan spec ignores the pipeline's depth/stencil
|
||
// state when the subpass has no depth attachment, so depth-tested geometry draws
|
||
// in submission order. This mirrors the OpenGL FBO's depth renderbuffer. Cubemap
|
||
// render targets (env/irradiance maps) never request this.
|
||
const bool wantDepth = ((flags & BMP_FLAG_RENDER_TARGET_DEPTH_ATTACHMENT) != 0) && !isCubemapRT;
|
||
vk::Format depthFormat = vk::Format::eUndefined;
|
||
if (wantDepth) {
|
||
depthFormat = getRendererInstance()->getDepthFormat();
|
||
if (depthFormat == vk::Format::eUndefined) {
|
||
Warning(LOCATION, "Vulkan render target requested a depth attachment before the depth format "
|
||
"was initialized; z-buffering will be unavailable for this target.");
|
||
} else if (!createImage(w, h, 1, depthFormat, vk::ImageTiling::eOptimal,
|
||
vk::ImageUsageFlagBits::eDepthStencilAttachment, MemoryUsage::GpuOnly,
|
||
ts->depthImage, ts->depthAllocation, 1, false)) {
|
||
nprintf(("vulkan", "Failed to create render target depth image!\n"));
|
||
depthFormat = vk::Format::eUndefined;
|
||
} else {
|
||
// Array2D view with a single layer matches the color framebuffer attachment.
|
||
ts->depthImageView = createImageView(ts->depthImage, depthFormat,
|
||
imageAspectFromFormat(depthFormat), 1, ImageViewType::Array2D);
|
||
if (!ts->depthImageView) {
|
||
m_device.destroyImage(ts->depthImage);
|
||
ts->depthImage = nullptr;
|
||
m_memoryManager->freeAllocation(ts->depthAllocation);
|
||
ts->depthAllocation = VulkanAllocation{};
|
||
depthFormat = vk::Format::eUndefined;
|
||
}
|
||
}
|
||
}
|
||
const bool hasDepth = (depthFormat != vk::Format::eUndefined) && static_cast<bool>(ts->depthImage);
|
||
|
||
// Create render pass for this target
|
||
vk::AttachmentDescription colorAttachment;
|
||
colorAttachment.format = format;
|
||
colorAttachment.samples = vk::SampleCountFlagBits::e1;
|
||
colorAttachment.loadOp = vk::AttachmentLoadOp::eClear;
|
||
colorAttachment.storeOp = vk::AttachmentStoreOp::eStore;
|
||
colorAttachment.stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
|
||
colorAttachment.stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
|
||
colorAttachment.initialLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||
colorAttachment.finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||
|
||
vk::AttachmentReference colorAttachmentRef;
|
||
colorAttachmentRef.attachment = 0;
|
||
colorAttachmentRef.layout = vk::ImageLayout::eColorAttachmentOptimal;
|
||
|
||
// Depth attachment: cleared at every pass begin (loadOp=eClear) so the target
|
||
// starts with a clean depth buffer regardless of what the caller does; contents
|
||
// are never sampled afterward (storeOp=eDontCare). Layout stays in
|
||
// eDepthStencilAttachmentOptimal across frames to avoid per-pass transitions.
|
||
vk::AttachmentDescription depthAttachment;
|
||
depthAttachment.format = depthFormat;
|
||
depthAttachment.samples = vk::SampleCountFlagBits::e1;
|
||
depthAttachment.loadOp = vk::AttachmentLoadOp::eClear;
|
||
depthAttachment.storeOp = vk::AttachmentStoreOp::eDontCare;
|
||
depthAttachment.stencilLoadOp = vk::AttachmentLoadOp::eDontCare;
|
||
depthAttachment.stencilStoreOp = vk::AttachmentStoreOp::eDontCare;
|
||
depthAttachment.initialLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal;
|
||
depthAttachment.finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal;
|
||
|
||
vk::AttachmentReference depthAttachmentRef;
|
||
depthAttachmentRef.attachment = 1;
|
||
depthAttachmentRef.layout = vk::ImageLayout::eDepthStencilAttachmentOptimal;
|
||
|
||
std::array<vk::AttachmentDescription, 2> attachments = {colorAttachment, depthAttachment};
|
||
|
||
vk::SubpassDescription subpass;
|
||
subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics;
|
||
subpass.colorAttachmentCount = 1;
|
||
subpass.pColorAttachments = &colorAttachmentRef;
|
||
if (hasDepth) {
|
||
subpass.pDepthStencilAttachment = &depthAttachmentRef;
|
||
}
|
||
|
||
// These targets are sampled as textures between passes, and some are re-rendered
|
||
// every frame while simultaneously being displayed (e.g. the SCPUI load bar, redrawn
|
||
// each frame and shown by the UI). With no explicit dependencies the render pass only
|
||
// gets the implicit TOP_OF_PIPE->BOTTOM_OF_PIPE dependency, which orders the layout
|
||
// transitions but creates no memory dependency between the color writes here and the
|
||
// fragment-shader sample in a later pass. That leaves a read-after-write (and, on
|
||
// re-render, write-after-read) hazard that manifests as flickering/garbage. Add
|
||
// both-direction external dependencies against the shader-read use to close it.
|
||
std::array<vk::SubpassDependency, 2> dependencies{};
|
||
// Pass start: a prior fragment-shader sample of this target must complete before we
|
||
// overwrite the color attachment (write-after-read; also covers loadOp=eLoad reads).
|
||
dependencies[0].srcSubpass = VK_SUBPASS_EXTERNAL;
|
||
dependencies[0].dstSubpass = 0;
|
||
dependencies[0].srcStageMask = vk::PipelineStageFlagBits::eFragmentShader;
|
||
dependencies[0].srcAccessMask = vk::AccessFlagBits::eShaderRead;
|
||
dependencies[0].dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
|
||
dependencies[0].dstAccessMask =
|
||
vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eColorAttachmentRead;
|
||
// Pass end: the color writes must be available and visible before the next
|
||
// fragment-shader sample of this target (read-after-write).
|
||
dependencies[1].srcSubpass = 0;
|
||
dependencies[1].dstSubpass = VK_SUBPASS_EXTERNAL;
|
||
dependencies[1].srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
|
||
dependencies[1].srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
|
||
dependencies[1].dstStageMask = vk::PipelineStageFlagBits::eFragmentShader;
|
||
dependencies[1].dstAccessMask = vk::AccessFlagBits::eShaderRead;
|
||
|
||
vk::RenderPassCreateInfo renderPassInfo;
|
||
renderPassInfo.attachmentCount = hasDepth ? 2u : 1u;
|
||
renderPassInfo.pAttachments = attachments.data();
|
||
renderPassInfo.subpassCount = 1;
|
||
renderPassInfo.pSubpasses = &subpass;
|
||
renderPassInfo.dependencyCount = static_cast<uint32_t>(dependencies.size());
|
||
renderPassInfo.pDependencies = dependencies.data();
|
||
|
||
try {
|
||
ts->renderPass = m_device.createRenderPass(renderPassInfo);
|
||
} catch (const vk::SystemError& e) {
|
||
nprintf(("vulkan", "Failed to create render pass: %s\n", e.what()));
|
||
if (hasDepth) {
|
||
m_device.destroyImageView(ts->depthImageView);
|
||
m_device.destroyImage(ts->depthImage);
|
||
ts->depthImageView = nullptr;
|
||
ts->depthImage = nullptr;
|
||
m_memoryManager->freeAllocation(ts->depthAllocation);
|
||
ts->depthAllocation = VulkanAllocation{};
|
||
}
|
||
m_device.destroyImageView(ts->imageView);
|
||
m_device.destroyImage(ts->image);
|
||
ts->image = nullptr;
|
||
ts->imageView = nullptr;
|
||
m_memoryManager->freeAllocation(ts->allocation);
|
||
return 0;
|
||
}
|
||
|
||
// Load-variant of the render pass (color loadOp=eLoad) so a mid-frame readback can
|
||
// resume rendering into this target without clearing what was already drawn. Only
|
||
// flat targets are read back via gr.screenToBlob, so skip it for cubemaps.
|
||
if (!isCubemapRT) {
|
||
vk::AttachmentDescription loadColorAttachment = colorAttachment;
|
||
loadColorAttachment.loadOp = vk::AttachmentLoadOp::eLoad;
|
||
std::array<vk::AttachmentDescription, 2> loadAttachments = {loadColorAttachment, depthAttachment};
|
||
|
||
vk::RenderPassCreateInfo loadPassInfo;
|
||
loadPassInfo.attachmentCount = hasDepth ? 2u : 1u;
|
||
loadPassInfo.pAttachments = loadAttachments.data();
|
||
loadPassInfo.subpassCount = 1;
|
||
loadPassInfo.pSubpasses = &subpass;
|
||
loadPassInfo.dependencyCount = static_cast<uint32_t>(dependencies.size());
|
||
loadPassInfo.pDependencies = dependencies.data();
|
||
|
||
try {
|
||
ts->renderPassLoad = m_device.createRenderPass(loadPassInfo);
|
||
} catch (const vk::SystemError& e) {
|
||
nprintf(("vulkan", "Failed to create load-variant render pass: %s\n", e.what()));
|
||
m_device.destroyRenderPass(ts->renderPass);
|
||
ts->renderPass = nullptr;
|
||
if (hasDepth) {
|
||
m_device.destroyImageView(ts->depthImageView);
|
||
m_device.destroyImage(ts->depthImage);
|
||
ts->depthImageView = nullptr;
|
||
ts->depthImage = nullptr;
|
||
m_memoryManager->freeAllocation(ts->depthAllocation);
|
||
ts->depthAllocation = VulkanAllocation{};
|
||
}
|
||
m_device.destroyImageView(ts->imageView);
|
||
m_device.destroyImage(ts->image);
|
||
ts->image = nullptr;
|
||
ts->imageView = nullptr;
|
||
m_memoryManager->freeAllocation(ts->allocation);
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
if (isCubemapRT) {
|
||
// Create per-face framebuffers
|
||
for (size_t face = 0; face < ts->cubeFaceFramebuffers.size(); face++) {
|
||
vk::FramebufferCreateInfo framebufferInfo;
|
||
framebufferInfo.renderPass = ts->renderPass;
|
||
framebufferInfo.attachmentCount = 1;
|
||
framebufferInfo.pAttachments = &ts->cubeFaceViews[face];
|
||
framebufferInfo.width = w;
|
||
framebufferInfo.height = h;
|
||
framebufferInfo.layers = 1;
|
||
|
||
try {
|
||
ts->cubeFaceFramebuffers[face] = m_device.createFramebuffer(framebufferInfo);
|
||
} catch (const vk::SystemError& e) {
|
||
nprintf(("vulkan", "Failed to create cubemap face %zu framebuffer: %s\n", face, e.what()));
|
||
return 0;
|
||
}
|
||
}
|
||
// Default framebuffer points to face 0
|
||
ts->framebuffer = ts->cubeFaceFramebuffers[0];
|
||
} else {
|
||
// Create framebuffer
|
||
// Use framebufferView (single-mip) if available, otherwise imageView.
|
||
// The depth view (when present) is attachment 1, matching the render pass.
|
||
std::array<vk::ImageView, 2> fbAttachments = {
|
||
ts->framebufferView ? ts->framebufferView : ts->imageView,
|
||
ts->depthImageView};
|
||
vk::FramebufferCreateInfo framebufferInfo;
|
||
framebufferInfo.renderPass = ts->renderPass;
|
||
framebufferInfo.attachmentCount = hasDepth ? 2u : 1u;
|
||
framebufferInfo.pAttachments = fbAttachments.data();
|
||
framebufferInfo.width = w;
|
||
framebufferInfo.height = h;
|
||
framebufferInfo.layers = 1;
|
||
|
||
try {
|
||
ts->framebuffer = m_device.createFramebuffer(framebufferInfo);
|
||
} catch (const vk::SystemError& e) {
|
||
nprintf(("vulkan", "Failed to create framebuffer: %s\n", e.what()));
|
||
if (hasDepth) {
|
||
m_device.destroyImageView(ts->depthImageView);
|
||
m_device.destroyImage(ts->depthImage);
|
||
ts->depthImageView = nullptr;
|
||
ts->depthImage = nullptr;
|
||
m_memoryManager->freeAllocation(ts->depthAllocation);
|
||
ts->depthAllocation = VulkanAllocation{};
|
||
}
|
||
m_device.destroyRenderPass(ts->renderPass);
|
||
if (ts->renderPassLoad) {
|
||
m_device.destroyRenderPass(ts->renderPassLoad);
|
||
ts->renderPassLoad = nullptr;
|
||
}
|
||
m_device.destroyImageView(ts->imageView);
|
||
m_device.destroyImage(ts->image);
|
||
ts->image = nullptr;
|
||
ts->imageView = nullptr;
|
||
ts->renderPass = nullptr;
|
||
m_memoryManager->freeAllocation(ts->allocation);
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
// Transition image to eShaderReadOnlyOptimal so it's in a valid layout
|
||
// if sampled before being rendered into (render pass expects this initial layout)
|
||
transitionImageLayout(ts->image, format, vk::ImageLayout::eUndefined,
|
||
vk::ImageLayout::eShaderReadOnlyOptimal, mipLevels, arrayLayers);
|
||
|
||
// Put the depth image in the layout the render pass expects as its initial layout.
|
||
// It stays there across frames (finalLayout matches), so no per-pass transition.
|
||
if (hasDepth) {
|
||
transitionImageLayout(ts->depthImage, depthFormat, vk::ImageLayout::eUndefined,
|
||
vk::ImageLayout::eDepthStencilAttachmentOptimal, 1, 1);
|
||
}
|
||
|
||
// Update slot info
|
||
ts->width = w;
|
||
ts->height = h;
|
||
ts->format = format;
|
||
ts->mipLevels = mipLevels;
|
||
ts->bpp = 32;
|
||
ts->arrayLayers = arrayLayers;
|
||
ts->bitmapHandle = handle;
|
||
ts->isRenderTarget = true;
|
||
ts->isCubemap = isCubemapRT;
|
||
ts->used = true;
|
||
ts->uScale = 1.0f;
|
||
ts->vScale = 1.0f;
|
||
ts->currentLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||
|
||
if (bpp) {
|
||
*bpp = 32;
|
||
}
|
||
if (mm_lvl) {
|
||
*mm_lvl = static_cast<int>(mipLevels);
|
||
}
|
||
|
||
nprintf(("vulkan", "Created Vulkan render target: %ux%u\n", w, h));
|
||
return 1;
|
||
}
|
||
|
||
int VulkanTextureManager::bm_set_render_target(int handle, int face)
|
||
{
|
||
if (!m_initialized) {
|
||
return 0;
|
||
}
|
||
|
||
auto* renderer = getRendererInstance();
|
||
|
||
// handle < 0 means reset to default framebuffer
|
||
if (handle < 0) {
|
||
if (renderer->isRenderTargetActive()) {
|
||
tcache_slot_vulkan* prevTs = nullptr;
|
||
if (m_currentRenderTarget >= 0) {
|
||
auto* prevSlot = bm_get_slot(m_currentRenderTarget, true);
|
||
if (prevSlot && prevSlot->gr_info) {
|
||
prevTs = static_cast<tcache_slot_vulkan*>(prevSlot->gr_info);
|
||
}
|
||
}
|
||
renderer->endRenderTarget(prevTs);
|
||
}
|
||
m_currentRenderTarget = -1;
|
||
return 1;
|
||
}
|
||
|
||
auto* slot = bm_get_slot(handle, true);
|
||
if (!slot || !slot->gr_info) {
|
||
return 0;
|
||
}
|
||
|
||
auto* ts = static_cast<tcache_slot_vulkan*>(slot->gr_info);
|
||
if (!ts->isRenderTarget || !ts->framebuffer) {
|
||
return 0;
|
||
}
|
||
|
||
renderer->beginRenderTarget(ts, face);
|
||
m_currentRenderTarget = handle;
|
||
|
||
return 1;
|
||
}
|
||
|
||
void VulkanTextureManager::update_texture(int bitmap_handle, int bpp, const ubyte* data,
|
||
int width, int height)
|
||
{
|
||
if (!m_initialized || !data) {
|
||
return;
|
||
}
|
||
|
||
auto* slot = bm_get_slot(bitmap_handle, true);
|
||
if (!slot || !slot->gr_info) {
|
||
return;
|
||
}
|
||
|
||
auto* ts = static_cast<tcache_slot_vulkan*>(slot->gr_info);
|
||
if (!ts->image) {
|
||
return;
|
||
}
|
||
|
||
auto w = static_cast<uint32_t>(width);
|
||
auto h = static_cast<uint32_t>(height);
|
||
|
||
// Verify dimensions match existing texture
|
||
if (ts->width != w || ts->height != h) {
|
||
nprintf(("vulkan", "VulkanTextureManager::update_texture: Size mismatch (%ux%u vs %ux%u)\n",
|
||
w, h, ts->width, ts->height));
|
||
return;
|
||
}
|
||
|
||
// Use bppToVkFormat to determine format, matching how bm_data creates textures
|
||
vk::Format format = bppToVkFormat(bpp);
|
||
if (format == vk::Format::eUndefined) {
|
||
nprintf(("vulkan", "VulkanTextureManager::update_texture: Unsupported bpp %d\n", bpp));
|
||
return;
|
||
}
|
||
|
||
// The update path copies straight into the existing image with its baked-in
|
||
// format; a bpp implying a different format than the texture was created with
|
||
// would silently corrupt the copy. Reject the mismatch instead.
|
||
if (format != ts->format) {
|
||
nprintf(("vulkan", "VulkanTextureManager::update_texture: format mismatch for bpp %d "
|
||
"(bitmap implies %d, texture is %d) — skipping update\n",
|
||
bpp, static_cast<int>(format), static_cast<int>(ts->format)));
|
||
return;
|
||
}
|
||
|
||
// Calculate staging buffer size (24bpp is uploaded as 32bpp BGRA)
|
||
size_t srcBytesPerPixel = bpp / 8;
|
||
size_t dstBytesPerPixel = (bpp == 24) ? 4 : srcBytesPerPixel;
|
||
size_t dataSize = w * h * dstBytesPerPixel;
|
||
|
||
// Create staging buffer
|
||
vk::Buffer stagingBuffer;
|
||
VulkanAllocation stagingAllocation;
|
||
if (!createStagingBuffer(dataSize, stagingBuffer, stagingAllocation)) {
|
||
nprintf(("vulkan", "VulkanTextureManager::update_texture: Failed to create staging buffer\n"));
|
||
return;
|
||
}
|
||
|
||
// Copy data to staging buffer
|
||
void* mapped = m_memoryManager->mapMemory(stagingAllocation);
|
||
Assert(mapped);
|
||
if (bpp == 24) {
|
||
graphics::util::expand_BGR_to_BGRA(data, static_cast<uint8_t*>(mapped), w * h);
|
||
} else {
|
||
memcpy(mapped, data, dataSize);
|
||
}
|
||
m_memoryManager->flushMemory(stagingAllocation, 0, dataSize);
|
||
m_memoryManager->unmapMemory(stagingAllocation);
|
||
|
||
// Record transitions + copy into a single command buffer and submit async
|
||
vk::CommandBuffer cmd = beginSingleTimeCommands();
|
||
recordUploadCommands(cmd, ts->image, stagingBuffer, format, w, h,
|
||
ts->mipLevels, ts->currentLayout);
|
||
submitUploadAsync(cmd, stagingBuffer, stagingAllocation);
|
||
|
||
// Update layout tracking
|
||
ts->currentLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||
}
|
||
|
||
ubyte* VulkanTextureManager::get_bitmap_from_texture(int bitmap_num, int* width_out, int* height_out)
|
||
{
|
||
if (!m_initialized || !width_out || !height_out) {
|
||
return nullptr;
|
||
}
|
||
|
||
*width_out = 0;
|
||
*height_out = 0;
|
||
|
||
auto* ts = bm_get_gr_info<tcache_slot_vulkan>(bitmap_num, true);
|
||
if (!ts) {
|
||
return nullptr;
|
||
}
|
||
|
||
// Ensure the texture is uploaded to the GPU. At model load time the
|
||
// Vulkan slot exists (vulkan_bm_create) but bm_data hasn't been called
|
||
// yet, so ts->image is null. This mirrors gr_opengl_tcache_set which
|
||
// the OpenGL readback calls to guarantee the texture is resident.
|
||
if (!ts->image) {
|
||
vulkan_preload(bitmap_num, 0);
|
||
|
||
// Re-fetch slot after preload (bm_data may have replaced the image)
|
||
ts = bm_get_gr_info<tcache_slot_vulkan>(bitmap_num, true);
|
||
if (!ts || !ts->image) {
|
||
return nullptr;
|
||
}
|
||
|
||
// bm_data submits uploads asynchronously via submitUploadAsync.
|
||
// Wait for completion so the GPU layout matches ts->currentLayout
|
||
// before we record the readback barriers.
|
||
m_graphicsQueue.waitIdle();
|
||
}
|
||
|
||
// Validate static-texture assumption: format must be one known
|
||
// from the bm_data upload path, and layout must be initialised.
|
||
Assertion(ts->format != vk::Format::eUndefined,
|
||
"get_bitmap_from_texture: undefined format on bitmap %d", bitmap_num);
|
||
Assertion(ts->currentLayout != vk::ImageLayout::eUndefined,
|
||
"get_bitmap_from_texture: undefined layout on bitmap %d", bitmap_num);
|
||
Assertion(
|
||
ts->format == vk::Format::eB8G8R8A8Unorm ||
|
||
ts->format == vk::Format::eR8Unorm ||
|
||
ts->format == vk::Format::eA1R5G5B5UnormPack16 ||
|
||
ts->format == vk::Format::eBc1RgbaUnormBlock ||
|
||
ts->format == vk::Format::eBc2UnormBlock ||
|
||
ts->format == vk::Format::eBc3UnormBlock ||
|
||
ts->format == vk::Format::eBc7UnormBlock ||
|
||
ts->format == vk::Format::eEtc2R8G8B8UnormBlock ||
|
||
ts->format == vk::Format::eEtc2R8G8B8SrgbBlock ||
|
||
ts->format == vk::Format::eEtc2R8G8B8A1UnormBlock ||
|
||
ts->format == vk::Format::eEtc2R8G8B8A1SrgbBlock ||
|
||
ts->format == vk::Format::eEtc2R8G8B8A8UnormBlock ||
|
||
ts->format == vk::Format::eEtc2R8G8B8A8SrgbBlock,
|
||
"get_bitmap_from_texture: unsupported format %d on bitmap %d"
|
||
" — may be a render target or dynamic texture",
|
||
static_cast<int>(ts->format), bitmap_num);
|
||
|
||
const uint32_t w = ts->width;
|
||
const uint32_t h = ts->height;
|
||
const uint32_t pixelCount = w * h;
|
||
const bool isCompressed = (ts->format == vk::Format::eBc1RgbaUnormBlock ||
|
||
ts->format == vk::Format::eBc2UnormBlock ||
|
||
ts->format == vk::Format::eBc3UnormBlock ||
|
||
ts->format == vk::Format::eBc7UnormBlock ||
|
||
ts->format == vk::Format::eEtc2R8G8B8UnormBlock ||
|
||
ts->format == vk::Format::eEtc2R8G8B8SrgbBlock ||
|
||
ts->format == vk::Format::eEtc2R8G8B8A1UnormBlock ||
|
||
ts->format == vk::Format::eEtc2R8G8B8A1SrgbBlock ||
|
||
ts->format == vk::Format::eEtc2R8G8B8A8UnormBlock ||
|
||
ts->format == vk::Format::eEtc2R8G8B8A8SrgbBlock);
|
||
const bool hasAlpha = bm_has_alpha_channel(bitmap_num);
|
||
const int outChannels = hasAlpha ? 4 : 3;
|
||
|
||
// ---- calculate source buffer size & per-pixel layout ----
|
||
size_t srcBytesPerPixel = 0;
|
||
size_t srcBufferSize = 0;
|
||
int blockSize = 0;
|
||
uint32_t blockW = 0;
|
||
uint32_t blockH = 0;
|
||
|
||
if (isCompressed) {
|
||
switch (ts->format) {
|
||
case vk::Format::eBc1RgbaUnormBlock: blockSize = BCDEC_BC1_BLOCK_SIZE; break;
|
||
case vk::Format::eBc2UnormBlock: blockSize = BCDEC_BC2_BLOCK_SIZE; break;
|
||
case vk::Format::eBc3UnormBlock: blockSize = BCDEC_BC3_BLOCK_SIZE; break;
|
||
case vk::Format::eBc7UnormBlock: blockSize = BCDEC_BC7_BLOCK_SIZE; break;
|
||
case vk::Format::eEtc2R8G8B8UnormBlock:
|
||
case vk::Format::eEtc2R8G8B8SrgbBlock: blockSize = ETCDEC_ETC_RGB_BLOCK_SIZE; break;
|
||
case vk::Format::eEtc2R8G8B8A1UnormBlock:
|
||
case vk::Format::eEtc2R8G8B8A1SrgbBlock: blockSize = ETCDEC_ETC_RGB_A1_BLOCK_SIZE; break;
|
||
case vk::Format::eEtc2R8G8B8A8UnormBlock:
|
||
case vk::Format::eEtc2R8G8B8A8SrgbBlock: blockSize = ETCDEC_EAC_RGBA_BLOCK_SIZE; break;
|
||
default: return nullptr;
|
||
}
|
||
blockW = (w + 3) / 4;
|
||
blockH = (h + 3) / 4;
|
||
srcBufferSize = static_cast<size_t>(blockW) * static_cast<size_t>(blockH) * static_cast<size_t>(blockSize);
|
||
} else {
|
||
switch (ts->format) {
|
||
case vk::Format::eB8G8R8A8Unorm: srcBytesPerPixel = 4; break;
|
||
case vk::Format::eR8Unorm: srcBytesPerPixel = 1; break;
|
||
case vk::Format::eA1R5G5B5UnormPack16: srcBytesPerPixel = 2; break;
|
||
default: return nullptr;
|
||
}
|
||
srcBufferSize = static_cast<size_t>(pixelCount) * srcBytesPerPixel;
|
||
}
|
||
|
||
// Output buffer: tightly packed w*h pixels at outChannels (3 or 4) bytes each,
|
||
// matching the layout gr_opengl_get_bitmap_from_texture() hands back. Caller
|
||
// owns this and frees it with vm_free (see bmpman.cpp's bm_lookup_cache).
|
||
auto* data_out = static_cast<ubyte*>(vm_malloc(static_cast<size_t>(pixelCount) * outChannels));
|
||
|
||
// ---- create readback staging buffer ----
|
||
vk::Buffer stagingBuffer;
|
||
VulkanAllocation stagingAlloc;
|
||
{
|
||
vk::BufferCreateInfo bufferInfo;
|
||
bufferInfo.size = srcBufferSize;
|
||
bufferInfo.usage = vk::BufferUsageFlagBits::eTransferDst;
|
||
bufferInfo.sharingMode = vk::SharingMode::eExclusive;
|
||
|
||
try {
|
||
stagingBuffer = m_device.createBuffer(bufferInfo);
|
||
} catch (const vk::SystemError& e) {
|
||
mprintf(("get_bitmap_from_texture: failed to create readback buffer: %s\n", e.what()));
|
||
vm_free(data_out);
|
||
return nullptr;
|
||
}
|
||
|
||
if (!m_memoryManager->allocateBufferMemory(stagingBuffer, MemoryUsage::GpuToCpu, stagingAlloc)) {
|
||
m_device.destroyBuffer(stagingBuffer);
|
||
vm_free(data_out);
|
||
return nullptr;
|
||
}
|
||
}
|
||
|
||
// ---- record layout-transition + copy command ----
|
||
const vk::ImageLayout oldLayout = ts->currentLayout;
|
||
vk::CommandBuffer cmd = beginSingleTimeCommands();
|
||
|
||
// Transition to eTransferSrcOptimal (single mip-0, single layer)
|
||
{
|
||
ImageBarrier2 barrier;
|
||
barrier.image = ts->image;
|
||
barrier.baseMipLevel = 0;
|
||
barrier.levelCount = 1;
|
||
barrier.baseArrayLayer = ts->arrayIndex;
|
||
barrier.layerCount = 1;
|
||
barrier.oldLayout = oldLayout;
|
||
barrier.newLayout = vk::ImageLayout::eTransferSrcOptimal;
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eFragmentShader;
|
||
barrier.srcAccess = vk::AccessFlagBits2::eShaderSampledRead;
|
||
barrier.dstStage = vk::PipelineStageFlagBits2::eCopy;
|
||
barrier.dstAccess = vk::AccessFlagBits2::eTransferRead;
|
||
|
||
cmdImageBarrier(cmd, barrier);
|
||
}
|
||
|
||
// Copy image -> buffer
|
||
{
|
||
vk::BufferImageCopy region;
|
||
region.bufferOffset = 0;
|
||
region.bufferRowLength = 0;
|
||
region.bufferImageHeight = 0;
|
||
region.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
|
||
region.imageSubresource.mipLevel = 0;
|
||
region.imageSubresource.baseArrayLayer = ts->arrayIndex;
|
||
region.imageSubresource.layerCount = 1;
|
||
region.imageOffset = vk::Offset3D(0, 0, 0);
|
||
region.imageExtent = vk::Extent3D(w, h, 1);
|
||
|
||
cmd.copyImageToBuffer(ts->image, vk::ImageLayout::eTransferSrcOptimal,
|
||
stagingBuffer, region);
|
||
}
|
||
|
||
// Transition back to original layout
|
||
{
|
||
ImageBarrier2 barrier;
|
||
barrier.image = ts->image;
|
||
barrier.baseMipLevel = 0;
|
||
barrier.levelCount = 1;
|
||
barrier.baseArrayLayer = ts->arrayIndex;
|
||
barrier.layerCount = 1;
|
||
barrier.oldLayout = vk::ImageLayout::eTransferSrcOptimal;
|
||
barrier.newLayout = oldLayout;
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eCopy;
|
||
barrier.srcAccess = vk::AccessFlagBits2::eTransferRead;
|
||
barrier.dstStage = vk::PipelineStageFlagBits2::eFragmentShader;
|
||
barrier.dstAccess = vk::AccessFlagBits2::eShaderSampledRead;
|
||
|
||
cmdImageBarrier(cmd, barrier);
|
||
}
|
||
|
||
endSingleTimeCommands(cmd); // synchronous submit + waitIdle
|
||
|
||
// ---- read back & convert ----
|
||
void* mapped = m_memoryManager->mapMemory(stagingAlloc);
|
||
if (mapped) {
|
||
m_memoryManager->invalidateMemory(stagingAlloc, 0, srcBufferSize);
|
||
decodeReadbackBuffer(mapped, ts->format, isCompressed, w, h, blockW, blockH, blockSize, outChannels, data_out);
|
||
m_memoryManager->unmapMemory(stagingAlloc);
|
||
}
|
||
|
||
m_device.destroyBuffer(stagingBuffer);
|
||
m_memoryManager->freeAllocation(stagingAlloc);
|
||
|
||
if (!mapped) {
|
||
vm_free(data_out);
|
||
return nullptr;
|
||
}
|
||
|
||
*width_out = ts->width;
|
||
*height_out = ts->height;
|
||
return data_out;
|
||
}
|
||
|
||
void VulkanTextureManager::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)
|
||
{
|
||
if (isCompressed) {
|
||
const uint32_t copyW = blockW * 4;
|
||
const uint32_t copyH = blockH * 4;
|
||
const size_t decompressedSize = static_cast<size_t>(copyW) * static_cast<size_t>(copyH) * 4;
|
||
SCP_vector<uint8_t> temp(decompressedSize);
|
||
|
||
const auto* srcBlock = static_cast<const uint8_t*>(mapped);
|
||
uint8_t* blockRowBase = temp.data();
|
||
|
||
for (uint32_t by = 0; by < blockH; ++by) {
|
||
uint8_t* blockColBase = blockRowBase;
|
||
for (uint32_t bx = 0; bx < blockW; ++bx) {
|
||
switch (format) {
|
||
case vk::Format::eBc1RgbaUnormBlock:
|
||
bcdec_bc1(srcBlock, blockColBase, static_cast<int>(copyW * 4));
|
||
break;
|
||
case vk::Format::eBc2UnormBlock:
|
||
bcdec_bc2(srcBlock, blockColBase, static_cast<int>(copyW * 4));
|
||
break;
|
||
case vk::Format::eBc3UnormBlock:
|
||
bcdec_bc3(srcBlock, blockColBase, static_cast<int>(copyW * 4));
|
||
break;
|
||
case vk::Format::eBc7UnormBlock:
|
||
bcdec_bc7(srcBlock, blockColBase, static_cast<int>(copyW * 4));
|
||
break;
|
||
case vk::Format::eEtc2R8G8B8UnormBlock:
|
||
case vk::Format::eEtc2R8G8B8SrgbBlock:
|
||
etcdec_etc_rgb(srcBlock, blockColBase, static_cast<int>(copyW * 4));
|
||
break;
|
||
case vk::Format::eEtc2R8G8B8A1UnormBlock:
|
||
case vk::Format::eEtc2R8G8B8A1SrgbBlock:
|
||
etcdec_etc_rgb_a1(srcBlock, blockColBase, static_cast<int>(copyW * 4));
|
||
break;
|
||
case vk::Format::eEtc2R8G8B8A8UnormBlock:
|
||
case vk::Format::eEtc2R8G8B8A8SrgbBlock:
|
||
etcdec_eac_rgba(srcBlock, blockColBase, static_cast<int>(copyW * 4));
|
||
break;
|
||
default: break;
|
||
}
|
||
srcBlock += blockSize;
|
||
blockColBase += 4 * 4; // 4 pixels wide × 4 bytes/pixel
|
||
}
|
||
blockRowBase += static_cast<size_t>(4) * copyW * 4; // 4 rows × full stride
|
||
}
|
||
|
||
const auto* src = temp.data();
|
||
auto* dst = static_cast<uint8_t*>(data_out);
|
||
if (outChannels == 4) {
|
||
for (uint32_t y = 0; y < h; ++y) {
|
||
memcpy(dst + y * w * 4, src + y * copyW * 4, w * 4);
|
||
}
|
||
} else {
|
||
for (uint32_t y = 0; y < h; ++y) {
|
||
graphics::util::convert_RGBA8888_to_RGB888(
|
||
src + y * copyW * 4, dst + y * w * 3, w);
|
||
}
|
||
}
|
||
} else {
|
||
const uint32_t pixelCount = w * h;
|
||
const auto* src = static_cast<const uint8_t*>(mapped);
|
||
auto* dst = static_cast<uint8_t*>(data_out);
|
||
|
||
switch (format) {
|
||
case vk::Format::eB8G8R8A8Unorm:
|
||
if (outChannels == 4) {
|
||
graphics::util::convert_BGRA8888_to_RGBA8888(src, dst, pixelCount);
|
||
} else {
|
||
graphics::util::convert_BGRA8888_to_RGB888(src, dst, pixelCount);
|
||
}
|
||
break;
|
||
|
||
case vk::Format::eR8Unorm:
|
||
if (outChannels == 4) {
|
||
graphics::util::expand_R8_to_RGBA(src, dst, pixelCount);
|
||
} else {
|
||
graphics::util::expand_R8_to_RGB(src, dst, pixelCount);
|
||
}
|
||
break;
|
||
|
||
case vk::Format::eA1R5G5B5UnormPack16:
|
||
if (outChannels == 4) {
|
||
graphics::util::convert_BGRA1555_REV_to_RGBA8888(reinterpret_cast<const uint16_t*>(src), dst, pixelCount);
|
||
} else {
|
||
graphics::util::convert_BGRA1555_REV_to_RGB888(reinterpret_cast<const uint16_t*>(src), dst, pixelCount);
|
||
}
|
||
break;
|
||
|
||
default: break;
|
||
}
|
||
}
|
||
}
|
||
|
||
vk::Sampler VulkanTextureManager::getSampler(vk::Filter magFilter, vk::Filter minFilter,
|
||
vk::SamplerAddressMode addressMode,
|
||
bool enableAnisotropy, float maxAnisotropy,
|
||
bool enableMipmaps)
|
||
{
|
||
// Create a key from sampler state
|
||
uint64_t key = 0;
|
||
key |= static_cast<uint64_t>(magFilter) << 0;
|
||
key |= static_cast<uint64_t>(minFilter) << 4;
|
||
key |= static_cast<uint64_t>(addressMode) << 8;
|
||
key |= static_cast<uint64_t>(enableAnisotropy) << 16;
|
||
key |= static_cast<uint64_t>(enableMipmaps) << 17;
|
||
key |= static_cast<uint64_t>(maxAnisotropy * 10) << 24;
|
||
|
||
auto it = m_samplerCache.find(key);
|
||
if (it != m_samplerCache.end()) {
|
||
return it->second;
|
||
}
|
||
|
||
// Create new sampler
|
||
vk::SamplerCreateInfo samplerInfo;
|
||
samplerInfo.magFilter = magFilter;
|
||
samplerInfo.minFilter = minFilter;
|
||
samplerInfo.addressModeU = addressMode;
|
||
samplerInfo.addressModeV = addressMode;
|
||
samplerInfo.addressModeW = addressMode;
|
||
samplerInfo.anisotropyEnable = enableAnisotropy && (m_maxAnisotropy > 1.0f);
|
||
samplerInfo.maxAnisotropy = std::max(1.0f, std::min(maxAnisotropy > 0.0f ? maxAnisotropy : m_maxAnisotropy, m_maxAnisotropy));
|
||
samplerInfo.borderColor = vk::BorderColor::eIntOpaqueBlack;
|
||
samplerInfo.unnormalizedCoordinates = false;
|
||
samplerInfo.compareEnable = false;
|
||
samplerInfo.compareOp = vk::CompareOp::eAlways;
|
||
samplerInfo.mipmapMode = enableMipmaps ? vk::SamplerMipmapMode::eLinear : vk::SamplerMipmapMode::eNearest;
|
||
samplerInfo.mipLodBias = 0.0f;
|
||
samplerInfo.minLod = 0.0f;
|
||
samplerInfo.maxLod = enableMipmaps ? VK_LOD_CLAMP_NONE : 0.0f;
|
||
|
||
try {
|
||
vk::Sampler sampler = m_device.createSampler(samplerInfo);
|
||
m_samplerCache[key] = sampler;
|
||
return sampler;
|
||
} catch (const vk::SystemError& e) {
|
||
nprintf(("vulkan", "Failed to create sampler: %s\n", e.what()));
|
||
return m_defaultSampler;
|
||
}
|
||
}
|
||
|
||
vk::DescriptorImageInfo VulkanTextureManager::getFallbackTextureInfo2D()
|
||
{
|
||
return {m_defaultSampler, m_fallbackTextureView2D,
|
||
vk::ImageLayout::eShaderReadOnlyOptimal};
|
||
}
|
||
|
||
vk::DescriptorImageInfo VulkanTextureManager::getFallbackTextureInfoCube()
|
||
{
|
||
return {m_defaultSampler, m_fallbackCubeView,
|
||
vk::ImageLayout::eShaderReadOnlyOptimal};
|
||
}
|
||
|
||
vk::DescriptorImageInfo VulkanTextureManager::getFallbackTextureInfo2DArray()
|
||
{
|
||
return {m_defaultSampler, m_fallback2DArrayView,
|
||
vk::ImageLayout::eShaderReadOnlyOptimal};
|
||
}
|
||
|
||
vk::DescriptorImageInfo VulkanTextureManager::getFallbackTextureInfo3D()
|
||
{
|
||
return {m_defaultSampler, m_fallback3DView,
|
||
vk::ImageLayout::eShaderReadOnlyOptimal};
|
||
}
|
||
|
||
tcache_slot_vulkan* VulkanTextureManager::getTextureSlot(int handle)
|
||
{
|
||
(void)this;
|
||
auto* slot = bm_get_slot(handle, true);
|
||
if (!slot || !slot->gr_info) {
|
||
return nullptr;
|
||
}
|
||
return static_cast<tcache_slot_vulkan*>(slot->gr_info);
|
||
}
|
||
|
||
bool VulkanTextureManager::ensureCubemapUploaded(int handle)
|
||
{
|
||
if (handle < 0) {
|
||
return false;
|
||
}
|
||
auto* ts = getTextureSlot(handle);
|
||
if (ts != nullptr && ts->imageView && ts->isCubemap) {
|
||
return true;
|
||
}
|
||
// one warning and no per-frame retry for a map that cannot be used; handles are reused, signatures are not
|
||
const uint signature = bm_get_entry(handle)->signature;
|
||
if (signature == m_failedCubemapSignature) {
|
||
return false;
|
||
}
|
||
|
||
bool uploaded = false;
|
||
// the cubemap kind lives in comp_type; type is just BM_TYPE_DDS
|
||
const BM_TYPE type = bm_get_entry(handle)->comp_type;
|
||
if (type == BM_TYPE_CUBEMAP_DDS || type == BM_TYPE_CUBEMAP_DXT1 || type == BM_TYPE_CUBEMAP_DXT3 ||
|
||
type == BM_TYPE_CUBEMAP_DXT5) {
|
||
// same lock as OpenGL's opengl_determine_bpp_and_flags for TCACHE_TYPE_CUBEMAP; DXT data stays compressed
|
||
const int compType = bm_is_compressed(handle);
|
||
const int bpp = bm_has_alpha_channel(handle) ? 32 : 24;
|
||
// bm_lock() uploads through bm_data(); upload here only if that did not leave a cube
|
||
bitmap* bmp = bm_lock(handle, static_cast<ubyte>(bpp), BMP_TEX_OTHER);
|
||
if (bmp != nullptr) {
|
||
ts = getTextureSlot(handle);
|
||
uploaded = (ts != nullptr && ts->imageView && ts->isCubemap) || uploadCubemap(handle, bmp, compType);
|
||
bm_unlock(handle);
|
||
}
|
||
}
|
||
|
||
if (!uploaded) {
|
||
m_failedCubemapSignature = signature;
|
||
Warning(LOCATION, "Environment map '%s' could not be uploaded as a cubemap; reflections are off.",
|
||
bm_get_filename(handle));
|
||
}
|
||
return uploaded;
|
||
}
|
||
|
||
vk::Format VulkanTextureManager::bppToVkFormat(int bpp, bool compressed, int compressionType)
|
||
{
|
||
if (compressed) {
|
||
// DDS compression types
|
||
switch (compressionType) {
|
||
case DDS_DXT1:
|
||
return vk::Format::eBc1RgbaUnormBlock;
|
||
case DDS_DXT3:
|
||
return vk::Format::eBc2UnormBlock;
|
||
case DDS_DXT5:
|
||
return vk::Format::eBc3UnormBlock;
|
||
case DDS_BC7:
|
||
return vk::Format::eBc7UnormBlock;
|
||
// KTX compression types
|
||
case KTX_ETC2_RGB:
|
||
return vk::Format::eEtc2R8G8B8UnormBlock;
|
||
case KTX_ETC2_SRGB:
|
||
return vk::Format::eEtc2R8G8B8SrgbBlock;
|
||
case KTX_ETC2_RGB_A1:
|
||
return vk::Format::eEtc2R8G8B8A1UnormBlock;
|
||
case KTX_ETC2_SRGB_A1:
|
||
return vk::Format::eEtc2R8G8B8A1SrgbBlock;
|
||
case KTX_ETC2_RGBA_EAC:
|
||
return vk::Format::eEtc2R8G8B8A8UnormBlock;
|
||
case KTX_ETC2_SRGBA_EAC:
|
||
return vk::Format::eEtc2R8G8B8A8SrgbBlock;
|
||
|
||
default:
|
||
return vk::Format::eUndefined;
|
||
}
|
||
}
|
||
|
||
switch (bpp) {
|
||
case 8:
|
||
return vk::Format::eR8Unorm;
|
||
case 16:
|
||
// OpenGL uses GL_UNSIGNED_SHORT_1_5_5_5_REV with GL_BGRA (A1R5G5B5)
|
||
return vk::Format::eA1R5G5B5UnormPack16;
|
||
case 24:
|
||
// 24bpp (BGR) is almost never supported for optimal tiling in Vulkan.
|
||
// We convert to 32bpp BGRA at upload time, so return the 32bpp format.
|
||
return vk::Format::eB8G8R8A8Unorm;
|
||
case 32:
|
||
// FSO uses BGRA format (BMP_AARRGGBB = BGRA in memory)
|
||
return vk::Format::eB8G8R8A8Unorm;
|
||
default:
|
||
return vk::Format::eUndefined;
|
||
}
|
||
}
|
||
|
||
void VulkanTextureManager::transitionImageLayout(vk::Image image, vk::Format format,
|
||
vk::ImageLayout oldLayout,
|
||
vk::ImageLayout newLayout,
|
||
uint32_t mipLevels,
|
||
uint32_t arrayLayers)
|
||
{
|
||
vk::CommandBuffer commandBuffer = beginSingleTimeCommands();
|
||
|
||
ImageBarrier2 barrier;
|
||
barrier.image = image;
|
||
barrier.aspectMask = imageAspectFromFormat(format);
|
||
barrier.baseMipLevel = 0;
|
||
barrier.levelCount = mipLevels;
|
||
barrier.baseArrayLayer = 0;
|
||
barrier.layerCount = arrayLayers;
|
||
barrier.oldLayout = oldLayout;
|
||
barrier.newLayout = newLayout;
|
||
|
||
// This is a generic, reusable transition helper -- the transfer/copy or
|
||
// blit operation (if any) that follows a given call happens in the caller,
|
||
// not here, so the transfer branches below stay at the generic eTransfer
|
||
// stage rather than being tightened to eCopy/eBlit.
|
||
if (oldLayout == vk::ImageLayout::eUndefined &&
|
||
newLayout == vk::ImageLayout::eTransferDstOptimal) {
|
||
barrier.srcAccess = {};
|
||
barrier.dstAccess = vk::AccessFlagBits2::eTransferWrite;
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eTopOfPipe;
|
||
barrier.dstStage = vk::PipelineStageFlagBits2::eTransfer;
|
||
} else if (oldLayout == vk::ImageLayout::eTransferDstOptimal &&
|
||
newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) {
|
||
barrier.srcAccess = vk::AccessFlagBits2::eTransferWrite;
|
||
barrier.dstAccess = vk::AccessFlagBits2::eShaderSampledRead;
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eTransfer;
|
||
barrier.dstStage = vk::PipelineStageFlagBits2::eFragmentShader;
|
||
} else if (oldLayout == vk::ImageLayout::eUndefined &&
|
||
newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) {
|
||
barrier.srcAccess = {};
|
||
barrier.dstAccess = vk::AccessFlagBits2::eShaderSampledRead;
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eTopOfPipe;
|
||
barrier.dstStage = vk::PipelineStageFlagBits2::eFragmentShader;
|
||
} else if (oldLayout == vk::ImageLayout::eUndefined &&
|
||
newLayout == vk::ImageLayout::eColorAttachmentOptimal) {
|
||
barrier.srcAccess = {};
|
||
barrier.dstAccess = vk::AccessFlagBits2::eColorAttachmentWrite;
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eTopOfPipe;
|
||
barrier.dstStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput;
|
||
} else if (oldLayout == vk::ImageLayout::eUndefined &&
|
||
newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) {
|
||
barrier.srcAccess = {};
|
||
barrier.dstAccess = vk::AccessFlagBits2::eDepthStencilAttachmentWrite;
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eTopOfPipe;
|
||
barrier.dstStage = vk::PipelineStageFlagBits2::eEarlyFragmentTests;
|
||
} else {
|
||
// Generic transition: the caller's prior/next GPU state isn't known
|
||
// here, so this deliberately stays maximally conservative rather than
|
||
// being narrowed -- an incorrect guess here would be a real under-sync
|
||
// hazard, which is a worse outcome than validation being coarse on
|
||
// this specific (currently unreached) fallback path.
|
||
barrier.srcAccess = vk::AccessFlagBits2::eMemoryWrite;
|
||
barrier.dstAccess = vk::AccessFlagBits2::eMemoryRead;
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eAllCommands;
|
||
barrier.dstStage = vk::PipelineStageFlagBits2::eAllCommands;
|
||
// This path is currently unreached; if it fires, a specific, narrower
|
||
// transition entry should be added for this layout pair above.
|
||
nprintf(("vulkan", "VulkanTexture: generic (conservative) layout transition %d -> %d; "
|
||
"consider adding a dedicated barrier for this pair\n",
|
||
static_cast<int>(oldLayout), static_cast<int>(newLayout)));
|
||
}
|
||
|
||
cmdImageBarrier(commandBuffer, barrier);
|
||
|
||
endSingleTimeCommands(commandBuffer);
|
||
}
|
||
|
||
void vulkan_generate_mipmap_chain(vk::CommandBuffer cmd, vk::Image image,
|
||
uint32_t width, uint32_t height,
|
||
uint32_t mipLevels, uint32_t arrayLayers)
|
||
{
|
||
if (mipLevels <= 1) {
|
||
return;
|
||
}
|
||
|
||
// Generate each mip level via blit from the previous level
|
||
for (uint32_t i = 1; i < mipLevels; i++) {
|
||
uint32_t srcW = std::max(1u, width >> (i - 1));
|
||
uint32_t srcH = std::max(1u, height >> (i - 1));
|
||
uint32_t dstW = std::max(1u, width >> i);
|
||
uint32_t dstH = std::max(1u, height >> i);
|
||
|
||
// Transition mip i from eUndefined to eTransferDstOptimal (blit destination)
|
||
{
|
||
ImageBarrier2 barrier;
|
||
barrier.image = image;
|
||
barrier.baseMipLevel = i;
|
||
barrier.levelCount = 1;
|
||
barrier.baseArrayLayer = 0;
|
||
barrier.layerCount = arrayLayers;
|
||
barrier.oldLayout = vk::ImageLayout::eUndefined;
|
||
barrier.newLayout = vk::ImageLayout::eTransferDstOptimal;
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eBlit;
|
||
barrier.srcAccess = {};
|
||
barrier.dstStage = vk::PipelineStageFlagBits2::eBlit;
|
||
barrier.dstAccess = vk::AccessFlagBits2::eTransferWrite;
|
||
|
||
cmdImageBarrier(cmd, barrier);
|
||
}
|
||
|
||
// Blit from mip i-1 to mip i
|
||
vk::ImageBlit blit;
|
||
blit.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
|
||
blit.srcSubresource.mipLevel = i - 1;
|
||
blit.srcSubresource.baseArrayLayer = 0;
|
||
blit.srcSubresource.layerCount = arrayLayers;
|
||
blit.srcOffsets[0] = vk::Offset3D(0, 0, 0);
|
||
blit.srcOffsets[1] = vk::Offset3D(static_cast<int32_t>(srcW), static_cast<int32_t>(srcH), 1);
|
||
|
||
blit.dstSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
|
||
blit.dstSubresource.mipLevel = i;
|
||
blit.dstSubresource.baseArrayLayer = 0;
|
||
blit.dstSubresource.layerCount = arrayLayers;
|
||
blit.dstOffsets[0] = vk::Offset3D(0, 0, 0);
|
||
blit.dstOffsets[1] = vk::Offset3D(static_cast<int32_t>(dstW), static_cast<int32_t>(dstH), 1);
|
||
|
||
cmd.blitImage(image, vk::ImageLayout::eTransferSrcOptimal,
|
||
image, vk::ImageLayout::eTransferDstOptimal,
|
||
blit, vk::Filter::eLinear);
|
||
|
||
// Transition mip i to eTransferSrcOptimal (source for next blit)
|
||
{
|
||
ImageBarrier2 barrier;
|
||
barrier.image = image;
|
||
barrier.baseMipLevel = i;
|
||
barrier.levelCount = 1;
|
||
barrier.baseArrayLayer = 0;
|
||
barrier.layerCount = arrayLayers;
|
||
barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
|
||
barrier.newLayout = vk::ImageLayout::eTransferSrcOptimal;
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eBlit;
|
||
barrier.srcAccess = vk::AccessFlagBits2::eTransferWrite;
|
||
barrier.dstStage = vk::PipelineStageFlagBits2::eBlit;
|
||
barrier.dstAccess = vk::AccessFlagBits2::eTransferRead;
|
||
|
||
cmdImageBarrier(cmd, barrier);
|
||
}
|
||
}
|
||
|
||
// Final transition: all mips to eShaderReadOnlyOptimal
|
||
{
|
||
ImageBarrier2 barrier;
|
||
barrier.image = image;
|
||
barrier.baseMipLevel = 0;
|
||
barrier.levelCount = mipLevels;
|
||
barrier.baseArrayLayer = 0;
|
||
barrier.layerCount = arrayLayers;
|
||
barrier.oldLayout = vk::ImageLayout::eTransferSrcOptimal;
|
||
barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eBlit;
|
||
barrier.srcAccess = vk::AccessFlagBits2::eTransferRead;
|
||
barrier.dstStage = vk::PipelineStageFlagBits2::eFragmentShader;
|
||
barrier.dstAccess = vk::AccessFlagBits2::eShaderSampledRead;
|
||
|
||
cmdImageBarrier(cmd, barrier);
|
||
}
|
||
}
|
||
|
||
void VulkanTextureManager::frameStart()
|
||
{
|
||
processPendingCommandBuffers();
|
||
}
|
||
|
||
bool VulkanTextureManager::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, bool cubemap,
|
||
uint32_t imageDepth, vk::ImageType imageType)
|
||
{
|
||
vk::ImageCreateInfo imageInfo;
|
||
imageInfo.imageType = imageType;
|
||
imageInfo.extent.width = width;
|
||
imageInfo.extent.height = height;
|
||
imageInfo.extent.depth = imageDepth;
|
||
imageInfo.mipLevels = mipLevels;
|
||
imageInfo.arrayLayers = arrayLayers;
|
||
imageInfo.format = format;
|
||
imageInfo.tiling = tiling;
|
||
imageInfo.initialLayout = vk::ImageLayout::eUndefined;
|
||
imageInfo.usage = usage;
|
||
imageInfo.sharingMode = vk::SharingMode::eExclusive;
|
||
imageInfo.samples = vk::SampleCountFlagBits::e1;
|
||
|
||
if (cubemap) {
|
||
imageInfo.flags |= vk::ImageCreateFlagBits::eCubeCompatible;
|
||
Assertion(arrayLayers == 6, "Cubemap images must have exactly 6 array layers!");
|
||
}
|
||
|
||
try {
|
||
image = m_device.createImage(imageInfo);
|
||
} catch (const vk::SystemError& e) {
|
||
nprintf(("vulkan", "Failed to create image: %s\n", e.what()));
|
||
return false;
|
||
}
|
||
|
||
if (!m_memoryManager->allocateImageMemory(image, memUsage, allocation)) {
|
||
m_device.destroyImage(image);
|
||
image = nullptr;
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
vk::ImageView VulkanTextureManager::createImageView(vk::Image image, vk::Format format,
|
||
vk::ImageAspectFlags aspectFlags,
|
||
uint32_t mipLevels,
|
||
ImageViewType viewType,
|
||
uint32_t layerCount,
|
||
uint32_t baseArrayLayer)
|
||
{
|
||
vk::ImageViewCreateInfo viewInfo;
|
||
viewInfo.image = image;
|
||
switch (viewType) {
|
||
case ImageViewType::Cube:
|
||
viewInfo.viewType = vk::ImageViewType::eCube;
|
||
break;
|
||
case ImageViewType::Array2D:
|
||
viewInfo.viewType = vk::ImageViewType::e2DArray;
|
||
break;
|
||
case ImageViewType::Volume3D:
|
||
viewInfo.viewType = vk::ImageViewType::e3D;
|
||
break;
|
||
case ImageViewType::Plain2D:
|
||
default:
|
||
viewInfo.viewType = vk::ImageViewType::e2D;
|
||
break;
|
||
}
|
||
viewInfo.format = format;
|
||
viewInfo.subresourceRange.aspectMask = aspectFlags;
|
||
viewInfo.subresourceRange.baseMipLevel = 0;
|
||
viewInfo.subresourceRange.levelCount = mipLevels;
|
||
viewInfo.subresourceRange.baseArrayLayer = baseArrayLayer;
|
||
viewInfo.subresourceRange.layerCount = layerCount;
|
||
|
||
try {
|
||
return m_device.createImageView(viewInfo);
|
||
} catch (const vk::SystemError& e) {
|
||
nprintf(("vulkan", "Failed to create image view: %s\n", e.what()));
|
||
return nullptr;
|
||
}
|
||
}
|
||
|
||
bool VulkanTextureManager::createFallbackTexture(vk::Image& outImage, VulkanAllocation& outAlloc,
|
||
vk::ImageView& outView, ImageViewType viewType,
|
||
uint32_t arrayLayers, bool cubemap,
|
||
vk::ImageType imageType)
|
||
{
|
||
if (!createImage(1, 1, 1, vk::Format::eR8G8B8A8Unorm, vk::ImageTiling::eOptimal,
|
||
vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled,
|
||
MemoryUsage::GpuOnly, outImage, outAlloc, arrayLayers, cubemap, 1, imageType)) {
|
||
nprintf(("vulkan", "Failed to create fallback texture image!\n"));
|
||
return false;
|
||
}
|
||
|
||
outView = createImageView(outImage, vk::Format::eR8G8B8A8Unorm,
|
||
vk::ImageAspectFlagBits::eColor, 1, viewType, arrayLayers);
|
||
if (!outView) {
|
||
nprintf(("vulkan", "Failed to create fallback texture view!\n"));
|
||
m_device.destroyImage(outImage);
|
||
m_memoryManager->freeAllocation(outAlloc);
|
||
return false;
|
||
}
|
||
|
||
// Upload white pixels via staging buffer
|
||
SCP_vector<uint32_t> whitePixels(arrayLayers, 0xFFFFFFFF);
|
||
vk::DeviceSize bufferSize = arrayLayers * sizeof(uint32_t);
|
||
|
||
vk::BufferCreateInfo bufferInfo;
|
||
bufferInfo.size = bufferSize;
|
||
bufferInfo.usage = vk::BufferUsageFlagBits::eTransferSrc;
|
||
bufferInfo.sharingMode = vk::SharingMode::eExclusive;
|
||
|
||
vk::Buffer stagingBuffer;
|
||
VulkanAllocation stagingAlloc;
|
||
try {
|
||
stagingBuffer = m_device.createBuffer(bufferInfo);
|
||
} catch (const vk::SystemError& e) {
|
||
nprintf(("vulkan", "Failed to create fallback staging buffer: %s\n", e.what()));
|
||
m_device.destroyImageView(outView);
|
||
m_device.destroyImage(outImage);
|
||
m_memoryManager->freeAllocation(outAlloc);
|
||
return false;
|
||
}
|
||
|
||
if (!m_memoryManager->allocateBufferMemory(stagingBuffer, MemoryUsage::CpuToGpu, stagingAlloc)) {
|
||
m_device.destroyBuffer(stagingBuffer);
|
||
m_device.destroyImageView(outView);
|
||
m_device.destroyImage(outImage);
|
||
m_memoryManager->freeAllocation(outAlloc);
|
||
return false;
|
||
}
|
||
|
||
void* mapped = m_memoryManager->mapMemory(stagingAlloc);
|
||
memcpy(mapped, whitePixels.data(), static_cast<size_t>(bufferSize));
|
||
m_memoryManager->unmapMemory(stagingAlloc);
|
||
|
||
SCP_vector<vk::BufferImageCopy> regions;
|
||
for (uint32_t i = 0; i < arrayLayers; i++) {
|
||
vk::BufferImageCopy region;
|
||
region.bufferOffset = i * sizeof(uint32_t);
|
||
region.bufferRowLength = 0;
|
||
region.bufferImageHeight = 0;
|
||
region.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
|
||
region.imageSubresource.mipLevel = 0;
|
||
region.imageSubresource.baseArrayLayer = i;
|
||
region.imageSubresource.layerCount = 1;
|
||
region.imageOffset = vk::Offset3D(0, 0, 0);
|
||
region.imageExtent = vk::Extent3D(1, 1, 1);
|
||
regions.push_back(region);
|
||
}
|
||
|
||
vk::CommandBuffer cmd = beginSingleTimeCommands();
|
||
recordUploadCommands(cmd, outImage, stagingBuffer, vk::Format::eR8G8B8A8Unorm,
|
||
1, 1, 1, vk::ImageLayout::eUndefined, false, regions, arrayLayers);
|
||
endSingleTimeCommands(cmd);
|
||
|
||
m_device.destroyBuffer(stagingBuffer);
|
||
m_memoryManager->freeAllocation(stagingAlloc);
|
||
|
||
return true;
|
||
}
|
||
|
||
bool VulkanTextureManager::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)
|
||
{
|
||
if (!createImage(width, height, 1, format, vk::ImageTiling::eOptimal,
|
||
vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled,
|
||
MemoryUsage::GpuOnly, outImage, outAlloc)) {
|
||
nprintf(("vulkan", "Failed to create static texture image '%s'!\n", debugName));
|
||
return false;
|
||
}
|
||
|
||
outView = createImageView(outImage, format, vk::ImageAspectFlagBits::eColor, 1, ImageViewType::Plain2D);
|
||
if (!outView) {
|
||
nprintf(("vulkan", "Failed to create static texture view '%s'!\n", debugName));
|
||
m_device.destroyImage(outImage);
|
||
m_memoryManager->freeAllocation(outAlloc);
|
||
return false;
|
||
}
|
||
|
||
vk::Buffer stagingBuffer;
|
||
VulkanAllocation stagingAlloc;
|
||
if (!createStagingBuffer(dataSize, stagingBuffer, stagingAlloc)) {
|
||
nprintf(("vulkan", "Failed to create staging buffer for static texture '%s'!\n", debugName));
|
||
m_device.destroyImageView(outView);
|
||
m_device.destroyImage(outImage);
|
||
m_memoryManager->freeAllocation(outAlloc);
|
||
return false;
|
||
}
|
||
|
||
void* mapped = m_memoryManager->mapMemory(stagingAlloc);
|
||
memcpy(mapped, pixelData, dataSize);
|
||
m_memoryManager->unmapMemory(stagingAlloc);
|
||
|
||
vk::BufferImageCopy region;
|
||
region.bufferOffset = 0;
|
||
region.bufferRowLength = 0;
|
||
region.bufferImageHeight = 0;
|
||
region.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
|
||
region.imageSubresource.mipLevel = 0;
|
||
region.imageSubresource.baseArrayLayer = 0;
|
||
region.imageSubresource.layerCount = 1;
|
||
region.imageOffset = vk::Offset3D(0, 0, 0);
|
||
region.imageExtent = vk::Extent3D(width, height, 1);
|
||
|
||
vk::CommandBuffer cmd = beginSingleTimeCommands();
|
||
recordUploadCommands(cmd, outImage, stagingBuffer, format, width, height,
|
||
1, vk::ImageLayout::eUndefined, false, {region}, 1);
|
||
endSingleTimeCommands(cmd);
|
||
|
||
m_device.destroyBuffer(stagingBuffer);
|
||
m_memoryManager->freeAllocation(stagingAlloc);
|
||
|
||
return true;
|
||
}
|
||
|
||
bool VulkanTextureManager::createStagingBuffer(size_t size, vk::Buffer& outBuffer,
|
||
VulkanAllocation& outAllocation)
|
||
{
|
||
vk::BufferCreateInfo bufferInfo;
|
||
bufferInfo.size = size;
|
||
bufferInfo.usage = vk::BufferUsageFlagBits::eTransferSrc;
|
||
bufferInfo.sharingMode = vk::SharingMode::eExclusive;
|
||
|
||
try {
|
||
outBuffer = m_device.createBuffer(bufferInfo);
|
||
} catch (const vk::SystemError& e) {
|
||
nprintf(("vulkan", "VulkanTexture: failed to create staging buffer: %s\n", e.what()));
|
||
outBuffer = nullptr;
|
||
return false;
|
||
}
|
||
|
||
if (!m_memoryManager->allocateBufferMemory(outBuffer, MemoryUsage::CpuOnly, outAllocation)) {
|
||
m_device.destroyBuffer(outBuffer);
|
||
outBuffer = nullptr;
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
vk::CommandBuffer VulkanTextureManager::beginSingleTimeCommands()
|
||
{
|
||
vk::CommandBufferAllocateInfo allocInfo;
|
||
allocInfo.level = vk::CommandBufferLevel::ePrimary;
|
||
allocInfo.commandPool = m_commandPool;
|
||
allocInfo.commandBufferCount = 1;
|
||
|
||
vk::CommandBuffer commandBuffer = m_device.allocateCommandBuffers(allocInfo)[0];
|
||
|
||
vk::CommandBufferBeginInfo beginInfo;
|
||
beginInfo.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
|
||
|
||
commandBuffer.begin(beginInfo);
|
||
|
||
return commandBuffer;
|
||
}
|
||
|
||
void VulkanTextureManager::endSingleTimeCommands(vk::CommandBuffer commandBuffer)
|
||
{
|
||
commandBuffer.end();
|
||
|
||
vk::SubmitInfo submitInfo;
|
||
submitInfo.commandBufferCount = 1;
|
||
submitInfo.pCommandBuffers = &commandBuffer;
|
||
|
||
m_graphicsQueue.submit(submitInfo, nullptr);
|
||
m_graphicsQueue.waitIdle();
|
||
|
||
m_device.freeCommandBuffers(m_commandPool, commandBuffer);
|
||
}
|
||
|
||
void VulkanTextureManager::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,
|
||
const SCP_vector<vk::BufferImageCopy>& regions,
|
||
uint32_t arrayLayers)
|
||
{
|
||
(void)format; // May be needed for depth/stencil transitions in the future
|
||
|
||
// Barrier 1: oldLayout -> eTransferDstOptimal (all mip levels, all layers).
|
||
// Destination stage is always eCopy: both branches below are unconditionally
|
||
// followed by a cmd.copyBufferToImage() a few lines down in this function.
|
||
{
|
||
ImageBarrier2 barrier;
|
||
barrier.image = image;
|
||
barrier.baseMipLevel = 0;
|
||
barrier.levelCount = mipLevels;
|
||
barrier.baseArrayLayer = 0;
|
||
barrier.layerCount = arrayLayers;
|
||
barrier.oldLayout = oldLayout;
|
||
barrier.newLayout = vk::ImageLayout::eTransferDstOptimal;
|
||
barrier.dstStage = vk::PipelineStageFlagBits2::eCopy;
|
||
barrier.dstAccess = vk::AccessFlagBits2::eTransferWrite;
|
||
|
||
if (oldLayout == vk::ImageLayout::eUndefined) {
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eTopOfPipe;
|
||
barrier.srcAccess = {};
|
||
} else {
|
||
// The image's prior producer isn't known at this generic reuse
|
||
// site (this path re-uploads an image that could have last been
|
||
// used in any number of ways) -- stay maximally conservative
|
||
// rather than guess.
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eAllCommands;
|
||
barrier.srcAccess = vk::AccessFlagBits2::eMemoryWrite;
|
||
}
|
||
|
||
cmdImageBarrier(cmd, barrier);
|
||
}
|
||
|
||
if (!regions.empty()) {
|
||
// Pre-baked mip levels: copy all regions (one per mip level) from the staging buffer
|
||
cmd.copyBufferToImage(stagingBuffer, image, vk::ImageLayout::eTransferDstOptimal,
|
||
static_cast<uint32_t>(regions.size()), regions.data());
|
||
} else {
|
||
// Single mip-0 copy
|
||
vk::BufferImageCopy region;
|
||
region.bufferOffset = 0;
|
||
region.bufferRowLength = 0;
|
||
region.bufferImageHeight = 0;
|
||
region.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
|
||
region.imageSubresource.mipLevel = 0;
|
||
region.imageSubresource.baseArrayLayer = 0;
|
||
region.imageSubresource.layerCount = arrayLayers;
|
||
region.imageOffset = vk::Offset3D(0, 0, 0);
|
||
region.imageExtent = vk::Extent3D(width, height, 1);
|
||
|
||
cmd.copyBufferToImage(stagingBuffer, image, vk::ImageLayout::eTransferDstOptimal, region);
|
||
}
|
||
|
||
if (generateMips && mipLevels > 1 && regions.empty()) {
|
||
// Generate mipmaps via blit chain: upload mip 0, then downsample each level
|
||
|
||
// Transition mip 0 from eTransferDstOptimal to eTransferSrcOptimal
|
||
// (source for the blit chain below)
|
||
{
|
||
ImageBarrier2 barrier;
|
||
barrier.image = image;
|
||
barrier.baseMipLevel = 0;
|
||
barrier.levelCount = 1;
|
||
barrier.baseArrayLayer = 0;
|
||
barrier.layerCount = arrayLayers;
|
||
barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
|
||
barrier.newLayout = vk::ImageLayout::eTransferSrcOptimal;
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eCopy;
|
||
barrier.srcAccess = vk::AccessFlagBits2::eTransferWrite;
|
||
barrier.dstStage = vk::PipelineStageFlagBits2::eBlit;
|
||
barrier.dstAccess = vk::AccessFlagBits2::eTransferRead;
|
||
|
||
cmdImageBarrier(cmd, barrier);
|
||
}
|
||
|
||
vulkan_generate_mipmap_chain(cmd, image, width, height, mipLevels, arrayLayers);
|
||
} else {
|
||
// Simple transition: all mips from eTransferDstOptimal to eShaderReadOnlyOptimal
|
||
{
|
||
ImageBarrier2 barrier;
|
||
barrier.image = image;
|
||
barrier.baseMipLevel = 0;
|
||
barrier.levelCount = mipLevels;
|
||
barrier.baseArrayLayer = 0;
|
||
barrier.layerCount = arrayLayers;
|
||
barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
|
||
barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
|
||
barrier.srcStage = vk::PipelineStageFlagBits2::eCopy;
|
||
barrier.srcAccess = vk::AccessFlagBits2::eTransferWrite;
|
||
barrier.dstStage = vk::PipelineStageFlagBits2::eFragmentShader;
|
||
barrier.dstAccess = vk::AccessFlagBits2::eShaderSampledRead;
|
||
|
||
cmdImageBarrier(cmd, barrier);
|
||
}
|
||
}
|
||
}
|
||
|
||
void VulkanTextureManager::submitUploadAsync(vk::CommandBuffer cmd, vk::Buffer stagingBuffer,
|
||
VulkanAllocation stagingAllocation)
|
||
{
|
||
cmd.end();
|
||
|
||
vk::SubmitInfo submitInfo;
|
||
submitInfo.commandBufferCount = 1;
|
||
submitInfo.pCommandBuffers = &cmd;
|
||
|
||
m_graphicsQueue.submit(submitInfo, nullptr);
|
||
|
||
// Defer staging buffer destruction (2 frames matches MAX_FRAMES_IN_FLIGHT)
|
||
auto* deletionQueue = getDeletionQueue();
|
||
deletionQueue->queueBuffer(stagingBuffer, stagingAllocation);
|
||
|
||
// Defer command buffer free
|
||
m_pendingCommandBuffers.push_back({cmd, VulkanDeletionQueue::FRAMES_TO_WAIT});
|
||
}
|
||
|
||
void VulkanTextureManager::processPendingCommandBuffers()
|
||
{
|
||
auto it = m_pendingCommandBuffers.begin();
|
||
while (it != m_pendingCommandBuffers.end()) {
|
||
if (it->framesRemaining == 0) {
|
||
m_device.freeCommandBuffers(m_commandPool, it->cb);
|
||
it = m_pendingCommandBuffers.erase(it);
|
||
} else {
|
||
it->framesRemaining--;
|
||
++it;
|
||
}
|
||
}
|
||
}
|
||
|
||
uint32_t VulkanTextureManager::calculateMipLevels(uint32_t width, uint32_t height)
|
||
{
|
||
return static_cast<uint32_t>(std::floor(std::log2(std::max(width, height)))) + 1;
|
||
}
|
||
|
||
|
||
} // namespace graphics::vulkan
|