#include "VulkanTexture.h" #include #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(mipW), static_cast(mipH), static_cast(l.blockSize)); } return static_cast(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& 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(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 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 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(ts->arrayIndex); int numFrames = static_cast(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(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(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(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(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(bm->w); auto height = static_cast(bm->h); auto arrayLayerCount = static_cast(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(bm_get_num_mipmaps(handle)); mipLevels = std::max(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 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(frame - baseFrame); size_t layerOffset = layerIndex * layerDataSize; uint8_t* dst = static_cast(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(frameBm->data), dst, static_cast(width) * height); } else { memcpy(dst, reinterpret_cast(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(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(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(slot->gr_info); auto faceW = static_cast(bm->w); auto faceH = static_cast(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(bm_get_num_mipmaps(handle)); mipLevels = std::max(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(bm->data), static_cast(mapped), (perFaceSize / 4) * 6); } else { memcpy(mapped, reinterpret_cast(bm->data), totalDataSize); } // Build per-face, per-mip copy regions (each face is one array layer) SCP_vector 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(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(slot->gr_info); auto width = static_cast(bm->w); auto height = static_cast(bm->h); auto depth3D = static_cast(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(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(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(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(baseSlot->gr_info); if (baseTs->image && baseTs->arrayLayers == static_cast(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(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(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(slot->gr_info); auto width = static_cast(bm->w); auto height = static_cast(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 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(bm_get_num_mipmaps(handle)); mipLevels = std::max(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(bm->data), dataSize); } else if (bm->bpp == 24) { graphics::util::expand_BGR_to_BGRA(reinterpret_cast(bm->data), static_cast(mapped), static_cast(width) * height); } else { memcpy(mapped, reinterpret_cast(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(*width) > m_maxTextureSize) { *width = static_cast(m_maxTextureSize); } if (static_cast(*height) > m_maxTextureSize) { *height = static_cast(m_maxTextureSize); } auto* slot = bm_get_slot(handle, true); if (!slot) { return 0; } if (!slot->gr_info) { bm_init(slot); } auto* ts = static_cast(slot->gr_info); // Free any existing resources bm_free_data(slot, false); auto w = static_cast(*width); auto h = static_cast(*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(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(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 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 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(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 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(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 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(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(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(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(slot->gr_info); if (!ts->image) { return; } auto w = static_cast(width); auto h = static_cast(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(format), static_cast(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(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(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(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(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(blockW) * static_cast(blockH) * static_cast(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(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(vm_malloc(static_cast(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(copyW) * static_cast(copyH) * 4; SCP_vector temp(decompressedSize); const auto* srcBlock = static_cast(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(copyW * 4)); break; case vk::Format::eBc2UnormBlock: bcdec_bc2(srcBlock, blockColBase, static_cast(copyW * 4)); break; case vk::Format::eBc3UnormBlock: bcdec_bc3(srcBlock, blockColBase, static_cast(copyW * 4)); break; case vk::Format::eBc7UnormBlock: bcdec_bc7(srcBlock, blockColBase, static_cast(copyW * 4)); break; case vk::Format::eEtc2R8G8B8UnormBlock: case vk::Format::eEtc2R8G8B8SrgbBlock: etcdec_etc_rgb(srcBlock, blockColBase, static_cast(copyW * 4)); break; case vk::Format::eEtc2R8G8B8A1UnormBlock: case vk::Format::eEtc2R8G8B8A1SrgbBlock: etcdec_etc_rgb_a1(srcBlock, blockColBase, static_cast(copyW * 4)); break; case vk::Format::eEtc2R8G8B8A8UnormBlock: case vk::Format::eEtc2R8G8B8A8SrgbBlock: etcdec_eac_rgba(srcBlock, blockColBase, static_cast(copyW * 4)); break; default: break; } srcBlock += blockSize; blockColBase += 4 * 4; // 4 pixels wide × 4 bytes/pixel } blockRowBase += static_cast(4) * copyW * 4; // 4 rows × full stride } const auto* src = temp.data(); auto* dst = static_cast(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(mapped); auto* dst = static_cast(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(src), dst, pixelCount); } else { graphics::util::convert_BGRA1555_REV_to_RGB888(reinterpret_cast(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(magFilter) << 0; key |= static_cast(minFilter) << 4; key |= static_cast(addressMode) << 8; key |= static_cast(enableAnisotropy) << 16; key |= static_cast(enableMipmaps) << 17; key |= static_cast(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(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(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(oldLayout), static_cast(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(srcW), static_cast(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(dstW), static_cast(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 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(bufferSize)); m_memoryManager->unmapMemory(stagingAlloc); SCP_vector 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& 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(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(std::floor(std::log2(std::max(width, height)))) + 1; } } // namespace graphics::vulkan