rexglue-skate3/include/rex/graphics/vulkan/texture_cache.h
2026-06-14 05:22:30 +01:00

466 lines
21 KiB
C++

#pragma once
/**
******************************************************************************
* 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. *
******************************************************************************
*
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <array>
#include <map>
#include <memory>
#include <tuple>
#include <unordered_map>
#include <utility>
#include <vector>
#include <rex/graphics/pipeline/texture/cache.h>
#include <rex/graphics/vulkan/shader.h>
#include <rex/graphics/vulkan/shared_memory.h>
#include <rex/hash.h>
#include <rex/ui/vulkan/mem_alloc.h>
#include <rex/ui/vulkan/single_layout_descriptor_set_pool.h>
namespace rex::graphics::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;
void BeginFrame() override;
void EndFrame();
// 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.
// If swap_source_needs_rb_swap_out is not nullptr, writes whether the final
// guest-to-host swizzle requires swapping red and blue (R <- B, B <- R) with
// green preserved, which is needed by the presentation fallback path on
// devices without imageViewFormatSwizzle.
VkImageView RequestSwapTexture(uint32_t& width_scaled_out, uint32_t& height_scaled_out,
xenos::TextureFormat& format_out,
uint32_t* width_unscaled_out = nullptr,
uint32_t* height_unscaled_out = nullptr,
bool* swap_source_needs_rb_swap_out = nullptr);
bool GetScaledResolveRange(uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2, uint64_t& start_scaled_out,
uint64_t& length_scaled_out) const;
bool CommitScaledResolveRange(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);
}
VkBuffer scaled_resolve_buffer() const { return scaled_resolve_buffer_; }
void UseScaledResolveBufferForRead();
void UseScaledResolveBufferForWrite(uint64_t written_start_scaled,
uint64_t written_length_scaled);
protected:
bool IsSignedVersionSeparateForFormat(TextureKey key) const override;
bool IsScaledResolveSupportedForFormat(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 EnsureScaledResolveMemoryCommitted(uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t length_scaled_alignment_log2 = 0) override;
bool MaterializeScaledResolveToSharedMemory(uint32_t start_unscaled, uint32_t length_unscaled,
uint32_t pixel_size_log2) override;
bool LoadTextureDataFromResidentMemoryImpl(Texture& texture, bool load_base,
bool load_mips) override;
void UpdateTextureBindingsImpl(uint32_t fetch_constant_mask) override;
void DebugLogTeamProfileBackgroundGpuEvent(const char* action, uint32_t fetch_index,
const Texture& texture, bool signed_view = false,
uint32_t host_swizzle = 0,
VkImageView image_view = VK_NULL_HANDLE) const;
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,
// Direct load shader writes through a storage image view.
kStorageWrite,
kGuestShaderSampled,
kSwapSampled,
};
// Takes ownership of the image and its memory.
explicit VulkanTexture(VulkanTextureCache& texture_cache, const TextureKey& key, VkImage image,
VmaAllocation allocation, bool track_usage = true);
~VulkanTexture();
VkImage image() const { return image_; }
Usage usage() const { return usage_; }
// 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);
VkImageView GetOrCreate3DAs2DImageView(bool is_signed, uint32_t host_swizzle);
// Descriptor set with an R32_UINT storage image view of mip 0 layer 0, for
// direct load shader writes. VK_NULL_HANDLE if unavailable (the image was
// not created with storage usage, or creation failed). Created lazily,
// released in the destructor.
VkDescriptorSet GetStorageDescriptorSet();
bool image_storage_capable() const { return image_storage_capable_; }
void SetImageStorageCapable(bool capable) { image_storage_capable_ = capable; }
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_;
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;
// Whether the image was created with storage usage and an R32_UINT view
// format for direct load shader writes.
bool image_storage_capable_ = false;
VkImageView storage_view_ = VK_NULL_HANDLE;
size_t storage_descriptor_set_index_ = SIZE_MAX;
bool storage_binding_attempted_ = false;
};
static const char* DebugVulkanTextureUsageName(VulkanTexture::Usage usage);
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;
bool uses_custom_border_color;
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();
bool InitializeScaledResolveBuffer();
void ShutdownScaledResolveBuffer();
const HostFormatPair& GetHostFormatPair(TextureKey key) const;
void GetTextureUsageMasks(VulkanTexture::Usage usage, VkPipelineStageFlags& stage_mask,
VkAccessFlags& access_mask, VkImageLayout& layout);
bool EnsureScaledResolveBufferAllocated(uint64_t start_scaled, uint64_t length_scaled);
void GetScaledResolveUsageMasks(VkPipelineStageFlags& stage_mask_out,
VkAccessFlags& access_mask_out, bool write) const;
xenos::ClampMode NormalizeClampMode(xenos::ClampMode clamp_mode) const;
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;
static const HostFormatPair kHostFormatDXT1Unaligned;
static const HostFormatPair kHostFormatDXT2_3Unaligned;
static const HostFormatPair kHostFormatDXT4_5Unaligned;
static const HostFormatPair kHostFormatDXNUnaligned;
static const HostFormatPair kHostFormatDXT5AUnaligned;
HostFormatPair host_formats_[64];
VkPipelineLayout load_pipeline_layout_ = VK_NULL_HANDLE;
std::array<VkPipeline, kLoadShaderCount> load_pipelines_{};
std::array<VkPipeline, kLoadShaderCount> load_pipelines_scaled_{};
// Texel-buffer-source variants of hot load pipelines: the source is read
// through a uniform texel buffer (the texture path with its caching)
// instead of raw storage buffer loads, which is several times faster for
// the scattered tiled access pattern of guest texture loading on some
// hosts. Lazily compiled from GLSL ports of the corresponding shipped
// shaders; the attempt flags prevent retrying after a failure.
VkDescriptorSetLayout load_descriptor_set_layout_source_texel_ = VK_NULL_HANDLE;
VkPipelineLayout load_pipeline_layout_texel_ = VK_NULL_HANDLE;
std::array<VkPipeline, kLoadShaderCount> load_pipelines_scaled_texel_{};
std::array<bool, kLoadShaderCount> load_pipelines_scaled_texel_attempted_{};
VkPipeline GetLoadPipelineScaledTexelBuffer(LoadShaderIndex load_shader);
// Storage-image-destination variants of hot load pipelines: the load shader
// writes the texture directly through an R32_UINT storage image view,
// skipping the intermediate copy buffer and the buffer-to-image copy (which
// is several times slower than device bandwidth on some hosts). Set 0 is
// the storage image destination, set 1 the storage buffer source.
VkDescriptorSetLayout load_descriptor_set_layout_dest_storage_ = VK_NULL_HANDLE;
VkPipelineLayout load_pipeline_layout_storage_dest_ = VK_NULL_HANDLE;
std::array<VkPipeline, kLoadShaderCount> load_pipelines_scaled_storage_dest_{};
std::array<bool, kLoadShaderCount> load_pipelines_scaled_storage_dest_attempted_{};
VkPipeline GetLoadPipelineScaledStorageDest(LoadShaderIndex load_shader);
std::unique_ptr<ui::vulkan::SingleLayoutDescriptorSetPool>
load_dest_storage_descriptor_set_pool_;
std::unique_ptr<ui::vulkan::SingleLayoutDescriptorSetPool>
load_source_texel_descriptor_set_pool_;
struct LoadSourceTexelBinding {
VkBufferView buffer_view;
size_t descriptor_set_index;
};
// Persistent cached buffer views and descriptor sets for texel-buffer load
// sources, keyed by (buffer, view offset, view range) - the underlying
// buffers (shared memory, scaled resolve) live for the cache's lifetime.
std::map<std::tuple<uint64_t, uint64_t, uint64_t>, LoadSourceTexelBinding>
load_source_texel_bindings_;
// Returns the descriptor set (or VK_NULL_HANDLE on failure) for reading
// [offset, offset + range) of the buffer as an RGBA32_UINT texel buffer.
// The offset must be aligned to minTexelBufferOffsetAlignment and the range
// to the texel size by the caller.
VkDescriptorSet GetLoadSourceTexelBufferDescriptorSet(VkBuffer buffer, VkDeviceSize offset,
VkDeviceSize range);
// 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_;
// Unsupported texture formats used during this frame (for research and
// testing).
enum : uint8_t {
kUnsupportedResourceBit = 1,
kUnsupportedUnormBit = kUnsupportedResourceBit << 1,
kUnsupportedSnormBit = kUnsupportedUnormBit << 1,
};
uint8_t unsupported_format_features_used_[64] = {};
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;
uint32_t custom_border_color_sampler_count_ = 0;
VkBuffer scaled_resolve_buffer_ = VK_NULL_HANDLE;
uint64_t scaled_resolve_buffer_size_ = 0;
bool scaled_resolve_buffer_sparse_ = false;
uint32_t scaled_resolve_buffer_memory_type_ = UINT32_MAX;
std::vector<VkDeviceMemory> scaled_resolve_buffer_memory_;
uint32_t scaled_resolve_sparse_granularity_log2_ = UINT32_MAX;
std::vector<uint64_t> scaled_resolve_sparse_allocated_;
bool scaled_resolve_last_usage_write_ = false;
std::pair<uint64_t, uint64_t> scaled_resolve_last_written_range_{0, 0};
};
} // namespace rex::graphics::vulkan