Adds vulkan version of mode 1 and 2 and fixes related crashes by keeping the 2d texture views from the texture cache.
488 lines
19 KiB
C++
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_
|