From 90c48e1d210ad872f6a415aac4b01d1ef541ffe1 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Tue, 13 Jan 2026 21:33:49 +0900 Subject: [PATCH] [GPU] 3d-to-d2 texture implementation Adds vulkan version of mode 1 and 2 and fixes related crashes by keeping the 2d texture views from the texture cache. --- src/xenia/gpu/d3d12/d3d12_texture_cache.cc | 22 +- src/xenia/gpu/d3d12/d3d12_texture_cache.h | 4 +- src/xenia/gpu/gpu_flags.cc | 6 + src/xenia/gpu/gpu_flags.h | 2 + src/xenia/gpu/texture_cache.cc | 44 +-- src/xenia/gpu/texture_cache.h | 9 +- .../gpu/vulkan/deferred_command_buffer.cc | 10 + .../gpu/vulkan/deferred_command_buffer.h | 37 +++ src/xenia/gpu/vulkan/vulkan_texture_cache.cc | 277 +++++++++++++++++- src/xenia/gpu/vulkan/vulkan_texture_cache.h | 20 +- src/xenia/ui/vulkan/functions/device_1_0.inc | 1 + 11 files changed, 389 insertions(+), 43 deletions(-) diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc index 648256293..7a788d641 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.cc +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.cc @@ -15,7 +15,6 @@ #include #include "xenia/base/assert.h" -#include "xenia/base/cvar.h" #include "xenia/base/logging.h" #include "xenia/base/math.h" #include "xenia/base/profiling.h" @@ -28,12 +27,6 @@ #include "xenia/ui/d3d12/d3d12_upload_buffer_pool.h" #include "xenia/ui/d3d12/d3d12_util.h" -DEFINE_int32(d3d12_3d_to_2d_texture_mode, 0, - "Handle shaders that sample 3D textures as 2D by creating a 2D " - "view of slice 0. 0 = disabled (default), 1 = GPU copy, " - "2 = CPU re-upload from guest memory.", - "D3D12"); - namespace xe { namespace gpu { namespace d3d12 { @@ -1235,8 +1228,9 @@ ID3D12Resource* D3D12TextureCache::RequestSwapTexture( D3D12TextureCache::D3D12Texture::D3D12Texture( D3D12TextureCache& texture_cache, const TextureKey& key, - ID3D12Resource* resource, D3D12_RESOURCE_STATES resource_state) - : Texture(texture_cache, key), + ID3D12Resource* resource, D3D12_RESOURCE_STATES resource_state, + bool track_usage) + : Texture(texture_cache, key, track_usage), resource_(resource), resource_state_(resource_state) { ID3D12Device* device = @@ -1842,7 +1836,7 @@ void D3D12TextureCache::UpdateTextureBindingsImpl( ID3D12Resource* D3D12TextureCache::D3D12Texture::GetOrCreate3DAs2DResource( D3D12_RESOURCE_STATES end_state) { - int32_t mode = cvars::d3d12_3d_to_2d_texture_mode; + int32_t mode = cvars::gpu_3d_to_2d_texture_mode; // Mode 0: Feature disabled. if (mode == 0) { @@ -1922,16 +1916,18 @@ ID3D12Resource* D3D12TextureCache::D3D12Texture::GetOrCreate3DAs2DResource( TextureKey key_2d = key(); key_2d.depth_or_array_size_minus_1 = 0; key_2d.mip_max_level = 0; - texture_3d_as_2d_.reset(new D3D12Texture(d3d12_cache, key_2d, - resource_2d.Get(), initial_state)); + texture_3d_as_2d_.reset(new D3D12Texture( + d3d12_cache, key_2d, resource_2d.Get(), initial_state, false)); } else { // Mode 2: CPU re-upload - reload texture data from guest memory. + // Keep dimension as k3D so LoadTextureData uses 3D tiling math to correctly + // read slice 0 from the 3D-tiled guest memory. TextureKey key_load = key(); key_load.depth_or_array_size_minus_1 = 0; key_load.mip_max_level = 0; std::unique_ptr texture_wrap(new D3D12Texture( - d3d12_cache, key_load, resource_2d.Get(), initial_state)); + d3d12_cache, key_load, resource_2d.Get(), initial_state, false)); if (!d3d12_cache.LoadTextureData(*texture_wrap)) { XELOGE("D3D12Texture: Failed to untile 3D-as-2D data"); diff --git a/src/xenia/gpu/d3d12/d3d12_texture_cache.h b/src/xenia/gpu/d3d12/d3d12_texture_cache.h index d6cae339b..d03244774 100644 --- a/src/xenia/gpu/d3d12/d3d12_texture_cache.h +++ b/src/xenia/gpu/d3d12/d3d12_texture_cache.h @@ -571,9 +571,11 @@ class D3D12TextureCache final : public TextureCache { ID3D12Resource* GetOrCreate3DAs2DResource(D3D12_RESOURCE_STATES end_state); + // track_usage: if false, texture won't participate in LRU cache eviction. explicit D3D12Texture(D3D12TextureCache& texture_cache, const TextureKey& key, ID3D12Resource* resource, - D3D12_RESOURCE_STATES resource_state); + D3D12_RESOURCE_STATES resource_state, + bool track_usage = true); ~D3D12Texture(); ID3D12Resource* resource() const { return resource_.Get(); } diff --git a/src/xenia/gpu/gpu_flags.cc b/src/xenia/gpu/gpu_flags.cc index 5ab1af8ed..21634dec8 100644 --- a/src/xenia/gpu/gpu_flags.cc +++ b/src/xenia/gpu/gpu_flags.cc @@ -89,3 +89,9 @@ DEFINE_bool(no_discard_stencil_in_transfer_pipelines, false, "Skip stencil bit discard in render target transfer pipelines. " "May improve performance on some GPUs.", "GPU"); + +DEFINE_int32(gpu_3d_to_2d_texture_mode, 2, + "Handle shaders that sample 3D textures as 2D by creating a 2D " + "copy of slice 0. 0 = disabled, 1 = GPU copy, " + "2 = CPU re-upload from guest memory (default).", + "GPU"); diff --git a/src/xenia/gpu/gpu_flags.h b/src/xenia/gpu/gpu_flags.h index bfeefdc53..e73854223 100644 --- a/src/xenia/gpu/gpu_flags.h +++ b/src/xenia/gpu/gpu_flags.h @@ -36,6 +36,8 @@ DECLARE_bool(disassemble_pm4); DECLARE_bool(no_discard_stencil_in_transfer_pipelines); +DECLARE_int32(gpu_3d_to_2d_texture_mode); + #define XE_GPU_FINE_GRAINED_DRAW_SCOPES 1 #endif // XENIA_GPU_GPU_FLAGS_H_ diff --git a/src/xenia/gpu/texture_cache.cc b/src/xenia/gpu/texture_cache.cc index 357e7be99..e0e55aa09 100644 --- a/src/xenia/gpu/texture_cache.cc +++ b/src/xenia/gpu/texture_cache.cc @@ -504,7 +504,7 @@ void TextureCache::Texture::LogAction(const char* action) const { // performed somehow. The list is maintained by the Texture, not the // TextureCache itself (unlike the `textures_` container). TextureCache::Texture::Texture(TextureCache& texture_cache, - const TextureKey& key) + const TextureKey& key, bool track_usage) : texture_cache_(texture_cache), key_(key), guest_layout_(key.GetGuestLayout()), @@ -512,14 +512,17 @@ TextureCache::Texture::Texture(TextureCache& texture_cache, mips_resolved_(key.scaled_resolve), last_usage_submission_index_(texture_cache.current_submission_index_), last_usage_time_(texture_cache.current_submission_time_), - used_previous_(texture_cache.texture_used_last_), - used_next_(nullptr) { - if (texture_cache.texture_used_last_) { - texture_cache.texture_used_last_->used_next_ = this; - } else { - texture_cache.texture_used_first_ = this; + used_previous_(track_usage ? texture_cache.texture_used_last_ : nullptr), + used_next_(nullptr), + in_usage_list_(track_usage) { + if (track_usage) { + if (texture_cache.texture_used_last_) { + texture_cache.texture_used_last_->used_next_ = this; + } else { + texture_cache.texture_used_first_ = this; + } + texture_cache.texture_used_last_ = this; } - texture_cache.texture_used_last_ = this; // Never try to upload data that doesn't exist. base_outdated_ = guest_layout().base.level_data_extent_bytes != 0; @@ -534,15 +537,18 @@ TextureCache::Texture::~Texture() { texture_cache().shared_memory().UnwatchMemoryRange(base_watch_handle_); } - if (used_previous_) { - used_previous_->used_next_ = used_next_; - } else { - texture_cache_.texture_used_first_ = used_next_; - } - if (used_next_) { - used_next_->used_previous_ = used_previous_; - } else { - texture_cache_.texture_used_last_ = used_previous_; + // Only remove from usage list if we were added to it (track_usage=true). + if (in_usage_list_) { + if (used_previous_) { + used_previous_->used_next_ = used_next_; + } else { + texture_cache_.texture_used_first_ = used_next_; + } + if (used_next_) { + used_next_->used_previous_ = used_previous_; + } else { + texture_cache_.texture_used_last_ = used_previous_; + } } texture_cache_.UpdateTexturesTotalHostMemoryUsage(0, host_memory_usage_); @@ -568,6 +574,10 @@ void TextureCache::Texture::MakeUpToDateAndWatch( } void TextureCache::Texture::MarkAsUsed() { + // Textures not in usage tracking (track_usage=false) should not be linked. + if (!in_usage_list_) { + return; + } assert_true(last_usage_submission_index_ <= texture_cache_.current_submission_index_); // This is called very frequently, don't relink unless needed for caching. diff --git a/src/xenia/gpu/texture_cache.h b/src/xenia/gpu/texture_cache.h index 4ceafa5aa..a8d1729dd 100644 --- a/src/xenia/gpu/texture_cache.h +++ b/src/xenia/gpu/texture_cache.h @@ -294,7 +294,11 @@ class TextureCache { void LogAction(const char* action) const; protected: - explicit Texture(TextureCache& texture_cache, const TextureKey& key); + // track_usage: if false, the texture won't be added to the LRU tracking + // list. Use this for wrapper textures that shouldn't participate in cache + // eviction (like texture_3d_as_2d_ wrappers). + explicit Texture(TextureCache& texture_cache, const TextureKey& key, + bool track_usage = true); void SetHostMemoryUsage(uint64_t new_host_memory_usage) { texture_cache_.UpdateTexturesTotalHostMemoryUsage(new_host_memory_usage, @@ -315,6 +319,9 @@ class TextureCache { uint64_t last_usage_time_; Texture* used_previous_; Texture* used_next_; + // Whether this texture is in the usage tracking list (for LRU eviction). + // Set to false via constructor for wrapper textures. + bool in_usage_list_; // Whether the most up-to-date base / mips contain pages with data from a // resolve operation (rather than from the CPU or memexport), primarily for diff --git a/src/xenia/gpu/vulkan/deferred_command_buffer.cc b/src/xenia/gpu/vulkan/deferred_command_buffer.cc index 0ddfef765..b533b625d 100644 --- a/src/xenia/gpu/vulkan/deferred_command_buffer.cc +++ b/src/xenia/gpu/vulkan/deferred_command_buffer.cc @@ -175,6 +175,16 @@ void DeferredCommandBuffer::Execute(VkCommandBuffer command_buffer) { args.filter); } break; + case Command::kVkCopyImage: { + auto& args = *reinterpret_cast(stream); + dfn.vkCmdCopyImage( + command_buffer, args.src_image, args.src_image_layout, + args.dst_image, args.dst_image_layout, args.region_count, + reinterpret_cast( + reinterpret_cast(stream) + + xe::align(sizeof(ArgsVkCopyImage), alignof(VkImageCopy)))); + } break; + case Command::kVkDispatch: { auto& args = *reinterpret_cast(stream); dfn.vkCmdDispatch(command_buffer, args.group_count_x, diff --git a/src/xenia/gpu/vulkan/deferred_command_buffer.h b/src/xenia/gpu/vulkan/deferred_command_buffer.h index 4fef1ee6f..8352e06d4 100644 --- a/src/xenia/gpu/vulkan/deferred_command_buffer.h +++ b/src/xenia/gpu/vulkan/deferred_command_buffer.h @@ -260,6 +260,32 @@ class DeferredCommandBuffer { regions, sizeof(VkImageBlit) * region_count); } + VkImageCopy* CmdCopyImageEmplace(VkImage src_image, + VkImageLayout src_image_layout, + VkImage dst_image, + VkImageLayout dst_image_layout, + uint32_t region_count) { + const size_t header_size = + xe::align(sizeof(ArgsVkCopyImage), alignof(VkImageCopy)); + uint8_t* args_ptr = reinterpret_cast( + WriteCommand(Command::kVkCopyImage, + header_size + sizeof(VkImageCopy) * region_count)); + auto& args = *reinterpret_cast(args_ptr); + args.src_image = src_image; + args.src_image_layout = src_image_layout; + args.dst_image = dst_image; + args.dst_image_layout = dst_image_layout; + args.region_count = region_count; + return reinterpret_cast(args_ptr + header_size); + } + void CmdVkCopyImage(VkImage src_image, VkImageLayout src_image_layout, + VkImage dst_image, VkImageLayout dst_image_layout, + uint32_t region_count, const VkImageCopy* regions) { + std::memcpy(CmdCopyImageEmplace(src_image, src_image_layout, dst_image, + dst_image_layout, region_count), + regions, sizeof(VkImageCopy) * region_count); + } + void CmdVkDispatch(uint32_t group_count_x, uint32_t group_count_y, uint32_t group_count_z) { auto& args = *reinterpret_cast( @@ -398,6 +424,7 @@ class DeferredCommandBuffer { kVkCopyBuffer, kVkCopyBufferToImage, kVkBlitImage, + kVkCopyImage, kVkDispatch, kVkDraw, kVkDrawIndexed, @@ -504,6 +531,16 @@ class DeferredCommandBuffer { static_assert(alignof(VkImageBlit) <= alignof(uintmax_t)); }; + struct ArgsVkCopyImage { + VkImage src_image; + VkImageLayout src_image_layout; + VkImage dst_image; + VkImageLayout dst_image_layout; + uint32_t region_count; + // Followed by aligned VkImageCopy[]. + static_assert(alignof(VkImageCopy) <= alignof(uintmax_t)); + }; + struct ArgsVkDispatch { uint32_t group_count_x; uint32_t group_count_y; diff --git a/src/xenia/gpu/vulkan/vulkan_texture_cache.cc b/src/xenia/gpu/vulkan/vulkan_texture_cache.cc index 26cd9123a..79d581f90 100644 --- a/src/xenia/gpu/vulkan/vulkan_texture_cache.cc +++ b/src/xenia/gpu/vulkan/vulkan_texture_cache.cc @@ -605,14 +605,34 @@ void VulkanTextureCache::RequestTextures(uint32_t used_texture_mask) { VkImageView VulkanTextureCache::GetActiveBindingOrNullImageView( uint32_t fetch_constant_index, xenos::FetchOpDimension dimension, - bool is_signed) const { + bool is_signed) { VkImageView image_view = VK_NULL_HANDLE; const TextureBinding* binding = GetValidTextureBinding(fetch_constant_index); if (binding && AreDimensionsCompatible(dimension, binding->key.dimension)) { - const VulkanTextureBinding& vulkan_binding = - vulkan_texture_bindings_[fetch_constant_index]; - image_view = is_signed ? vulkan_binding.image_view_signed - : vulkan_binding.image_view_unsigned; + // Check for 3D texture sampled as 2D. + bool force_special_view = + (dimension == xenos::FetchOpDimension::k2D && + binding->key.dimension == xenos::DataDimension::k3D); + + if (force_special_view) { + // Get the appropriate texture for signed/unsigned. + Texture* texture = nullptr; + if (is_signed && IsSignedVersionSeparateForFormat(binding->key)) { + texture = binding->texture_signed; + } else { + texture = binding->texture; + } + if (texture) { + image_view = + static_cast(texture)->GetOrCreate3DAs2DImageView( + is_signed, binding->host_swizzle); + } + } else { + const VulkanTextureBinding& vulkan_binding = + vulkan_texture_bindings_[fetch_constant_index]; + image_view = is_signed ? vulkan_binding.image_view_signed + : vulkan_binding.image_view_unsigned; + } } if (image_view != VK_NULL_HANDLE) { return image_view; @@ -1081,7 +1101,8 @@ std::unique_ptr VulkanTextureCache::CreateTexture( VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; // For scaled resolve textures with mips, we need transfer source to generate // mip levels via blit from the base level. - if (key.scaled_resolve && key.mip_max_level > 0) { + // For 3D textures, we need transfer source to support 3D-to-2D conversion. + if ((key.scaled_resolve && key.mip_max_level > 0) || is_3d) { image_create_info.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; } image_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; @@ -1765,8 +1786,10 @@ void VulkanTextureCache::UpdateTextureBindingsImpl( VulkanTextureCache::VulkanTexture::VulkanTexture( VulkanTextureCache& texture_cache, const TextureKey& key, VkImage image, - VmaAllocation allocation) - : Texture(texture_cache, key), image_(image), allocation_(allocation) { + VmaAllocation allocation, bool track_usage) + : Texture(texture_cache, key, track_usage), + image_(image), + allocation_(allocation) { VmaAllocationInfo allocation_info; vmaGetAllocationInfo(texture_cache.vma_allocator_, allocation_, &allocation_info); @@ -1783,6 +1806,15 @@ VulkanTextureCache::VulkanTexture::~VulkanTexture() { for (const auto& view_pair : views_) { dfn.vkDestroyImageView(device, view_pair.second, nullptr); } + // Clean up 3D-as-2D image views. The texture_3d_as_2d_ wrapper will clean + // itself up via unique_ptr destructor. + if (image_view_3d_as_2d_unsigned_ != VK_NULL_HANDLE) { + dfn.vkDestroyImageView(device, image_view_3d_as_2d_unsigned_, nullptr); + } + if (image_view_3d_as_2d_signed_ != VK_NULL_HANDLE) { + dfn.vkDestroyImageView(device, image_view_3d_as_2d_signed_, nullptr); + } + // texture_3d_as_2d_ is a unique_ptr and will destroy its image/allocation. vmaDestroyImage(vulkan_texture_cache.vma_allocator_, image_, allocation_); } @@ -1877,6 +1909,235 @@ VkImageView VulkanTextureCache::VulkanTexture::GetView(bool is_signed, return view; } +VkImageView VulkanTextureCache::VulkanTexture::GetOrCreate3DAs2DImageView( + bool is_signed, uint32_t host_swizzle) { + int32_t mode = cvars::gpu_3d_to_2d_texture_mode; + + // Mode 0: Feature disabled. + if (mode == 0) { + return VK_NULL_HANDLE; + } + + // Return cached view if available. + VkImageView& cached_view = + is_signed ? image_view_3d_as_2d_signed_ : image_view_3d_as_2d_unsigned_; + if (cached_view != VK_NULL_HANDLE) { + return cached_view; + } + + VulkanTextureCache& vulkan_texture_cache = + static_cast(texture_cache()); + const ui::vulkan::VulkanDevice* const vulkan_device = + vulkan_texture_cache.command_processor_.GetVulkanDevice(); + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); + const VkDevice device = vulkan_device->device(); + + // Create the 2D texture wrapper if it doesn't exist. + if (!texture_3d_as_2d_) { + const HostFormatPair& host_format_pair = + vulkan_texture_cache.GetHostFormatPair(key()); + VkFormat format = host_format_pair.format_unsigned.format; + if (format == VK_FORMAT_UNDEFINED) { + return VK_NULL_HANDLE; + } + + VkImageCreateInfo image_create_info = {}; + image_create_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + image_create_info.imageType = VK_IMAGE_TYPE_2D; + image_create_info.format = format; + image_create_info.extent.width = key().GetWidth(); + image_create_info.extent.height = key().GetHeight(); + image_create_info.extent.depth = 1; + image_create_info.mipLevels = 1; + image_create_info.arrayLayers = 1; + image_create_info.samples = VK_SAMPLE_COUNT_1_BIT; + image_create_info.tiling = VK_IMAGE_TILING_OPTIMAL; + image_create_info.usage = + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + image_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + image_create_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VmaAllocationCreateInfo allocation_create_info = {}; + allocation_create_info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + + VkImage image_2d; + VmaAllocation allocation_2d; + if (vmaCreateImage(vulkan_texture_cache.vma_allocator_, &image_create_info, + &allocation_create_info, &image_2d, &allocation_2d, + nullptr) != VK_SUCCESS) { + XELOGE("VulkanTexture: Failed to create 3D-as-2D image"); + return VK_NULL_HANDLE; + } + + // Create a modified key for the 2D wrapper with depth=1 and + // mip_max_level=0. Keep dimension as k3D so that in Mode 2, LoadTextureData + // uses 3D tiling math to correctly read slice 0 from the 3D-tiled guest + // memory. + TextureKey key_2d = key(); + key_2d.depth_or_array_size_minus_1 = 0; + key_2d.mip_max_level = 0; + + if (mode == 1) { + // Mode 1: GPU copy - copy slice 0 from the 3D image to the 2D image. + DeferredCommandBuffer& command_buffer = + vulkan_texture_cache.command_processor_.deferred_command_buffer(); + + // Get the current layout from usage tracking (like D3D12 does). + VkPipelineStageFlags src_stage_mask; + VkAccessFlags src_access_mask; + VkImageLayout src_old_layout; + vulkan_texture_cache.GetTextureUsageMasks( + usage_, src_stage_mask, src_access_mask, src_old_layout); + + // Transition 2D image to transfer destination. + VkImageMemoryBarrier barrier_2d_to_dst = {}; + barrier_2d_to_dst.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier_2d_to_dst.srcAccessMask = 0; + barrier_2d_to_dst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier_2d_to_dst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier_2d_to_dst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier_2d_to_dst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier_2d_to_dst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier_2d_to_dst.image = image_2d; + barrier_2d_to_dst.subresourceRange = + ui::vulkan::util::InitializeSubresourceRange(); + command_buffer.CmdVkPipelineBarrier( + VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + 0, nullptr, 0, nullptr, 1, &barrier_2d_to_dst); + + // Transition 3D image to transfer source. + VkImageMemoryBarrier barrier_3d_to_src = {}; + barrier_3d_to_src.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier_3d_to_src.srcAccessMask = src_access_mask; + barrier_3d_to_src.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier_3d_to_src.oldLayout = src_old_layout; + barrier_3d_to_src.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier_3d_to_src.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier_3d_to_src.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier_3d_to_src.image = image_; + barrier_3d_to_src.subresourceRange = + ui::vulkan::util::InitializeSubresourceRange(); + command_buffer.CmdVkPipelineBarrier( + src_stage_mask, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, + nullptr, 1, &barrier_3d_to_src); + + // Use vkCmdCopyImage for the slice copy. + // This works for all formats including compressed (BC) formats. + VkImageCopy copy_region = {}; + copy_region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + copy_region.srcSubresource.mipLevel = 0; + copy_region.srcSubresource.baseArrayLayer = 0; + copy_region.srcSubresource.layerCount = 1; + copy_region.srcOffset = {0, 0, 0}; + copy_region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + copy_region.dstSubresource.mipLevel = 0; + copy_region.dstSubresource.baseArrayLayer = 0; + copy_region.dstSubresource.layerCount = 1; + copy_region.dstOffset = {0, 0, 0}; + copy_region.extent.width = key().GetWidth(); + copy_region.extent.height = key().GetHeight(); + copy_region.extent.depth = 1; + command_buffer.CmdVkCopyImage( + image_, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image_2d, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©_region); + + // Transition 3D image back to guest shader sampled state. + VkPipelineStageFlags dst_stage_mask; + VkAccessFlags dst_access_mask; + VkImageLayout new_layout; + vulkan_texture_cache.GetTextureUsageMasks(Usage::kGuestShaderSampled, + dst_stage_mask, dst_access_mask, + new_layout); + barrier_3d_to_src.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier_3d_to_src.dstAccessMask = dst_access_mask; + barrier_3d_to_src.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier_3d_to_src.newLayout = new_layout; + command_buffer.CmdVkPipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, + dst_stage_mask, 0, 0, nullptr, 0, + nullptr, 1, &barrier_3d_to_src); + // Update tracking - texture is now in guest shader sampled state. + SetUsage(Usage::kGuestShaderSampled); + + // Transition 2D image to shader read. + barrier_2d_to_dst.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier_2d_to_dst.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier_2d_to_dst.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier_2d_to_dst.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + command_buffer.CmdVkPipelineBarrier( + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier_2d_to_dst); + + // Create the wrapper after successful GPU copy. + texture_3d_as_2d_.reset(new VulkanTexture( + vulkan_texture_cache, key_2d, image_2d, allocation_2d, false)); + texture_3d_as_2d_->SetUsage(Usage::kGuestShaderSampled); + } else { + // Mode 2: CPU re-upload - reload texture data from guest memory. + // Create the wrapper first so LoadTextureData can work with it. + texture_3d_as_2d_.reset(new VulkanTexture( + vulkan_texture_cache, key_2d, image_2d, allocation_2d, false)); + + if (!vulkan_texture_cache.LoadTextureData(*texture_3d_as_2d_)) { + XELOGE("VulkanTexture: Failed to untile 3D-as-2D data"); + texture_3d_as_2d_.reset(); + return VK_NULL_HANDLE; + } + + // LoadTextureData leaves the texture in TRANSFER_DST state. + // Transition to shader read state. + DeferredCommandBuffer& command_buffer = + vulkan_texture_cache.command_processor_.deferred_command_buffer(); + VkImageMemoryBarrier barrier_to_shader = {}; + barrier_to_shader.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier_to_shader.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier_to_shader.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier_to_shader.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier_to_shader.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier_to_shader.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier_to_shader.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier_to_shader.image = texture_3d_as_2d_->image(); + barrier_to_shader.subresourceRange = + ui::vulkan::util::InitializeSubresourceRange(); + command_buffer.CmdVkPipelineBarrier( + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + 0, 0, nullptr, 0, nullptr, 1, &barrier_to_shader); + texture_3d_as_2d_->SetUsage(Usage::kGuestShaderSampled); + } + } + + // Create the image view. + const HostFormatPair& host_format_pair = + vulkan_texture_cache.GetHostFormatPair(key()); + VkFormat format = (is_signed ? host_format_pair.format_signed + : host_format_pair.format_unsigned) + .format; + if (format == VK_FORMAT_UNDEFINED) { + return VK_NULL_HANDLE; + } + + VkImageViewCreateInfo view_create_info = {}; + view_create_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + view_create_info.image = texture_3d_as_2d_->image(); + // Use 2D_ARRAY to match shader expectations (Dim = 2D, Arrayed = 1). + view_create_info.viewType = VK_IMAGE_VIEW_TYPE_2D_ARRAY; + view_create_info.format = format; + view_create_info.components.r = GetComponentSwizzle(host_swizzle, 0); + view_create_info.components.g = GetComponentSwizzle(host_swizzle, 1); + view_create_info.components.b = GetComponentSwizzle(host_swizzle, 2); + view_create_info.components.a = GetComponentSwizzle(host_swizzle, 3); + view_create_info.subresourceRange = + ui::vulkan::util::InitializeSubresourceRange(); + view_create_info.subresourceRange.layerCount = 1; + + if (dfn.vkCreateImageView(device, &view_create_info, nullptr, &cached_view) != + VK_SUCCESS) { + XELOGE("VulkanTexture: Failed to create 3D-as-2D image view"); + return VK_NULL_HANDLE; + } + + return cached_view; +} + VulkanTextureCache::VulkanTextureCache( const RegisterFile& register_file, VulkanSharedMemory& shared_memory, uint32_t draw_resolution_scale_x, uint32_t draw_resolution_scale_y, diff --git a/src/xenia/gpu/vulkan/vulkan_texture_cache.h b/src/xenia/gpu/vulkan/vulkan_texture_cache.h index 7e8f5525c..00319ddbf 100644 --- a/src/xenia/gpu/vulkan/vulkan_texture_cache.h +++ b/src/xenia/gpu/vulkan/vulkan_texture_cache.h @@ -92,7 +92,7 @@ class VulkanTextureCache final : public TextureCache { VkImageView GetActiveBindingOrNullImageView(uint32_t fetch_constant_index, xenos::FetchOpDimension dimension, - bool is_signed) const; + bool is_signed); SamplerParameters GetSamplerParameters( const VulkanShader::SamplerBinding& binding) const; @@ -254,9 +254,10 @@ class VulkanTextureCache final : public TextureCache { }; // Takes ownership of the image and its memory. + // track_usage: if false, texture won't participate in LRU cache eviction. explicit VulkanTexture(VulkanTextureCache& texture_cache, const TextureKey& key, VkImage image, - VmaAllocation allocation); + VmaAllocation allocation, bool track_usage = true); ~VulkanTexture(); VkImage image() const { return image_; } @@ -271,6 +272,10 @@ class VulkanTextureCache final : public TextureCache { VkImageView GetView(bool is_signed, uint32_t host_swizzle, bool is_array = true); + // For 3D textures sampled as 2D - creates a 2D copy of slice 0. + VkImageView GetOrCreate3DAs2DImageView(bool is_signed, + uint32_t host_swizzle); + private: union ViewKey { uint32_t key; @@ -331,6 +336,14 @@ class VulkanTextureCache final : public TextureCache { Usage usage_ = Usage::kUndefined; std::unordered_map views_; + + // For 3D textures sampled as 2D - cached 2D copy of slice 0. + // This is a wrapper around the 2D image with a modified key (depth=1). + // For Mode 1 (GPU copy), the wrapper is created after the copy. + // For Mode 2 (CPU re-upload), LoadTextureData is called on the wrapper. + std::unique_ptr texture_3d_as_2d_; + VkImageView image_view_3d_as_2d_unsigned_ = VK_NULL_HANDLE; + VkImageView image_view_3d_as_2d_signed_ = VK_NULL_HANDLE; }; struct VulkanTextureBinding { @@ -359,7 +372,8 @@ class VulkanTextureCache final : public TextureCache { case xenos::FetchOpDimension::k1D: case xenos::FetchOpDimension::k2D: return resource_dimension == xenos::DataDimension::k1D || - resource_dimension == xenos::DataDimension::k2DOrStacked; + resource_dimension == xenos::DataDimension::k2DOrStacked || + resource_dimension == xenos::DataDimension::k3D; case xenos::FetchOpDimension::k3DOrStacked: return resource_dimension == xenos::DataDimension::k3D; case xenos::FetchOpDimension::kCube: diff --git a/src/xenia/ui/vulkan/functions/device_1_0.inc b/src/xenia/ui/vulkan/functions/device_1_0.inc index c2c251131..b0c36538c 100644 --- a/src/xenia/ui/vulkan/functions/device_1_0.inc +++ b/src/xenia/ui/vulkan/functions/device_1_0.inc @@ -15,6 +15,7 @@ XE_UI_VULKAN_FUNCTION(vkCmdClearAttachments) XE_UI_VULKAN_FUNCTION(vkCmdClearColorImage) XE_UI_VULKAN_FUNCTION(vkCmdCopyBuffer) XE_UI_VULKAN_FUNCTION(vkCmdCopyBufferToImage) +XE_UI_VULKAN_FUNCTION(vkCmdCopyImage) XE_UI_VULKAN_FUNCTION(vkCmdCopyImageToBuffer) XE_UI_VULKAN_FUNCTION(vkCmdDispatch) XE_UI_VULKAN_FUNCTION(vkCmdDraw)