/** ****************************************************************************** * 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 #include #include #include #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" #include "xenia/ui/vulkan/vulkan_provider.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{}(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 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 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) const; 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); protected: 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 CreateTexture(TextureKey key) override; bool LoadTextureDataFromResidentMemoryImpl(Texture& texture, bool load_base, bool load_mips) override; void UpdateTextureBindingsImpl(uint32_t fetch_constant_mask) override; private: enum LoadDescriptorSetIndex { kLoadDescriptorSetIndexDestination, kLoadDescriptorSetIndexSource, kLoadDescriptorSetIndexConstants, 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. explicit VulkanTexture(VulkanTextureCache& texture_cache, const TextureKey& key, VkImage image, VmaAllocation allocation); ~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); 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{}(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 views_; }; 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* used_previous; std::pair* 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; 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; 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 load_pipelines_{}; std::array load_pipelines_scaled_{}; // If both images can be placed in the same allocation, it's one allocation, // otherwise it's two separate. std::array 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 vulkan_texture_bindings_; uint32_t sampler_max_count_; xenos::AnisoFilter max_anisotropy_; std::unordered_map samplers_; std::pair* sampler_used_first_ = nullptr; std::pair* sampler_used_last_ = nullptr; }; } // namespace vulkan } // namespace gpu } // namespace xe #endif // XENIA_GPU_VULKAN_VULKAN_TEXTURE_CACHE_H_