Files
Xenia-Canary/src/xenia/gpu/vulkan/vulkan_texture_cache.h
Herman S. 90c48e1d21 [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.
2026-02-26 14:22:46 +09:00

488 lines
19 KiB
C++

/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_GPU_VULKAN_VULKAN_TEXTURE_CACHE_H_
#define XENIA_GPU_VULKAN_VULKAN_TEXTURE_CACHE_H_
#include <array>
#include <memory>
#include <unordered_map>
#include <utility>
#include "xenia/base/hash.h"
#include "xenia/gpu/texture_cache.h"
#include "xenia/gpu/vulkan/vulkan_shader.h"
#include "xenia/gpu/vulkan/vulkan_shared_memory.h"
#include "xenia/ui/vulkan/vulkan_mem_alloc.h"
namespace xe {
namespace gpu {
namespace vulkan {
class VulkanCommandProcessor;
class VulkanTextureCache final : public TextureCache {
public:
// Sampler parameters that can be directly converted to a host sampler or used
// for checking whether samplers bindings are up to date.
union SamplerParameters {
uint32_t value;
struct {
xenos::ClampMode clamp_x : 3; // 3
xenos::ClampMode clamp_y : 3; // 6
xenos::ClampMode clamp_z : 3; // 9
xenos::BorderColor border_color : 2; // 11
uint32_t mag_linear : 1; // 12
uint32_t min_linear : 1; // 13
uint32_t mip_linear : 1; // 14
xenos::AnisoFilter aniso_filter : 3; // 17
uint32_t mip_min_level : 4; // 21
uint32_t mip_base_map : 1; // 22
// Maximum mip level is in the texture resource itself, but mip_base_map
// can be used to limit fetching to mip_min_level.
};
SamplerParameters() : value(0) { static_assert_size(*this, sizeof(value)); }
struct Hasher {
size_t operator()(const SamplerParameters& parameters) const {
return std::hash<uint32_t>{}(parameters.value);
}
};
bool operator==(const SamplerParameters& parameters) const {
return value == parameters.value;
}
bool operator!=(const SamplerParameters& parameters) const {
return value != parameters.value;
}
};
// Transient descriptor set layouts must be initialized in the command
// processor.
static std::unique_ptr<VulkanTextureCache> Create(
const RegisterFile& register_file, VulkanSharedMemory& shared_memory,
uint32_t draw_resolution_scale_x, uint32_t draw_resolution_scale_y,
VulkanCommandProcessor& command_processor,
VkPipelineStageFlags guest_shader_pipeline_stages) {
std::unique_ptr<VulkanTextureCache> texture_cache(new VulkanTextureCache(
register_file, shared_memory, draw_resolution_scale_x,
draw_resolution_scale_y, command_processor,
guest_shader_pipeline_stages));
if (!texture_cache->Initialize()) {
return nullptr;
}
return std::move(texture_cache);
}
~VulkanTextureCache();
void BeginSubmission(uint64_t new_submission_index) override;
// Must be called within a frame - creates and untiles textures needed by
// shaders, and enqueues transitioning them into the sampled usage. This may
// bind compute pipelines (notifying the command processor about that), and
// also since it may insert deferred barriers, before flushing the barriers
// preceding host GPU work.
void RequestTextures(uint32_t used_texture_mask) override;
VkImageView GetActiveBindingOrNullImageView(uint32_t fetch_constant_index,
xenos::FetchOpDimension dimension,
bool is_signed);
SamplerParameters GetSamplerParameters(
const VulkanShader::SamplerBinding& binding) const;
// Must be called for every used sampler at least once in a single submission,
// and a submission must be open for this to be callable.
// Returns:
// - The sampler, if obtained successfully - and increases its last usage
// submission index - and has_overflown_out = false.
// - VK_NULL_HANDLE and has_overflown_out = true if there's a total sampler
// count overflow in a submission that potentially hasn't completed yet.
// - VK_NULL_HANDLE and has_overflown_out = false in case of a general failure
// to create a sampler.
VkSampler UseSampler(SamplerParameters parameters, bool& has_overflown_out);
// Returns the submission index to await (may be the current submission in
// case of an overflow within a single submission - in this case, it must be
// ended, and a new one must be started) in case of sampler count overflow, so
// samplers may be freed, and UseSamplers may take their slots.
uint64_t GetSubmissionToAwaitOnSamplerOverflow(
uint32_t overflowed_sampler_count) const;
// Returns the 2D view of the front buffer texture (for fragment shader
// reading - the barrier will be pushed in the command processor if needed),
// or VK_NULL_HANDLE in case of failure. May call LoadTextureData.
VkImageView RequestSwapTexture(uint32_t& width_scaled_out,
uint32_t& height_scaled_out,
xenos::TextureFormat& format_out);
// Scaled resolve buffer management (for use by VulkanRenderTargetCache)
// Simple non-overlapping buffer (fallback when sparse binding unavailable)
struct ScaledResolveBuffer {
VkBuffer buffer = VK_NULL_HANDLE;
VmaAllocation allocation = VK_NULL_HANDLE;
uint64_t size = 0;
uint64_t range_start_scaled = 0;
uint64_t range_length_scaled = 0;
};
// Sparse buffer wrapper for overlapping 2GB windows
class ScaledResolveSparseBuffer {
public:
explicit ScaledResolveSparseBuffer(VkBuffer buffer) : buffer_(buffer) {}
VkBuffer buffer() const { return buffer_; }
private:
VkBuffer buffer_ = VK_NULL_HANDLE;
};
// Constants for sparse scaled resolve
static constexpr uint32_t kScaledResolveHeapSizeLog2 = 24; // 16MB heaps
static constexpr uint32_t kScaledResolveHeapSize =
uint32_t(1) << kScaledResolveHeapSizeLog2;
static constexpr uint64_t kScaledResolveSparseBufferSize =
uint64_t(2) << 30; // 2GB per buffer
// Public scaled resolve buffer methods for use by VulkanRenderTargetCache
bool EnsureScaledResolveMemoryCommittedPublic(
uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2 = 0) {
return EnsureScaledResolveMemoryCommitted(start_unscaled, length_unscaled,
length_scaled_alignment_log2);
}
bool MakeScaledResolveRangeCurrent(uint32_t start_unscaled,
uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2 = 0);
VkBuffer GetCurrentScaledResolveBuffer() const;
// Returns the base scaled address that the current buffer starts at.
// For sparse buffers: buffer N starts at N GB (N << 30)
// For simple buffers: returns the buffer's range_start_scaled
uint64_t GetCurrentScaledResolveBufferBaseOffset() const {
if (sparse_scaled_resolve_supported_) {
return uint64_t(scaled_resolve_current_buffer_index_) << 30;
}
if (scaled_resolve_current_buffer_index_ < scaled_resolve_buffers_.size()) {
return scaled_resolve_buffers_[scaled_resolve_current_buffer_index_]
.range_start_scaled;
}
return 0;
}
size_t GetScaledResolveCurrentBufferIndex() const {
return scaled_resolve_current_buffer_index_;
}
const ScaledResolveBuffer* GetScaledResolveBufferInfo(size_t index) const {
if (index < scaled_resolve_buffers_.size()) {
return &scaled_resolve_buffers_[index];
}
return nullptr;
}
protected:
bool IsScaledResolveSupportedForFormat(TextureKey key) const override;
bool IsSignedVersionSeparateForFormat(TextureKey key) const override;
uint32_t GetHostFormatSwizzle(TextureKey key) const override;
uint32_t GetMaxHostTextureWidthHeight(
xenos::DataDimension dimension) const override;
uint32_t GetMaxHostTextureDepthOrArraySize(
xenos::DataDimension dimension) const override;
std::unique_ptr<Texture> CreateTexture(TextureKey key) override;
bool LoadTextureDataFromResidentMemoryImpl(Texture& texture, bool load_base,
bool load_mips) override;
bool EnsureScaledResolveMemoryCommitted(
uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2 = 0) override;
void UpdateTextureBindingsImpl(uint32_t fetch_constant_mask) override;
private:
enum LoadDescriptorSetIndex {
kLoadDescriptorSetIndexDestination,
kLoadDescriptorSetIndexSource,
kLoadDescriptorSetCount,
};
struct HostFormat {
LoadShaderIndex load_shader;
// Do NOT add integer formats to this - they are not filterable, can only be
// read with ImageFetch, not ImageSample! If any game is seen using
// num_format 1 for fixed-point formats (for floating-point, it's normally
// set to 1 though), add a constant buffer containing multipliers for the
// textures and multiplication to the tfetch implementation.
VkFormat format;
// Whether the format is block-compressed on the host (the host block size
// matches the guest format block size in this case), and isn't decompressed
// on load.
bool block_compressed;
// Set up dynamically based on what's supported by the device.
bool linear_filterable;
};
struct HostFormatPair {
HostFormat format_unsigned;
HostFormat format_signed;
// Mapping of Xenos swizzle components to Vulkan format components.
uint32_t swizzle;
// Whether the unsigned and the signed formats are compatible for one image
// and the same image data (on a portability subset device, this should also
// take imageViewFormatReinterpretation into account).
bool unsigned_signed_compatible;
};
class VulkanTexture final : public Texture {
public:
enum class Usage {
kUndefined,
kTransferDestination,
kGuestShaderSampled,
kSwapSampled,
};
// 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, bool track_usage = true);
~VulkanTexture();
VkImage image() const { return image_; }
// Doesn't transition (the caller must insert the barrier).
Usage SetUsage(Usage new_usage) {
Usage old_usage = usage_;
usage_ = new_usage;
return old_usage;
}
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;
struct {
uint32_t is_signed_separate_view : 1;
uint32_t host_swizzle : 12;
uint32_t is_array : 1;
};
ViewKey() : key(0) { static_assert_size(*this, sizeof(key)); }
struct Hasher {
size_t operator()(const ViewKey& key) const {
return std::hash<decltype(key.key)>{}(key.key);
}
};
bool operator==(const ViewKey& other_key) const {
return key == other_key.key;
}
bool operator!=(const ViewKey& other_key) const {
return !(*this == other_key);
}
};
static constexpr VkComponentSwizzle GetComponentSwizzle(
uint32_t texture_swizzle, uint32_t component_index) {
xenos::XE_GPU_TEXTURE_SWIZZLE texture_component_swizzle =
xenos::XE_GPU_TEXTURE_SWIZZLE(
(texture_swizzle >> (3 * component_index)) & 0b111);
if (texture_component_swizzle ==
xenos::XE_GPU_TEXTURE_SWIZZLE(component_index)) {
// The portability subset requires all swizzles to be IDENTITY, return
// IDENTITY specifically, not R, G, B, A.
return VK_COMPONENT_SWIZZLE_IDENTITY;
}
switch (texture_component_swizzle) {
case xenos::XE_GPU_TEXTURE_SWIZZLE_R:
return VK_COMPONENT_SWIZZLE_R;
case xenos::XE_GPU_TEXTURE_SWIZZLE_G:
return VK_COMPONENT_SWIZZLE_G;
case xenos::XE_GPU_TEXTURE_SWIZZLE_B:
return VK_COMPONENT_SWIZZLE_B;
case xenos::XE_GPU_TEXTURE_SWIZZLE_A:
return VK_COMPONENT_SWIZZLE_A;
case xenos::XE_GPU_TEXTURE_SWIZZLE_0:
return VK_COMPONENT_SWIZZLE_ZERO;
case xenos::XE_GPU_TEXTURE_SWIZZLE_1:
return VK_COMPONENT_SWIZZLE_ONE;
default:
// An invalid value.
return VK_COMPONENT_SWIZZLE_IDENTITY;
}
}
VkImage image_;
VmaAllocation allocation_;
Usage usage_ = Usage::kUndefined;
std::unordered_map<ViewKey, VkImageView, ViewKey::Hasher> 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<VulkanTexture> 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 {
VkImageView image_view_unsigned;
VkImageView image_view_signed;
VulkanTextureBinding() { Reset(); }
void Reset() {
image_view_unsigned = VK_NULL_HANDLE;
image_view_signed = VK_NULL_HANDLE;
}
};
struct Sampler {
VkSampler sampler;
uint64_t last_usage_submission;
std::pair<const SamplerParameters, Sampler>* used_previous;
std::pair<const SamplerParameters, Sampler>* used_next;
};
static constexpr bool AreDimensionsCompatible(
xenos::FetchOpDimension binding_dimension,
xenos::DataDimension resource_dimension) {
switch (binding_dimension) {
case xenos::FetchOpDimension::k1D:
case xenos::FetchOpDimension::k2D:
return resource_dimension == xenos::DataDimension::k1D ||
resource_dimension == xenos::DataDimension::k2DOrStacked ||
resource_dimension == xenos::DataDimension::k3D;
case xenos::FetchOpDimension::k3DOrStacked:
return resource_dimension == xenos::DataDimension::k3D;
case xenos::FetchOpDimension::kCube:
return resource_dimension == xenos::DataDimension::kCube;
default:
return false;
}
}
explicit VulkanTextureCache(
const RegisterFile& register_file, VulkanSharedMemory& shared_memory,
uint32_t draw_resolution_scale_x, uint32_t draw_resolution_scale_y,
VulkanCommandProcessor& command_processor,
VkPipelineStageFlags guest_shader_pipeline_stages);
bool Initialize();
const HostFormatPair& GetHostFormatPair(TextureKey key) const;
void GetTextureUsageMasks(VulkanTexture::Usage usage,
VkPipelineStageFlags& stage_mask,
VkAccessFlags& access_mask, VkImageLayout& layout);
xenos::ClampMode NormalizeClampMode(xenos::ClampMode clamp_mode) const;
// Sparse scaled resolve helper functions
bool InitializeSparseScaledResolve();
void ShutdownSparseScaledResolve();
size_t GetScaledResolveSparseBufferCount() const;
std::array<size_t, 2> GetPossibleScaledResolveBufferIndices(
uint64_t address_scaled) const;
bool EnsureScaledResolveMemoryCommittedSparse(
uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2);
bool MakeScaledResolveRangeCurrentSparse(
uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2);
void BindHeapToOverlappingBuffers(uint32_t heap_index, VkDeviceMemory heap);
VulkanCommandProcessor& command_processor_;
VkPipelineStageFlags guest_shader_pipeline_stages_;
// Using the Vulkan Memory Allocator because texture count in games is
// naturally pretty much unbounded, while Vulkan implementations, especially
// on Windows versions before 10, may have an allocation count limit as low as
// 4096.
VmaAllocator vma_allocator_ = VK_NULL_HANDLE;
static const HostFormatPair kBestHostFormats[64];
static const HostFormatPair kHostFormatGBGRUnaligned;
static const HostFormatPair kHostFormatBGRGUnaligned;
HostFormatPair host_formats_[64];
VkPipelineLayout load_pipeline_layout_ = VK_NULL_HANDLE;
std::array<VkPipeline, kLoadShaderCount> load_pipelines_{};
std::array<VkPipeline, kLoadShaderCount> load_pipelines_scaled_{};
// If both images can be placed in the same allocation, it's one allocation,
// otherwise it's two separate.
std::array<VkDeviceMemory, 2> null_images_memory_{};
VkImage null_image_2d_array_cube_ = VK_NULL_HANDLE;
VkImage null_image_3d_ = VK_NULL_HANDLE;
VkImageView null_image_view_2d_array_ = VK_NULL_HANDLE;
VkImageView null_image_view_cube_ = VK_NULL_HANDLE;
VkImageView null_image_view_3d_ = VK_NULL_HANDLE;
bool null_images_cleared_ = false;
std::array<VulkanTextureBinding, xenos::kTextureFetchConstantCount>
vulkan_texture_bindings_;
uint32_t sampler_max_count_;
xenos::AnisoFilter max_anisotropy_;
std::unordered_map<SamplerParameters, Sampler, SamplerParameters::Hasher>
samplers_;
std::pair<const SamplerParameters, Sampler>* sampler_used_first_ = nullptr;
std::pair<const SamplerParameters, Sampler>* sampler_used_last_ = nullptr;
// Scaled resolve buffer storage (simple non-overlapping, fallback path)
std::vector<ScaledResolveBuffer> scaled_resolve_buffers_;
// Current scaled resolve range tracking
uint64_t scaled_resolve_current_range_start_scaled_ = 0;
uint64_t scaled_resolve_current_range_length_scaled_ = 0;
size_t scaled_resolve_current_buffer_index_ = SIZE_MAX;
// Sparse scaled resolve (overlapping 2GB windows)
bool sparse_scaled_resolve_supported_ = false;
// 2GB overlapping sparse buffers - buffer N covers [N GB ... (N+2) GB)
// For 3x3 scale (4.5GB), need 4 buffers: 0:[0-2GB), 1:[1-3GB), 2:[2-4GB),
// 3:[3-4.5GB)
static constexpr size_t kMaxScaledResolveSparseBuffers =
(uint64_t(SharedMemory::kBufferSize) * kMaxDrawResolutionScaleAlongAxis *
kMaxDrawResolutionScaleAlongAxis -
1) >>
30;
std::array<std::unique_ptr<ScaledResolveSparseBuffer>,
kMaxScaledResolveSparseBuffers>
scaled_resolve_sparse_buffers_;
// 16MB heaps that can be mapped to multiple buffer regions
std::vector<VkDeviceMemory> scaled_resolve_heaps_;
uint32_t scaled_resolve_heap_count_ = 0;
// Memory type for sparse allocations
uint32_t scaled_resolve_memory_type_ = UINT32_MAX;
};
} // namespace vulkan
} // namespace gpu
} // namespace xe
#endif // XENIA_GPU_VULKAN_VULKAN_TEXTURE_CACHE_H_