diff --git a/include/rex/graphics/command_processor.h b/include/rex/graphics/command_processor.h index 38a3a9e..4ecb770 100644 --- a/include/rex/graphics/command_processor.h +++ b/include/rex/graphics/command_processor.h @@ -12,6 +12,7 @@ #pragma once #include +#include #include #include #include @@ -306,6 +307,19 @@ class CommandProcessor { const char* legacy_readback_memexport_cvar_name_ = nullptr; private: + // CPU-side frame attribution for the GPU emulation thread (gpu_cpu_profile + // cvar): manages the rex::perf::cpu_profile enable state and logs the + // per-bucket breakdown every kCpuProfileLogFrameInterval frames. Called once + // per guest frame from the XE_SWAP packet handler. +#ifdef REXGLUE_ENABLE_PERF_COUNTERS + void CpuProfileOnFrameEnd(); + + static constexpr uint32_t kCpuProfileLogFrameInterval = 120; + uint32_t cpu_profile_frames_ = 0; + uint64_t cpu_profile_window_start_ticks_ = 0; + std::chrono::steady_clock::time_point cpu_profile_window_start_time_{}; +#endif + reg::DC_LUT_30_COLOR gamma_ramp_256_entry_table_[256] = {}; reg::DC_LUT_PWL_DATA gamma_ramp_pwl_rgb_[128][3] = {}; uint32_t gamma_ramp_rw_component_ = 0; diff --git a/include/rex/graphics/d3d12/command_processor.h b/include/rex/graphics/d3d12/command_processor.h index 72bbe7c..209a5c2 100644 --- a/include/rex/graphics/d3d12/command_processor.h +++ b/include/rex/graphics/d3d12/command_processor.h @@ -387,9 +387,10 @@ class D3D12CommandProcessor : public CommandProcessor { void EndGpuTimestampFrame(); void ResolveGpuTimestampFrame(ID3D12GraphicsCommandList* command_list); void ProcessGpuTimestampResults(); + public: + // Also used by the texture and render target caches for profiling regions. uint32_t BeginGpuTimestampedDraw(rex::perf::DrawBucket bucket); void EndGpuTimestampedDraw(uint32_t start_query_index); - public: uint32_t BeginGpuTimestampedCounter(ID3D12GraphicsCommandList* command_list, rex::perf::CounterId counter_id); void EndGpuTimestampedCounter(ID3D12GraphicsCommandList* command_list, @@ -693,6 +694,7 @@ class D3D12CommandProcessor : public CommandProcessor { uint64_t* gpu_timestamp_readback_mapping_ = nullptr; uint64_t gpu_timestamp_frequency_ = 0; bool gpu_timestamp_resources_available_ = false; + bool gpu_timestamp_draws_enabled_ = false; struct GpuTimestampFrame { uint64_t frame = 0; uint64_t submission = 0; @@ -707,6 +709,12 @@ class D3D12CommandProcessor : public CommandProcessor { std::vector counter_start_queries; }; std::array gpu_timestamp_frames_; + // Averaged full-frame GPU time attribution, logged periodically when bucket + // timestamps are enabled. + static constexpr uint32_t kGpuProfileLogFrameInterval = 120; + uint64_t gpu_profile_bucket_us_[size_t(rex::perf::DrawBucket::kCount)] = {}; + uint64_t gpu_profile_span_us_ = 0; + uint32_t gpu_profile_frames_ = 0; struct VertexBufferState { uint32_t address = UINT32_MAX; uint32_t size = UINT32_MAX; diff --git a/include/rex/graphics/pipeline/render_target/cache.h b/include/rex/graphics/pipeline/render_target/cache.h index 18e6b14..a22dc71 100644 --- a/include/rex/graphics/pipeline/render_target/cache.h +++ b/include/rex/graphics/pipeline/render_target/cache.h @@ -541,6 +541,16 @@ class RenderTargetCache { RenderTarget*& color_render_target_out, std::vector& color_transfers_out); + // Returns whether every EDRAM tile range for which the given render target + // holds the up-to-date contents - as the current owner, or as the latest + // host depth copy if the host depth encoding differs from the guest - lies + // fully within the given range relative to the render target's base (using + // the same wrapping rules as ChangeOwnership). When this holds, destroying + // the host render target's contents outside that range (for example with a + // whole-image clear) loses no data that could still be read. + bool IsOwnershipConfinedToRange(RenderTargetKey key, uint32_t start_tiles_base_relative, + uint32_t length_tiles) const; + // For restoring EDRAM contents from frame traces, obtains or creates a render // target at base 0 with of 1280 (only 1 sample and color because copying // between MSAA render targets and buffers is not possible in Direct3D 12, and diff --git a/include/rex/graphics/pipeline/texture/cache.h b/include/rex/graphics/pipeline/texture/cache.h index 1909cdb..dec0767 100644 --- a/include/rex/graphics/pipeline/texture/cache.h +++ b/include/rex/graphics/pipeline/texture/cache.h @@ -115,6 +115,13 @@ class TextureCache { } virtual void RequestTextures(uint32_t used_texture_mask); + + // Monotonic generation of the active texture binding views - incremented + // whenever any binding's textures change (UpdateTextureBindingsImpl calls). + // Backends can compare this between draws to skip rebuilding descriptor + // contents when the bound views are unchanged. + uint64_t texture_bindings_generation() const { return texture_bindings_generation_; } + void DebugRecordResolveReadback(uint32_t start, uint32_t length, bool scaled); void DebugClearRecentResolveReadbacks(); void DebugRecordTeamProfileBackgroundResolveRange(uint32_t start, uint32_t length, bool scaled, @@ -669,6 +676,8 @@ class TextureCache { // Bit vector with bits reset on fetch constant writes to avoid parsing fetch // constants again and again. uint32_t texture_bindings_in_sync_ = 0; + // See texture_bindings_generation(). + uint64_t texture_bindings_generation_ = 1; int32_t debug_scaled_texture_bindings_remaining_seen_ = 0; uint32_t debug_scaled_texture_binding_logs_this_sample_ = 0; diff --git a/include/rex/graphics/vulkan/command_processor.h b/include/rex/graphics/vulkan/command_processor.h index cf14d72..2a66a46 100644 --- a/include/rex/graphics/vulkan/command_processor.h +++ b/include/rex/graphics/vulkan/command_processor.h @@ -614,6 +614,12 @@ class VulkanCommandProcessor : public CommandProcessor { // Descriptor set layouts used by different shaders. VkDescriptorSetLayout descriptor_set_layout_empty_ = VK_NULL_HANDLE; VkDescriptorSetLayout descriptor_set_layout_constants_ = VK_NULL_HANDLE; + // Whether the guest constant buffer descriptors are pushed via + // VK_KHR_push_descriptor instead of being written to transient descriptor + // sets (vulkan_push_constants_descriptors and driver support, applied at + // setup; descriptor_set_layout_constants_ is created with the push + // descriptor flag and must not be used with vkAllocateDescriptorSets then). + bool constants_push_descriptors_used_ = false; std::array descriptor_set_layouts_single_transient_{}; VkDescriptorSetLayout descriptor_set_layout_shared_memory_and_edram_ = VK_NULL_HANDLE; @@ -646,6 +652,21 @@ class VulkanCommandProcessor : public CommandProcessor { std::unordered_map, TextureDescriptorSetLayoutKey::Hasher> texture_transient_descriptor_sets_free_; + // Frame-local reuse of transient texture/sampler descriptor sets: XXH3 of + // the binding contents (stage, counts, image views, samplers) -> a set + // already written with those contents this frame. Transient sets are + // recycled only after their frame completes on the GPU, so within a frame + // their contents are immutable and the same set can simply be rebound + // instead of allocating and writing a new one for every draw. Cleared at + // frame open. + std::unordered_map> + texture_transient_descriptor_set_reuse_cache_; + // Content hashes of the texture descriptor sets currently recorded in + // current_graphics_descriptor_sets_, indexed [0] = vertex, [1] = pixel. The + // valid flag is false when the recorded set's contents are unknown (frame + // open, write failure, or reuse disabled). + bool current_texture_descriptor_set_hash_valid_[2] = {}; + uint64_t current_texture_descriptor_set_hashes_[2] = {}; std::unique_ptr shared_memory_; @@ -784,8 +805,12 @@ class VulkanCommandProcessor : public CommandProcessor { bool valid = false; } active_occlusion_query_; // MoltenVK maps timestamp queries to Metal counter sample buffers with a - // small per-pool limit, so keep the per-frame query budget modest. + // small per-pool limit, so keep the per-frame query budget modest there. +#if REX_PLATFORM_MAC static constexpr uint32_t kMaxGpuTimestampQueriesPerFrame = 1024; +#else + static constexpr uint32_t kMaxGpuTimestampQueriesPerFrame = 4096; +#endif VkQueryPool gpu_timestamp_query_pool_ = VK_NULL_HANDLE; VkBuffer gpu_timestamp_readback_buffer_ = VK_NULL_HANDLE; VkDeviceMemory gpu_timestamp_readback_memory_ = VK_NULL_HANDLE; @@ -793,9 +818,13 @@ class VulkanCommandProcessor : public CommandProcessor { VkDeviceSize gpu_timestamp_readback_memory_size_ = 0; uint64_t* gpu_timestamp_readback_mapping_ = nullptr; bool gpu_timestamp_resources_available_ = false; + bool gpu_timestamp_draws_enabled_ = false; struct GpuTimestampFrame { uint64_t frame = 0; uint64_t submission = 0; + // Submission index when the frame began - for counting the queue + // submissions the frame spans in the profile log. + uint64_t first_submission = 0; uint32_t query_base = 0; uint32_t query_count = 0; uint32_t frame_start_query = UINT32_MAX; @@ -809,6 +838,13 @@ class VulkanCommandProcessor : public CommandProcessor { std::vector bucket_end_queries; }; std::array gpu_timestamp_frames_; + // Averaged full-frame GPU time attribution, logged periodically when bucket + // timestamps are enabled. + static constexpr uint32_t kGpuProfileLogFrameInterval = 120; + uint64_t gpu_profile_bucket_us_[size_t(rex::perf::DrawBucket::kCount)] = {}; + uint64_t gpu_profile_span_us_ = 0; + uint64_t gpu_profile_submissions_ = 0; + uint32_t gpu_profile_frames_ = 0; struct VertexBufferState { uint32_t address = UINT32_MAX; uint32_t size = UINT32_MAX; @@ -856,6 +892,20 @@ class VulkanCommandProcessor : public CommandProcessor { std::vector> current_samplers_vertex_; std::vector> current_samplers_pixel_; + // State the per-draw binding fast path compares against + // (vulkan_reuse_unchanged_draw_bindings): when the previous draw in the same + // submission used the same shaders, identical sampler parameters and the + // texture cache's binding views are unchanged, the sampler cache walks and + // the texture descriptor gather/hash in UpdateBindings are skipped. + bool draw_binding_state_valid_ = false; + uint64_t draw_binding_submission_ = 0; + const VulkanShader* draw_binding_vertex_shader_ = nullptr; + const VulkanShader* draw_binding_pixel_shader_ = nullptr; + uint64_t draw_binding_texture_generation_ = 0; + // Set by IssueDraw for the draw currently being issued, consumed by + // UpdateBindings. + bool texture_bindings_unchanged_this_draw_ = false; + // Cache render pass currently started in the command buffer with the // framebuffer. For dynamic rendering, current_render_pass_ is VK_NULL_HANDLE // but in_render_pass_ is true. diff --git a/include/rex/graphics/vulkan/deferred_command_buffer.h b/include/rex/graphics/vulkan/deferred_command_buffer.h index f6fea06..3e12ade 100644 --- a/include/rex/graphics/vulkan/deferred_command_buffer.h +++ b/include/rex/graphics/vulkan/deferred_command_buffer.h @@ -164,6 +164,28 @@ class DeferredCommandBuffer { sizeof(VkImageSubresourceRange) * range_count); } + VkImageSubresourceRange* CmdClearDepthStencilImageEmplace( + VkImage image, VkImageLayout image_layout, const VkClearDepthStencilValue* depth_stencil, + uint32_t range_count) { + const size_t header_size = + rex::align(sizeof(ArgsVkClearDepthStencilImage), alignof(VkImageSubresourceRange)); + uint8_t* args_ptr = reinterpret_cast( + WriteCommand(Command::kVkClearDepthStencilImage, + header_size + sizeof(VkImageSubresourceRange) * range_count)); + auto& args = *reinterpret_cast(args_ptr); + args.image = image; + args.image_layout = image_layout; + args.depth_stencil = *depth_stencil; + args.range_count = range_count; + return reinterpret_cast(args_ptr + header_size); + } + void CmdVkClearDepthStencilImage(VkImage image, VkImageLayout image_layout, + const VkClearDepthStencilValue* depth_stencil, + uint32_t range_count, const VkImageSubresourceRange* ranges) { + std::memcpy(CmdClearDepthStencilImageEmplace(image, image_layout, depth_stencil, range_count), + ranges, sizeof(VkImageSubresourceRange) * range_count); + } + VkBufferCopy* CmdCopyBufferEmplace(VkBuffer src_buffer, VkBuffer dst_buffer, uint32_t region_count) { const size_t header_size = rex::align(sizeof(ArgsVkCopyBuffer), alignof(VkBufferCopy)); @@ -294,6 +316,29 @@ class DeferredCommandBuffer { std::memcpy(args_ptr + sizeof(ArgsVkPushConstants), values, size); } + // Pushes uniform buffer descriptors (bindings 0 through buffer_count - 1 of + // the set, one buffer each) via VK_KHR_push_descriptor - the extension must + // be available and the set layout must have been created with the push + // descriptor flag. + void CmdVkPushUniformBufferDescriptorSet(VkPipelineBindPoint pipeline_bind_point, + VkPipelineLayout layout, uint32_t set, + uint32_t buffer_count, + const VkDescriptorBufferInfo* buffer_infos) { + size_t arguments_size = rex::align(sizeof(ArgsVkPushUniformBufferDescriptorSet), + alignof(VkDescriptorBufferInfo)); + size_t buffer_infos_offset = arguments_size; + arguments_size += sizeof(VkDescriptorBufferInfo) * buffer_count; + uint8_t* args_ptr = reinterpret_cast( + WriteCommand(Command::kVkPushUniformBufferDescriptorSet, arguments_size)); + auto& args = *reinterpret_cast(args_ptr); + args.pipeline_bind_point = pipeline_bind_point; + args.layout = layout; + args.set = set; + args.buffer_count = buffer_count; + std::memcpy(args_ptr + buffer_infos_offset, buffer_infos, + sizeof(VkDescriptorBufferInfo) * buffer_count); + } + void CmdVkSetBlendConstants(const float* blend_constants) { auto& args = *reinterpret_cast( WriteCommand(Command::kVkSetBlendConstants, sizeof(ArgsVkSetBlendConstants))); @@ -370,6 +415,7 @@ class DeferredCommandBuffer { kVkBindVertexBuffers, kVkClearAttachments, kVkClearColorImage, + kVkClearDepthStencilImage, kVkCopyBuffer, kVkCopyBufferToImage, kVkCopyQueryPoolResults, @@ -382,6 +428,7 @@ class DeferredCommandBuffer { kVkEndRendering, kVkPipelineBarrier, kVkPushConstants, + kVkPushUniformBufferDescriptorSet, kVkResetQueryPool, kVkSetBlendConstants, kVkSetDepthBias, @@ -477,6 +524,15 @@ class DeferredCommandBuffer { static_assert(alignof(VkImageSubresourceRange) <= alignof(uintmax_t)); }; + struct ArgsVkClearDepthStencilImage { + VkImage image; + VkImageLayout image_layout; + VkClearDepthStencilValue depth_stencil; + uint32_t range_count; + // Followed by aligned VkImageSubresourceRange[]. + static_assert(alignof(VkImageSubresourceRange) <= alignof(uintmax_t)); + }; + struct ArgsVkCopyBuffer { VkBuffer src_buffer; VkBuffer dst_buffer; @@ -564,6 +620,15 @@ class DeferredCommandBuffer { // Followed by `size` bytes of values. }; + struct ArgsVkPushUniformBufferDescriptorSet { + VkPipelineBindPoint pipeline_bind_point; + VkPipelineLayout layout; + uint32_t set; + uint32_t buffer_count; + // Followed by aligned VkDescriptorBufferInfo[buffer_count]. + static_assert(alignof(VkDescriptorBufferInfo) <= alignof(uintmax_t)); + }; + struct ArgsVkSetBlendConstants { float blend_constants[4]; }; diff --git a/include/rex/graphics/vulkan/render_target_cache.h b/include/rex/graphics/vulkan/render_target_cache.h index 63dd7ea..849f271 100644 --- a/include/rex/graphics/vulkan/render_target_cache.h +++ b/include/rex/graphics/vulkan/render_target_cache.h @@ -127,6 +127,13 @@ class VulkanRenderTargetCache final : public RenderTargetCache { uint32_t* color_attachment_count_out, VkRenderingAttachmentInfo* depth_attachment, VkRenderingAttachmentInfo* stencil_attachment) const; + // To be called by the command processor right after entering the render pass + // for guest draws - replays the deferred resolve clears pending on the bound + // render targets with vkCmdClearAttachments inside that render pass. + // render_area_extent is the render area of the entered render pass which the + // clear rectangles must be contained in (entries that don't fit are kept + // deferred). + void FlushDeferredResolveClearsIntoEnteredRenderPass(const VkExtent2D& render_area_extent); // Using R16G16[B16A16]_SNORM, which are -1...1, not the needed -32...32. // Persistent data doesn't depend on this, so can be overriden by per-game @@ -372,6 +379,18 @@ class VulkanRenderTargetCache final : public RenderTargetCache { uint32_t temporary_sort_index() const { return temporary_sort_index_; } void SetTemporarySortIndex(uint32_t index) { temporary_sort_index_ = index; } + // Resolve clears deferred until the next render pass binding this render + // target, or a standalone flush if its contents are needed earlier. + struct DeferredClear { + Transfer::Rectangle rectangle; + uint64_t clear_value; + // Absolute EDRAM tile range (may wrap across the addressing period) + // conservatively covering the rectangle, for read intersection checks. + uint32_t tiles_start; + uint32_t tiles_length; + }; + std::vector& deferred_clears() { return deferred_clears_; } + private: VulkanRenderTargetCache& render_target_cache_; @@ -396,6 +415,8 @@ class VulkanRenderTargetCache final : public RenderTargetCache { // Temporary storage for indices in operations like transfers and dumps. uint32_t temporary_sort_index_ = 0; + + std::vector deferred_clears_; }; struct FramebufferKey { @@ -769,6 +790,13 @@ class VulkanRenderTargetCache final : public RenderTargetCache { uint32_t source_base_tiles; uint32_t source_pitch_tiles; uint32_t dispatch_first_tile; + // Resolve rectangle height in pixels (the width is in + // resolve.dest_relative.coordinate_info, but the height is normally only + // implied by the dispatch size). + uint32_t height_pixels; + // How to pack the sampled host render target texel into the raw 32bpp + // guest dword: 0 = unorm8x4 (k_8_8_8_8 family), 1 = unorm 2_10_10_10. + uint32_t source_format_case; }; struct DirectResolvePipelineKey { @@ -813,9 +841,43 @@ class VulkanRenderTargetCache final : public RenderTargetCache { VkPipeline GetDumpPipeline(DumpPipelineKey key); VkPipeline GetDirectResolvePipeline(DirectResolvePipelineKey key); + // Compute pipeline reading a 32bpp color or unorm24 depth / stencil host + // render target (single-sampled, native 2x MSAA or 4x MSAA) and writing the + // raw resolve destination directly, skipping the EDRAM buffer round trip. + // VK_NULL_HANDLE if unsupported or creation failed. + VkPipeline GetDirectResolveFast32Pipeline(bool is_depth, xenos::MsaaSamples source_msaa_samples); + // When this returns true, the destination has been fully written - the + // caller must skip both the render target dump and the EDRAM buffer copy. bool TryResolveCopyDirectly(const draw_util::ResolveInfo& resolve_info, draw_util::ResolveCopyShaderIndex copy_shader, - bool draw_resolution_scaled); + bool draw_resolution_scaled, VulkanSharedMemory& shared_memory, + const draw_util::ResolveCopyShaderConstants& copy_shader_constants); + + // Deferred resolve clears: instead of an own render pass with a rectangle + // attachment clear per resolve, clears are accumulated on the render target + // and replayed with vkCmdClearAttachments inside the next render pass that + // binds it for drawing (via FlushDeferredResolveClearsIntoEnteredRenderPass) + // or in a standalone ownership transfer render pass if something needs the + // render target's contents before then. This removes the per-resolve render + // pass and layout transition overhead, which is a large fixed cost on some + // hosts. + void DeferResolveClear(VulkanRenderTarget& render_target, + const Transfer::Rectangle& clear_rectangle, uint64_t clear_value); + // Emits the pending deferred clears of the render target in a standalone + // ownership transfer render pass. + void FlushDeferredResolveClears(VulkanRenderTarget& render_target); + // Flushes the pending deferred clears of any render target whose deferred + // tile ranges intersect the given absolute EDRAM tile range that's about to + // be read (such as by a resolve dump). + void FlushDeferredResolveClearsBeforeEdramRead(uint32_t base_tiles, uint32_t length_tiles); + // Converts the guest resolve clear value the same way for all the clear + // paths. through_transfer_view selects between the image's own format (used + // when clearing through the guest render pass attachment) and the ownership + // transfer view format (uint for 16- and 32-bit-per-component color). + VkClearDepthStencilValue GetResolveClearDepthStencilValue(RenderTargetKey key, + uint64_t clear_value) const; + VkClearColorValue GetResolveClearColorValue(RenderTargetKey key, uint64_t clear_value, + bool through_transfer_view) const; // Writes contents of host render targets within rectangles from // ResolveInfo::GetCopyEdramTileSpan to edram_buffer_. @@ -867,6 +929,21 @@ class VulkanRenderTargetCache final : public RenderTargetCache { VkPipelineLayout direct_resolve_pipeline_layout_depth_ = VK_NULL_HANDLE; std::unordered_map direct_resolve_pipelines_; + // Lazily created (compiled from GLSL at runtime); the attempt flags prevent + // retrying the compilation every resolve after a failure. Indexed + // [is_depth][uint32_t(source MsaaSamples)]. + VkPipeline direct_resolve_fast32_pipelines_[2][3] = {}; + bool direct_resolve_fast32_pipelines_attempted_[2][3] = {}; + + // Render targets that currently have deferred resolve clears pending. + std::vector deferred_clear_render_targets_; + // Temporary storage for FlushDeferredResolveClearsBeforeEdramRead. + std::vector flush_deferred_clear_temp_; + // Deferred resolve clear statistics, logged periodically for diagnostics. + uint64_t deferred_clear_stat_deferred_ = 0; + uint64_t deferred_clear_stat_pass_entry_flushed_ = 0; + uint64_t deferred_clear_stat_standalone_flushed_ = 0; + uint64_t deferred_clear_stat_immediate_ = 0; // Temporary storage for Resolve. std::vector clear_transfers_[2]; diff --git a/include/rex/graphics/vulkan/texture_cache.h b/include/rex/graphics/vulkan/texture_cache.h index 4beeeec..9e0d621 100644 --- a/include/rex/graphics/vulkan/texture_cache.h +++ b/include/rex/graphics/vulkan/texture_cache.h @@ -11,7 +11,9 @@ */ #include +#include #include +#include #include #include #include @@ -21,6 +23,7 @@ #include #include #include +#include namespace rex::graphics::vulkan { @@ -198,6 +201,8 @@ class VulkanTextureCache final : public TextureCache { enum class Usage { kUndefined, kTransferDestination, + // Direct load shader writes through a storage image view. + kStorageWrite, kGuestShaderSampled, kSwapSampled, }; @@ -220,6 +225,14 @@ class VulkanTextureCache final : public TextureCache { 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; @@ -277,6 +290,13 @@ class VulkanTextureCache final : public TextureCache { 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; + + // 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); @@ -360,6 +380,48 @@ class VulkanTextureCache final : public TextureCache { std::array load_pipelines_{}; std::array 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 load_pipelines_scaled_texel_{}; + std::array 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 load_pipelines_scaled_storage_dest_{}; + std::array load_pipelines_scaled_storage_dest_attempted_{}; + VkPipeline GetLoadPipelineScaledStorageDest(LoadShaderIndex load_shader); + std::unique_ptr + load_dest_storage_descriptor_set_pool_; + std::unique_ptr + 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, 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 null_images_memory_{}; diff --git a/include/rex/perf/counter.h b/include/rex/perf/counter.h index 5307043..1064b09 100644 --- a/include/rex/perf/counter.h +++ b/include/rex/perf/counter.h @@ -10,6 +10,7 @@ */ #pragma once +#include #include #include #include @@ -21,6 +22,14 @@ #include #endif +#if defined(_M_X64) || defined(_M_IX86) +#include +#define REX_PERF_CPU_PROFILE_RDTSC 1 +#elif defined(__x86_64__) || defined(__i386__) +#include +#define REX_PERF_CPU_PROFILE_RDTSC 1 +#endif + namespace rex::perf { enum class DrawBucket : uint8_t { @@ -34,8 +43,66 @@ enum class DrawBucket : uint8_t { kCopyResolveFast32, kCopyResolveFull32, kResolveDownscale, + // Guest render pass entry: ending the previous render pass, beginning the + // new one and replaying deferred resolve clears. Not aggregated into the + // draw/res/dump FPS overlay lines - shows up in "other". + kRenderPassEntry, + // Render target ownership transfers and resolve clears performed in + // PerformTransfersAndResolveClears. + kRtTransfer, + // Texture cache uploads (guest texture data load compute dispatches). + kTextureUpload, + // Shared memory (guest memory mirror buffer) uploads. + kSharedMemoryUpload, + // From the end of a resolve operation until the next instrumented point - + // the post-resolve barriers and any idle gap before the next draws. + kResolveGap, + // The buffer-to-image copy phase of a texture upload (kTextureUpload then + // covers only the load compute dispatches and their barriers). + kTextureUploadCopy, + kCount, }; +// Short display name of a draw bucket (for logs and capture CSVs). +inline const char* DrawBucketName(DrawBucket bucket) { + switch (bucket) { + case DrawBucket::kMainColorDepth: + return "Main"; + case DrawBucket::kDepthOnly: + return "Depth"; + case DrawBucket::kCopyResolve: + return "Copy"; + case DrawBucket::kMemexport: + return "MemExp"; + case DrawBucket::kNoPixelShader: + return "NoPS"; + case DrawBucket::kCopyDump: + return "CopyDump"; + case DrawBucket::kCopyResolveShader: + return "CopyResolve"; + case DrawBucket::kCopyResolveFast32: + return "CopyResolveFast32"; + case DrawBucket::kCopyResolveFull32: + return "CopyResolveFull32"; + case DrawBucket::kResolveDownscale: + return "ResolveDownscale"; + case DrawBucket::kRenderPassEntry: + return "PassEntry"; + case DrawBucket::kRtTransfer: + return "RtTransfer"; + case DrawBucket::kTextureUpload: + return "TexUpload"; + case DrawBucket::kSharedMemoryUpload: + return "SMUpload"; + case DrawBucket::kResolveGap: + return "ResolveGap"; + case DrawBucket::kTextureUploadCopy: + return "TexUpCopy"; + default: + return "Unknown"; + } +} + struct DrawFingerprint { DrawBucket bucket = DrawBucket::kMainColorDepth; uint64_t vertex_shader_hash = 0; @@ -91,6 +158,13 @@ enum class CounterId : uint16_t { kDrawStageVertexBuffersUs, kDrawStageBarriersUs, kDrawStageSubmitUs, + kDrawStageSystemConstantsUs, + kDrawSamplerFastPathDraws, + kDrawTextureFastPathDraws, + kDrawStageAnalyzeUs, + kDrawStageTranslateUs, + kDrawStageSamplersUs, + kDrawStagePreDrawUs, kGpuMainUs, kGpuDepthUs, kGpuCopyUs, @@ -113,6 +187,8 @@ enum class CounterId : uint16_t { kGpuResolveQueryUs, kCpuPrimaryBufferUs, kCpuIndirectBufferUs, + kCpuGuestWaitUs, + kCpuSwapUs, kCpuD3D12BeginSubmissionUs, kCpuD3D12BeginSubmissionFenceUs, kCpuD3D12BeginSubmissionFrameOpenUs, @@ -135,6 +211,10 @@ enum class CounterId : uint16_t { kCpuD3D12PaintUiUs, kCpuD3D12PresentWaitUs, kCpuD3D12PresentUs, + // Time the GPU emulation thread spent blocked waiting for submission + // fences (both backends) - high values mean GPU-bound, near zero means the + // thread itself is the bottleneck. + kGpuThreadFenceWaitUs, kTextureRequestUs, kTextureRequestCalls, kTextureFetchesRequested, @@ -243,6 +323,50 @@ enum class CounterId : uint16_t { kCount // sentinel -- must be last }; +// Lightweight CPU-side scope attribution for perf-counter builds. When enabled +// at runtime (the gpu_cpu_profile cvar), every ScopedCounterTimer scope +// accumulates raw timestamp ticks and a hit count per CounterId; the command +// processor converts ticks to microseconds against a wall-clock calibration +// window and logs the breakdown periodically at frame end. Constraint: each +// CounterId may only be written from one thread (slots are not atomic). +namespace cpu_profile { + +inline uint64_t Ticks() { +#ifdef REX_PERF_CPU_PROFILE_RDTSC + return __rdtsc(); +#else + return uint64_t(std::chrono::steady_clock::now().time_since_epoch().count()); +#endif +} + +struct Slot { + uint64_t ticks; + uint64_t count; +}; + +inline std::atomic enabled{false}; +inline Slot slots[size_t(CounterId::kCount)]{}; + +inline void Reset() { + for (Slot& slot : slots) { + slot.ticks = 0; + slot.count = 0; + } +} + +// Count an event (no time attribution) when the CPU profile is armed. +inline void CountEvent(CounterId id) { +#ifdef REXGLUE_ENABLE_PERF_COUNTERS + if (enabled.load(std::memory_order_relaxed)) { + ++slots[size_t(id)].count; + } +#else + (void)id; +#endif +} + +} // namespace cpu_profile + #ifdef REXGLUE_ENABLE_PERF_COUNTERS // Returns human-readable name for a counter (e.g. "frame_time_us") @@ -294,6 +418,9 @@ class ScopedCounterTimer { if (active_) { start_ = Clock::now(); } + if (cpu_profile::enabled.load(std::memory_order_relaxed)) { + cpu_profile_start_ticks_ = cpu_profile::Ticks(); + } } ~ScopedCounterTimer() { @@ -301,6 +428,12 @@ class ScopedCounterTimer { } void Stop() { + if (cpu_profile_start_ticks_) { + cpu_profile::Slot& slot = cpu_profile::slots[size_t(counter_id_)]; + slot.ticks += cpu_profile::Ticks() - cpu_profile_start_ticks_; + ++slot.count; + cpu_profile_start_ticks_ = 0; + } if (!active_) { return; } @@ -318,6 +451,7 @@ class ScopedCounterTimer { CounterId counter_id_; bool active_; Clock::time_point start_; + uint64_t cpu_profile_start_ticks_ = 0; }; // Read a counter's current live value @@ -406,6 +540,9 @@ class ScopedCounterTimer { (void)counter_id; (void)active; } + + ~ScopedCounterTimer() = default; + void Stop() {} ScopedCounterTimer(const ScopedCounterTimer&) = delete; @@ -487,7 +624,12 @@ class Profiler { } // namespace rex::perf -// Perf counter macros -- compile to no-ops when counters are disabled. +// Perf counter macros -- compile to no-ops when counters are disabled. CPU +// profile scope timers are perf-build-only so shipped builds do not pay +// per-scope constructor/destructor or atomic-check costs. +#define REX_PERF_CONCAT_INNER(a, b) a##b +#define REX_PERF_CONCAT(a, b) REX_PERF_CONCAT_INNER(a, b) + #ifdef REXGLUE_ENABLE_PERF_COUNTERS // Generic helpers for easily adding new counters @@ -510,8 +652,6 @@ class Profiler { #define PROFILE_DRAW_FINGERPRINT(fingerprint) rex::perf::RecordDrawFingerprint(fingerprint) #define PROFILE_DRAW_STAGE_US(id, delta) rex::perf::IncrementCounter((id), (delta)) #define PROFILE_GPU_TIMESTAMPED_DRAW() PERF_counter_inc(kGpuTimestampedDraws) -#define REX_PERF_CONCAT_INNER(a, b) a##b -#define REX_PERF_CONCAT(a, b) REX_PERF_CONCAT_INNER(a, b) #define PROFILE_SCOPE_COUNTER(id) \ rex::perf::ScopedCounterTimer REX_PERF_CONCAT(perf_scope_timer_, __LINE__)(rex::perf::CounterId::id) #define PROFILE_SCOPE_COUNTER_IF(id, active) \ diff --git a/include/rex/rex_app.h b/include/rex/rex_app.h index 6021c5d..2a2ec90 100644 --- a/include/rex/rex_app.h +++ b/include/rex/rex_app.h @@ -54,6 +54,7 @@ struct PathConfig { namespace ui { class ConsoleDialog; +class FpsOverlayDialog; class SettingsDialog; } // namespace ui @@ -238,6 +239,7 @@ class ReXApp : public ui::WindowedApp, public ui::WindowListener, public ui::Win std::unique_ptr debug_overlay_; std::unique_ptr console_overlay_; std::unique_ptr settings_overlay_; + std::unique_ptr fps_overlay_; ui::DebugOverlayDialog::FrameStatsProvider frame_stats_provider_; std::filesystem::path config_path_; }; diff --git a/include/rex/ui/imgui_dialog.h b/include/rex/ui/imgui_dialog.h index bce6d78..873ed47 100644 --- a/include/rex/ui/imgui_dialog.h +++ b/include/rex/ui/imgui_dialog.h @@ -39,6 +39,14 @@ class ImGuiDialog { void Draw(); + // Whether the dialog needs the UI to repaint continuously while it's open + // (interactive dialogs - input handling, animations). Passive readouts + // should return false so they only repaint when something else (such as a + // new guest output frame) triggers a paint - a continuous repaint loop + // presents without new guest frames, inflating externally measured FPS and + // burning CPU/GPU. + virtual bool WantsContinuousRepaint() const { return true; } + protected: ImGuiDialog(ImGuiDrawer* imgui_drawer); diff --git a/include/rex/ui/overlay/fps_overlay.h b/include/rex/ui/overlay/fps_overlay.h new file mode 100644 index 0000000..f1f2ae2 --- /dev/null +++ b/include/rex/ui/overlay/fps_overlay.h @@ -0,0 +1,36 @@ +/** + * @file rex/ui/overlay/fps_overlay.h + * + * @brief Minimal guest FPS readout overlay. + * + * @copyright Copyright (c) 2026 Tom Clay + * All rights reserved. + * + * @license BSD 3-Clause License + * See LICENSE file in the project root for full license text. + */ +#pragma once +#include +#include + +namespace rex::ui { + +// Compact corner readout of the guest frame rate measured by the presenter. +// Unlike the interactive overlays, this one does not request continuous +// repaints, so host presents stay paced by guest output refreshes and +// external FPS measurement tools (e.g. RTSS) remain accurate while it's open. +class FpsOverlayDialog : public ImGuiDialog { + public: + FpsOverlayDialog(ImGuiDrawer* imgui_drawer, const Presenter* presenter) + : ImGuiDialog(imgui_drawer), presenter_(presenter) {} + + bool WantsContinuousRepaint() const override { return false; } + + protected: + void OnDraw(ImGuiIO& io) override; + + private: + const Presenter* presenter_; +}; + +} // namespace rex::ui diff --git a/include/rex/ui/presenter.h b/include/rex/ui/presenter.h index bba6e15..0cff204 100644 --- a/include/rex/ui/presenter.h +++ b/include/rex/ui/presenter.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -339,6 +340,67 @@ class Presenter { bool RefreshGuestOutput(uint32_t frontbuffer_width, uint32_t frontbuffer_height, uint32_t display_aspect_ratio_x, uint32_t display_aspect_ratio_y, std::function refresher); + + // Guest frame pacing measured at RefreshGuestOutput - one sample per guest + // swap, independent of how many times the host paints (UI repaints while a + // dialog is open present without a new guest frame, so host present rate is + // not a valid guest FPS measurement). Thread-safe. + struct GuestFrameStats { + // Averaged over the sampling window (zero when no recent frames). + double fps = 0.0; + double frame_time_ms = 0.0; + // Time the guest output producer (GPU emulation thread) reported spending + // blocked on host GPU waits during the last frame, via + // AddGuestFrameWaitMicroseconds. Near frame_time_ms = host GPU bound, + // near zero = producer thread bound. + double wait_ms = 0.0; + // Host GPU time span of the most recently measured guest frame (first to + // last command on the device timeline), via + // SetGuestFrameGpuSpanMicroseconds. Zero when the backend doesn't report + // it. + double gpu_ms = 0.0; + // Optional GPU time breakdown of the measured frame (guest draws, + // resolves, render target dumps), via + // SetGuestFrameGpuBreakdownMicroseconds. All zero when the backend isn't + // measuring buckets. + double gpu_draw_ms = 0.0; + double gpu_resolve_ms = 0.0; + double gpu_dump_ms = 0.0; + // Total guest frames since startup. + uint64_t frame_count = 0; + }; + GuestFrameStats GetGuestFrameStats() const; + bool GuestFrameStatsEnabled() const { + return guest_frame_stats_enabled_.load(std::memory_order_relaxed); + } + void SetGuestFrameStatsEnabled(bool enabled); + // Reports time the guest output producer spent blocked waiting for the host + // GPU; accumulated into the current frame's stats and reset at the next + // RefreshGuestOutput. Thread-safe. + void AddGuestFrameWaitMicroseconds(uint64_t wait_us) { + if (!GuestFrameStatsEnabled()) { + return; + } + guest_frame_wait_accumulator_us_.fetch_add(wait_us, std::memory_order_relaxed); + } + // Reports the host GPU time span of a completed guest frame (measured by + // the backend, typically a few frames behind real time). Thread-safe. + void SetGuestFrameGpuSpanMicroseconds(uint64_t gpu_span_us) { + if (!GuestFrameStatsEnabled()) { + return; + } + guest_frame_gpu_span_us_.store(gpu_span_us, std::memory_order_relaxed); + } + // Reports the GPU time breakdown of a completed guest frame. Thread-safe. + void SetGuestFrameGpuBreakdownMicroseconds(uint64_t draw_us, uint64_t resolve_us, + uint64_t dump_us) { + if (!GuestFrameStatsEnabled()) { + return; + } + guest_frame_gpu_draw_us_.store(draw_us, std::memory_order_relaxed); + guest_frame_gpu_resolve_us_.store(resolve_us, std::memory_order_relaxed); + guest_frame_gpu_dump_us_.store(dump_us, std::memory_order_relaxed); + } // The implementation must be callable from any thread, including from // multiple at the same time, and it should acquire the latest guest output // image via ConsumeGuestOutput. @@ -968,6 +1030,26 @@ class Presenter { // rather than being blank. bool guest_output_active_last_refresh_ = false; + // Guest frame timestamps for GetGuestFrameStats, written by the refreshing + // thread, read by any thread. Sized to cover the stats window even at very + // high frame rates. + static constexpr size_t kGuestFrameTimestampCount = 512; + std::atomic guest_frame_stats_enabled_{false}; + mutable std::mutex guest_frame_stats_mutex_; + std::array + guest_frame_timestamps_{}; + size_t guest_frame_timestamp_next_ = 0; + size_t guest_frame_timestamp_count_ = 0; + uint64_t guest_frame_total_count_ = 0; + std::atomic guest_frame_wait_accumulator_us_{0}; + // Captured from the accumulator at each guest output refresh; guarded by + // guest_frame_stats_mutex_. + uint64_t guest_frame_last_wait_us_ = 0; + std::atomic guest_frame_gpu_span_us_{0}; + std::atomic guest_frame_gpu_draw_us_{0}; + std::atomic guest_frame_gpu_resolve_us_{0}; + std::atomic guest_frame_gpu_dump_us_{0}; + // Ordered by the Z order, and then by the time of addition. // Note: All the iteration logic involving this Z ordering must be the same as // in input handling (in the input listeners in the Window), but in reverse. diff --git a/include/rex/ui/vulkan/device.h b/include/rex/ui/vulkan/device.h index 5f55deb..bf1ca34 100644 --- a/include/rex/ui/vulkan/device.h +++ b/include/rex/ui/vulkan/device.h @@ -74,6 +74,8 @@ class VulkanDevice { uint32_t maxViewportDimensions[2] = {4096, 4096}; VkDeviceSize minUniformBufferOffsetAlignment = 256; VkDeviceSize minStorageBufferOffsetAlignment = 256; + uint32_t maxTexelBufferElements = 65536; + VkDeviceSize minTexelBufferOffsetAlignment = 256; uint32_t maxFramebufferWidth = 4096; uint32_t maxFramebufferHeight = 4096; VkSampleCountFlags framebufferColorSampleCounts = VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_4_BIT; @@ -184,6 +186,7 @@ class VulkanDevice { // `ext_major_minor_` prefix rather than `ext_`). struct Extensions { bool ext_KHR_swapchain = false; // #2 + bool ext_KHR_push_descriptor = false; // #81 bool ext_1_1_KHR_dedicated_allocation = false; // #128 bool ext_EXT_shader_stencil_export = false; // #141 bool ext_1_1_KHR_get_memory_requirements2 = false; // #147 @@ -214,6 +217,8 @@ class VulkanDevice { #include // VK_KHR_swapchain (#2) #include + // VK_KHR_push_descriptor (#81) +#include // VK_KHR_get_memory_requirements2 (#147, promoted to 1.1) #include // VK_KHR_bind_memory2 (#158, promoted to 1.1) diff --git a/include/rex/ui/vulkan/functions/device_1_0.inc b/include/rex/ui/vulkan/functions/device_1_0.inc index 8034d58..5b619f9 100644 --- a/include/rex/ui/vulkan/functions/device_1_0.inc +++ b/include/rex/ui/vulkan/functions/device_1_0.inc @@ -12,6 +12,7 @@ XE_UI_VULKAN_FUNCTION(vkCmdBindPipeline) XE_UI_VULKAN_FUNCTION(vkCmdBindVertexBuffers) XE_UI_VULKAN_FUNCTION(vkCmdClearAttachments) XE_UI_VULKAN_FUNCTION(vkCmdClearColorImage) +XE_UI_VULKAN_FUNCTION(vkCmdClearDepthStencilImage) XE_UI_VULKAN_FUNCTION(vkCmdCopyBuffer) XE_UI_VULKAN_FUNCTION(vkCmdCopyBufferToImage) XE_UI_VULKAN_FUNCTION(vkCmdCopyImageToBuffer) diff --git a/include/rex/ui/vulkan/functions/device_khr_push_descriptor.inc b/include/rex/ui/vulkan/functions/device_khr_push_descriptor.inc new file mode 100644 index 0000000..98625bb --- /dev/null +++ b/include/rex/ui/vulkan/functions/device_khr_push_descriptor.inc @@ -0,0 +1,2 @@ +// VK_KHR_push_descriptor functions used in Xenia. +XE_UI_VULKAN_FUNCTION(vkCmdPushDescriptorSetKHR) diff --git a/src/core/perf/counter.cpp b/src/core/perf/counter.cpp index 17954e6..0298f25 100644 --- a/src/core/perf/counter.cpp +++ b/src/core/perf/counter.cpp @@ -159,6 +159,13 @@ constexpr const char* kCounterNames[] = { "draw_stage_vertex_buffers_us", "draw_stage_barriers_us", "draw_stage_submit_us", + "draw_stage_system_constants_us", + "draw_sampler_fast_path_draws", + "draw_texture_fast_path_draws", + "draw_stage_analyze_us", + "draw_stage_translate_us", + "draw_stage_samplers_us", + "draw_stage_pre_draw_us", "gpu_main_us", "gpu_depth_us", "gpu_copy_us", @@ -181,6 +188,8 @@ constexpr const char* kCounterNames[] = { "gpu_resolve_query_us", "cpu_primary_buffer_us", "cpu_indirect_buffer_us", + "cpu_guest_wait_us", + "cpu_swap_us", "cpu_d3d12_begin_submission_us", "cpu_d3d12_begin_submission_fence_us", "cpu_d3d12_begin_submission_frame_open_us", @@ -203,6 +212,7 @@ constexpr const char* kCounterNames[] = { "cpu_d3d12_paint_ui_us", "cpu_d3d12_present_wait_us", "cpu_d3d12_present_us", + "gpu_thread_fence_wait_us", "texture_request_us", "texture_request_calls", "texture_fetches_requested", @@ -337,6 +347,13 @@ constexpr bool kIsGauge[] = { false, // kDrawStageVertexBuffersUs false, // kDrawStageBarriersUs false, // kDrawStageSubmitUs + false, // kDrawStageSystemConstantsUs + false, // kDrawSamplerFastPathDraws + false, // kDrawTextureFastPathDraws + false, // kDrawStageAnalyzeUs + false, // kDrawStageTranslateUs + false, // kDrawStageSamplersUs + false, // kDrawStagePreDrawUs false, // kGpuMainUs false, // kGpuDepthUs false, // kGpuCopyUs @@ -359,6 +376,8 @@ constexpr bool kIsGauge[] = { false, // kGpuResolveQueryUs false, // kCpuPrimaryBufferUs false, // kCpuIndirectBufferUs + false, // kCpuGuestWaitUs + false, // kCpuSwapUs false, // kCpuD3D12BeginSubmissionUs false, // kCpuD3D12BeginSubmissionFenceUs false, // kCpuD3D12BeginSubmissionFrameOpenUs @@ -381,6 +400,7 @@ constexpr bool kIsGauge[] = { false, // kCpuD3D12PaintUiUs false, // kCpuD3D12PresentWaitUs false, // kCpuD3D12PresentUs + false, // kGpuThreadFenceWaitUs false, // kTextureRequestUs false, // kTextureRequestCalls false, // kTextureFetchesRequested @@ -487,32 +507,7 @@ int g_csv_frame_count = 0; } // anonymous namespace -const char* DrawBucketName(DrawBucket bucket) { - switch (bucket) { - case DrawBucket::kMainColorDepth: - return "Main"; - case DrawBucket::kDepthOnly: - return "Depth"; - case DrawBucket::kCopyResolve: - return "Copy"; - case DrawBucket::kMemexport: - return "MemExp"; - case DrawBucket::kNoPixelShader: - return "NoPS"; - case DrawBucket::kCopyDump: - return "CopyDump"; - case DrawBucket::kCopyResolveShader: - return "CopyResolve"; - case DrawBucket::kCopyResolveFast32: - return "CopyResolveFast32"; - case DrawBucket::kCopyResolveFull32: - return "CopyResolveFull32"; - case DrawBucket::kResolveDownscale: - return "ResolveDownscale"; - default: - return "Unknown"; - } -} +// DrawBucketName is defined inline in counter.h. const char* CounterName(CounterId id) { auto idx = static_cast(id); diff --git a/src/graphics/command_processor.cpp b/src/graphics/command_processor.cpp index 1e2b37d..f6b274a 100644 --- a/src/graphics/command_processor.cpp +++ b/src/graphics/command_processor.cpp @@ -109,6 +109,13 @@ REXCVAR_DEFINE_BOOL(async_shader_compilation, true, "GPU", "pipelines are being prepared.") .lifecycle(rex::cvar::Lifecycle::kHotReload); +#ifdef REXGLUE_ENABLE_PERF_COUNTERS +REXCVAR_DEFINE_BOOL(gpu_cpu_profile, false, "GPU", + "Log per-frame CPU time attribution for the GPU emulation thread " + "(packet processing, draw stages, submission) every 120 frames") + .lifecycle(rex::cvar::Lifecycle::kHotReload); +#endif + namespace rex::graphics { using namespace rex::graphics::xenos; @@ -1132,12 +1139,104 @@ bool CommandProcessor::ExecutePacketType3_XE_SWAP(memory::RingBuffer* reader, ui uint32_t frontbuffer_height = reader->ReadAndSwap(); reader->AdvanceRead((count - 4) * sizeof(uint32_t)); - IssueSwap(frontbuffer_ptr, frontbuffer_width, frontbuffer_height); + { + PROFILE_SCOPE_COUNTER(kCpuSwapUs); + IssueSwap(frontbuffer_ptr, frontbuffer_width, frontbuffer_height); + } + +#ifdef REXGLUE_ENABLE_PERF_COUNTERS + CpuProfileOnFrameEnd(); +#endif ++counter_; return true; } +#ifdef REXGLUE_ENABLE_PERF_COUNTERS +void CommandProcessor::CpuProfileOnFrameEnd() { + namespace cpu_profile = rex::perf::cpu_profile; + if (!REXCVAR_GET(gpu_cpu_profile)) { + if (cpu_profile::enabled.load(std::memory_order_relaxed)) { + cpu_profile::enabled.store(false, std::memory_order_relaxed); + } + return; + } + const uint64_t now_ticks = cpu_profile::Ticks(); + const std::chrono::steady_clock::time_point now_time = std::chrono::steady_clock::now(); + if (!cpu_profile::enabled.load(std::memory_order_relaxed)) { + // Newly armed - start accumulating from the next frame. + cpu_profile::enabled.store(true, std::memory_order_relaxed); + cpu_profile::Reset(); + cpu_profile_frames_ = 0; + cpu_profile_window_start_ticks_ = now_ticks; + cpu_profile_window_start_time_ = now_time; + REXGPU_INFO("CPU profile armed (gpu_cpu_profile): logging every {} frames", + kCpuProfileLogFrameInterval); + return; + } + if (++cpu_profile_frames_ < kCpuProfileLogFrameInterval) { + return; + } + + // Scope timers record raw TSC ticks; calibrate them against the wall-clock + // duration of the whole logging window. + const int64_t window_us = std::chrono::duration_cast( + now_time - cpu_profile_window_start_time_) + .count(); + const uint64_t window_ticks = now_ticks - cpu_profile_window_start_ticks_; + const uint32_t frames = cpu_profile_frames_; + if (window_us > 0 && window_ticks > 0) { + const double us_per_tick = double(window_us) / double(window_ticks); + using rex::perf::CounterId; + auto avg_us = [&](CounterId id) -> uint64_t { + return uint64_t(double(cpu_profile::slots[size_t(id)].ticks) * us_per_tick) / frames; + }; + auto avg_count = [&](CounterId id) -> uint64_t { + return cpu_profile::slots[size_t(id)].count / frames; + }; + // Nesting: everything below is inside pm4. copy is inside draw (resolves + // are initiated by draw packets); sub_begin/fence waits happen inside + // draws; sub_end happens inside swap (frame-ending submission) unless + // submitting per primary buffer end. + const uint64_t pm4_us = avg_us(CounterId::kCpuPrimaryBufferUs); + const uint64_t guest_wait_us = avg_us(CounterId::kCpuGuestWaitUs); + const uint64_t draw_us = avg_us(CounterId::kDrawStageTotalUs); + const uint64_t swap_us = avg_us(CounterId::kCpuSwapUs); + const uint64_t accounted_us = guest_wait_us + draw_us + swap_us; + REXGPU_INFO( + "CPU profile (avg us/frame over {} frames): frame={} pm4={} draws={} | draw={} [anlz={} " + "prim={} xlat={} smp={} rt={} pso={} tex={} fixfn={} sysconst={} bind={} vb={} barrier={} " + "pre={} subm={}] copy={} ({}x) | sub_begin={} [fence_check={} fence_wait={}] sub_end={} " + "({}x) [pipe_end={} barriers={} record={} close={} qsubmit={} signal={}] | swap={} " + "guest_wait={} pm4_other={} | fast_smp={} fast_tex={}", + frames, window_us / frames, pm4_us, avg_count(CounterId::kDrawStageTotalUs), draw_us, + avg_us(CounterId::kDrawStageAnalyzeUs), avg_us(CounterId::kDrawStagePrimitiveUs), + avg_us(CounterId::kDrawStageTranslateUs), avg_us(CounterId::kDrawStageSamplersUs), + avg_us(CounterId::kDrawStageRenderTargetUs), + avg_us(CounterId::kDrawStagePipelineUs), avg_us(CounterId::kDrawStageTextureUs), + avg_us(CounterId::kDrawStageFixedFunctionUs), + avg_us(CounterId::kDrawStageSystemConstantsUs), avg_us(CounterId::kDrawStageBindingsUs), + avg_us(CounterId::kDrawStageVertexBuffersUs), avg_us(CounterId::kDrawStageBarriersUs), + avg_us(CounterId::kDrawStagePreDrawUs), avg_us(CounterId::kDrawStageSubmitUs), + avg_us(CounterId::kRtResolveUs), + avg_count(CounterId::kRtResolveUs), avg_us(CounterId::kCpuD3D12BeginSubmissionUs), + avg_us(CounterId::kCpuD3D12CheckFenceUs), avg_us(CounterId::kCpuD3D12FenceWaitUs), + avg_us(CounterId::kCpuD3D12EndSubmissionUs), avg_count(CounterId::kCpuD3D12EndSubmissionUs), + avg_us(CounterId::kCpuD3D12PipelineEndUs), avg_us(CounterId::kCpuD3D12SubmitBarriersUs), + avg_us(CounterId::kCpuD3D12DeferredExecuteUs), + avg_us(CounterId::kCpuD3D12CommandListCloseUs), + avg_us(CounterId::kCpuD3D12ExecuteCommandListsUs), avg_us(CounterId::kCpuD3D12SignalUs), + swap_us, guest_wait_us, pm4_us > accounted_us ? pm4_us - accounted_us : 0, + avg_count(CounterId::kDrawSamplerFastPathDraws), + avg_count(CounterId::kDrawTextureFastPathDraws)); + } + cpu_profile::Reset(); + cpu_profile_frames_ = 0; + cpu_profile_window_start_ticks_ = now_ticks; + cpu_profile_window_start_time_ = now_time; +} +#endif + bool CommandProcessor::ExecutePacketType3_INDIRECT_BUFFER(memory::RingBuffer* reader, uint32_t packet, uint32_t count) { // indirect buffer dispatch @@ -1205,7 +1304,9 @@ bool CommandProcessor::ExecutePacketType3_WAIT_REG_MEM(memory::RingBuffer* reade break; } if (!matched) { - // Wait. + // Wait. Attributed separately so time spent blocked on guest-side + // progress isn't read as command processing cost. + PROFILE_SCOPE_COUNTER(kCpuGuestWaitUs); if (wait >= 0x100) { PrepareForWait(); if (!REXCVAR_GET(vsync)) { diff --git a/src/graphics/d3d12/command_processor.cpp b/src/graphics/d3d12/command_processor.cpp index 7e13c51..a93d9cb 100644 --- a/src/graphics/d3d12/command_processor.cpp +++ b/src/graphics/d3d12/command_processor.cpp @@ -50,6 +50,11 @@ REXCVAR_DEFINE_BOOL(d3d12_submit_on_primary_buffer_end, true, "GPU/D3D12", "Submit command list when PM4 primary buffer ends") .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_BOOL(d3d12_gpu_timestamp_buckets, false, "GPU/D3D12", + "Measure GPU time of draws and resolves with timestamp queries and report " + "the breakdown in the FPS overlay and a periodic profile log") + .lifecycle(rex::cvar::Lifecycle::kHotReload); + REXCVAR_DEFINE_BOOL(skate3_ultrawide_skip_shadow_targets, true, "Skate 3", "Do not apply Skate 3 Hor+ NDC correction while rendering likely shadow maps") .lifecycle(rex::cvar::Lifecycle::kHotReload); @@ -215,6 +220,18 @@ bool ShouldApplySkate3UltrawideHorPlus( return selected; } +// Frame-cached read of the cross-TU draw trace flag - the registry Query is a +// mutex + string hash, too expensive to run for every draw. +bool QuerySkate3UltrawideTraceDraws(uint64_t frame) { + thread_local uint64_t cached_frame = UINT64_MAX; + thread_local bool cached_value = false; + if (cached_frame != frame) { + cached_value = rex::cvar::Query("skate3_ultrawide_trace_draws"); + cached_frame = frame; + } + return cached_value; +} + #ifdef REXGLUE_ENABLE_PERF_COUNTERS class ScopedDrawStageTimer { public: @@ -246,7 +263,9 @@ class ScopedDrawStageTimer { #define REX_D3D12_DRAW_STAGE_TIMER(counter_id) \ ScopedDrawStageTimer REX_CONCAT(draw_stage_timer_, __LINE__)(rex::perf::CounterId::counter_id) #else -#define REX_D3D12_DRAW_STAGE_TIMER(counter_id) +// Without compiled-in perf counters, draw stage timers still feed the +// runtime-gated rex::perf::cpu_profile attribution (gpu_cpu_profile cvar). +#define REX_D3D12_DRAW_STAGE_TIMER(counter_id) PROFILE_SCOPE_COUNTER(counter_id) #endif } // namespace @@ -1929,9 +1948,7 @@ bool D3D12CommandProcessor::SetupContext() { pix_capture_requested_.store(false, std::memory_order_relaxed); pix_capturing_ = false; occlusion_query_resources_available_ = InitializeOcclusionQueryResources(); -#ifdef REXGLUE_ENABLE_PERF_COUNTERS gpu_timestamp_resources_available_ = InitializeGpuTimestampResources(); -#endif // Just not to expose uninitialized memory. std::memset(&system_constants_, 0, sizeof(system_constants_)); @@ -1942,9 +1959,7 @@ bool D3D12CommandProcessor::SetupContext() { void D3D12CommandProcessor::ShutdownContext() { AwaitAllQueueOperationsCompletion(); InvalidateAllVertexBufferResidency(); -#ifdef REXGLUE_ENABLE_PERF_COUNTERS ShutdownGpuTimestampResources(); -#endif ShutdownOcclusionQueryResources(); ui::d3d12::util::ReleaseAndNull(readback_buffer_); @@ -2812,9 +2827,12 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3 // Update viewport, scissor, blend factor and stencil reference. UpdateFixedFunctionState(viewport_info, scissor, primitive_polygonal, normalized_depth_control); + } - // Update system constants before uploading them. - // TODO(Triang3l): With ROV, pass the disabled render target mask for safety. + // Update system constants before uploading them. + // TODO(Triang3l): With ROV, pass the disabled render target mask for safety. + { + REX_D3D12_DRAW_STAGE_TIMER(kDrawStageSystemConstantsUs); UpdateSystemConstantValues(memexport_used, primitive_polygonal, primitive_processing_result.host_vertex_shader_type, primitive_processing_result.line_loop_closing_index, @@ -2983,7 +3001,9 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3 const uint32_t skate3_ultrawide_draw_primitive_count = draw_util::EstimatePrimitiveCount( primitive_processing_result.host_primitive_type, host_draw_vertex_count); bool skate3_ultrawide_submit_draw = true; - if (rex::cvar::Query("skate3_ultrawide_trace_draws")) { + // Frame-cached: the registry Query takes the registry mutex and hashes the + // name, far too expensive for a per-draw debug-flag check. + if (QuerySkate3UltrawideTraceDraws(frame_current_)) { ultrawide_debug::DrawFingerprint fingerprint; fingerprint.bucket = memexport_used @@ -3043,9 +3063,6 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3 if (!skate3_ultrawide_submit_draw) { return true; } -#ifdef REXGLUE_ENABLE_PERF_COUNTERS - const uint32_t host_draw_primitive_count = draw_util::EstimatePrimitiveCount( - primitive_processing_result.host_primitive_type, host_draw_vertex_count); const rex::perf::DrawBucket draw_bucket = memexport_used ? rex::perf::DrawBucket::kMemexport @@ -3053,6 +3070,9 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3 ? rex::perf::DrawBucket::kDepthOnly : (pixel_shader == nullptr ? rex::perf::DrawBucket::kNoPixelShader : rex::perf::DrawBucket::kMainColorDepth)); +#ifdef REXGLUE_ENABLE_PERF_COUNTERS + const uint32_t host_draw_primitive_count = draw_util::EstimatePrimitiveCount( + primitive_processing_result.host_primitive_type, host_draw_vertex_count); const bool collect_draw_fingerprints = rex::perf::ShouldCollectDrawFingerprints(); rex::perf::DrawFingerprint draw_fingerprint; if (collect_draw_fingerprints) { @@ -3105,12 +3125,10 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3 if (collect_draw_fingerprints) { PROFILE_DRAW_FINGERPRINT(draw_fingerprint); } +#endif uint32_t gpu_timestamp_query = BeginGpuTimestampedDraw(draw_bucket); -#endif deferred_command_list_.D3DDrawInstanced(host_draw_vertex_count, 1, 0, 0); -#ifdef REXGLUE_ENABLE_PERF_COUNTERS EndGpuTimestampedDraw(gpu_timestamp_query); -#endif } } else { D3D12_INDEX_BUFFER_VIEW index_buffer_view; @@ -3187,12 +3205,10 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3 if (collect_draw_fingerprints) { PROFILE_DRAW_FINGERPRINT(draw_fingerprint); } +#endif uint32_t gpu_timestamp_query = BeginGpuTimestampedDraw(draw_bucket); -#endif deferred_command_list_.D3DDrawIndexedInstanced(host_draw_vertex_count, 1, 0, 0, 0); -#ifdef REXGLUE_ENABLE_PERF_COUNTERS EndGpuTimestampedDraw(gpu_timestamp_query); -#endif } if (scratch_index_buffer != nullptr) { ReleaseScratchGPUBuffer(scratch_index_buffer, D3D12_RESOURCE_STATE_INDEX_BUFFER); @@ -3399,6 +3415,7 @@ bool D3D12CommandProcessor::IssueCopy() { #if XE_GPU_FINE_GRAINED_DRAW_SCOPES SCOPE_profile_cpu_f("gpu"); #endif // XE_GPU_FINE_GRAINED_DRAW_SCOPES + PROFILE_SCOPE_COUNTER(kRtResolveUs); if (!BeginSubmission(true)) { return false; } @@ -3407,8 +3424,14 @@ bool D3D12CommandProcessor::IssueCopy() { if (readback_mode == ReadbackResolveMode::kDisabled && (!texture_cache_->IsDrawResolutionScaled() || gameplay_state_active)) { uint32_t written_address, written_length; - return render_target_cache_->Resolve(*memory_, *shared_memory_, *texture_cache_, - written_address, written_length); + // Time the whole resolve (render target dump, copy and clear) for the GPU + // profile breakdown. + uint32_t gpu_timestamp_query = + BeginGpuTimestampedDraw(rex::perf::DrawBucket::kCopyResolve); + bool resolved = render_target_cache_->Resolve(*memory_, *shared_memory_, *texture_cache_, + written_address, written_length); + EndGpuTimestampedDraw(gpu_timestamp_query); + return resolved; } return IssueCopy_ReadbackResolvePath(); } @@ -3662,7 +3685,16 @@ void D3D12CommandProcessor::CheckSubmissionFence(uint64_t await_submission) { fence_value, fence_completion_event_)))) { PROFILE_CMD_BUFFER_STALL(); rex::perf::ScopedCounterTimer wait_timer(rex::perf::CounterId::kCpuD3D12FenceWaitUs); + rex::perf::ScopedCounterTimer gpu_thread_wait_timer( + rex::perf::CounterId::kGpuThreadFenceWaitUs, true); + const std::chrono::steady_clock::time_point wait_start = std::chrono::steady_clock::now(); WaitForSingleObject(fence_completion_event_, INFINITE); + if (ui::Presenter* wait_presenter = graphics_system_->presenter()) { + wait_presenter->AddGuestFrameWaitMicroseconds(uint64_t( + std::chrono::duration_cast( + std::chrono::steady_clock::now() - wait_start) + .count())); + } queue_operations_done_since_submission_signal_ = false; } else { REXGPU_ERROR( @@ -3682,7 +3714,18 @@ void D3D12CommandProcessor::CheckSubmissionFence(uint64_t await_submission) { submission_fence_->SetEventOnCompletion(await_submission, fence_completion_event_))) { PROFILE_CMD_BUFFER_STALL(); rex::perf::ScopedCounterTimer wait_timer(rex::perf::CounterId::kCpuD3D12FenceWaitUs); + rex::perf::ScopedCounterTimer gpu_thread_wait_timer( + rex::perf::CounterId::kGpuThreadFenceWaitUs, true); + // Also reported through the presenter so the FPS overlay can show the + // blocked time in builds without perf counters. + const std::chrono::steady_clock::time_point wait_start = std::chrono::steady_clock::now(); WaitForSingleObject(fence_completion_event_, INFINITE); + if (ui::Presenter* wait_presenter = graphics_system_->presenter()) { + wait_presenter->AddGuestFrameWaitMicroseconds(uint64_t( + std::chrono::duration_cast(std::chrono::steady_clock::now() - + wait_start) + .count())); + } submission_completed_ = submission_fence_->GetCompletedValue(); } } @@ -3933,9 +3976,7 @@ bool D3D12CommandProcessor::BeginSubmission(bool is_guest_command) { texture_cache_->BeginFrame(); -#ifdef REXGLUE_ENABLE_PERF_COUNTERS BeginGpuTimestampFrame(); -#endif } return true; @@ -3967,9 +4008,7 @@ bool D3D12CommandProcessor::EndSubmission(bool is_swap) { if (is_closing_frame) { { PROFILE_SCOPE_COUNTER(kCpuD3D12EndFrameUs); -#ifdef REXGLUE_ENABLE_PERF_COUNTERS EndGpuTimestampFrame(); -#endif texture_cache_->EndFrame(); @@ -4011,9 +4050,7 @@ bool D3D12CommandProcessor::EndSubmission(bool is_swap) { deferred_command_list_.Execute(command_list_, command_list_1_); } if (is_closing_frame) { -#ifdef REXGLUE_ENABLE_PERF_COUNTERS ResolveGpuTimestampFrame(command_list_); -#endif } { PROFILE_SCOPE_COUNTER(kCpuD3D12CommandListCloseUs); @@ -5511,12 +5548,15 @@ void D3D12CommandProcessor::ShutdownGpuTimestampResources() { gpu_timestamp_query_heap_.Reset(); gpu_timestamp_frequency_ = 0; gpu_timestamp_resources_available_ = false; + gpu_timestamp_draws_enabled_ = false; for (auto& frame : gpu_timestamp_frames_) { frame = {}; } } void D3D12CommandProcessor::BeginGpuTimestampFrame() { + gpu_timestamp_draws_enabled_ = + rex::perf::IsCaptureRecording() || REXCVAR_GET(d3d12_gpu_timestamp_buckets); if (!gpu_timestamp_resources_available_) { return; } @@ -5532,7 +5572,7 @@ void D3D12CommandProcessor::BeginGpuTimestampFrame() { frame.bucket_start_queries.clear(); frame.counter_ids.clear(); frame.counter_start_queries.clear(); - if (rex::perf::IsCaptureRecording() && gpu_timestamp_query_heap_) { + if (gpu_timestamp_draws_enabled_ && gpu_timestamp_query_heap_) { frame.frame_start_query = frame.query_count++; deferred_command_list_.D3DEndQuery(gpu_timestamp_query_heap_.Get(), D3D12_QUERY_TYPE_TIMESTAMP, frame.query_base + frame.frame_start_query); @@ -5582,17 +5622,24 @@ void D3D12CommandProcessor::ProcessGpuTimestampResults() { continue; } const uint64_t* timestamps = gpu_timestamp_readback_mapping_ + frame.query_base; + int64_t frame_span_us = 0; if (frame.frame_start_query != UINT32_MAX && frame.frame_end_query != UINT32_MAX) { uint64_t start = timestamps[frame.frame_start_query]; uint64_t end = timestamps[frame.frame_end_query]; if (end > start) { int64_t elapsed_us = static_cast((end - start) * uint64_t(1000000) / gpu_timestamp_frequency_); + frame_span_us = elapsed_us; rex::perf::IncrementCounter(rex::perf::CounterId::kGpuCommandProcessorFrameUs, elapsed_us); rex::perf::IncrementCounter(rex::perf::CounterId::kGpuTimestampedFrames); + if (ui::Presenter* presenter = graphics_system_->presenter()) { + presenter->SetGuestFrameGpuSpanMicroseconds(uint64_t(elapsed_us)); + } } } + uint64_t bucket_draw_us = 0; + uint64_t bucket_resolve_us = 0; for (uint32_t i = 0; i < frame.buckets.size(); ++i) { if (i >= frame.bucket_start_queries.size()) { break; @@ -5608,28 +5655,70 @@ void D3D12CommandProcessor::ProcessGpuTimestampResults() { } int64_t elapsed_us = static_cast((end - start) * uint64_t(1000000) / gpu_timestamp_frequency_); + if (size_t(frame.buckets[i]) < size_t(rex::perf::DrawBucket::kCount)) { + gpu_profile_bucket_us_[size_t(frame.buckets[i])] += uint64_t(elapsed_us); + } rex::perf::CounterId counter_id; switch (frame.buckets[i]) { case rex::perf::DrawBucket::kMainColorDepth: counter_id = rex::perf::CounterId::kGpuMainUs; + bucket_draw_us += uint64_t(elapsed_us); break; case rex::perf::DrawBucket::kDepthOnly: counter_id = rex::perf::CounterId::kGpuDepthUs; + bucket_draw_us += uint64_t(elapsed_us); break; case rex::perf::DrawBucket::kCopyResolve: counter_id = rex::perf::CounterId::kGpuCopyUs; + bucket_resolve_us += uint64_t(elapsed_us); break; case rex::perf::DrawBucket::kMemexport: counter_id = rex::perf::CounterId::kGpuMemexportUs; + bucket_draw_us += uint64_t(elapsed_us); break; case rex::perf::DrawBucket::kNoPixelShader: counter_id = rex::perf::CounterId::kGpuNoPixelShaderUs; + bucket_draw_us += uint64_t(elapsed_us); break; default: continue; } rex::perf::IncrementCounter(counter_id, elapsed_us); } + if (ui::Presenter* presenter = graphics_system_->presenter()) { + // Zeros when bucket timestamps are disabled, which hides the breakdown + // in the FPS overlay. Unlike on Vulkan, the draw buckets are individual + // begin/end pairs around the draw commands, so barriers and other + // in-between work are not included anywhere ("other" covers them). + presenter->SetGuestFrameGpuBreakdownMicroseconds(bucket_draw_us, bucket_resolve_us, 0); + } + // Periodic full attribution log, mirroring the Vulkan backend's. + if (frame_span_us > 0 && REXCVAR_GET(d3d12_gpu_timestamp_buckets)) { + gpu_profile_span_us_ += uint64_t(frame_span_us); + if (++gpu_profile_frames_ >= kGpuProfileLogFrameInterval) { + uint64_t buckets_sum_us = 0; + std::string breakdown; + for (size_t bucket = 0; bucket < size_t(rex::perf::DrawBucket::kCount); ++bucket) { + uint64_t total_us = gpu_profile_bucket_us_[bucket]; + buckets_sum_us += total_us; + if (!total_us) { + continue; + } + breakdown += + fmt::format(" {}={}", rex::perf::DrawBucketName(rex::perf::DrawBucket(bucket)), + total_us / gpu_profile_frames_); + } + uint64_t span_avg_us = gpu_profile_span_us_ / gpu_profile_frames_; + uint64_t sum_avg_us = buckets_sum_us / gpu_profile_frames_; + REXGPU_INFO( + "D3D12 GPU profile (avg us/frame over {} frames): span={} buckets={} untimed={} |{}", + gpu_profile_frames_, span_avg_us, sum_avg_us, + span_avg_us > sum_avg_us ? span_avg_us - sum_avg_us : 0, breakdown); + std::memset(gpu_profile_bucket_us_, 0, sizeof(gpu_profile_bucket_us_)); + gpu_profile_span_us_ = 0; + gpu_profile_frames_ = 0; + } + } for (uint32_t i = 0; i < frame.counter_ids.size(); ++i) { if (i >= frame.counter_start_queries.size()) { break; @@ -5657,7 +5746,7 @@ void D3D12CommandProcessor::ProcessGpuTimestampResults() { uint32_t D3D12CommandProcessor::BeginGpuTimestampedDraw(rex::perf::DrawBucket bucket) { if (!gpu_timestamp_resources_available_ || !gpu_timestamp_query_heap_ || - !rex::perf::IsCaptureRecording() || !frame_open_) { + !gpu_timestamp_draws_enabled_ || !frame_open_) { return UINT32_MAX; } diff --git a/src/graphics/d3d12/render_target_cache.cpp b/src/graphics/d3d12/render_target_cache.cpp index cfb636f..ecd5ef8 100644 --- a/src/graphics/d3d12/render_target_cache.cpp +++ b/src/graphics/d3d12/render_target_cache.cpp @@ -3968,6 +3968,24 @@ void D3D12RenderTargetCache::PerformTransfersAndResolveClears( DeferredCommandList& command_list = command_processor_.GetDeferredCommandList(); bool resolve_clear_needed = render_target_resolve_clear_values && resolve_clear_rectangle; + + // Time ownership transfers and resolve clears for the GPU profile breakdown. + // This is called with nothing to do for most draws - only time when there's + // actual work. + bool transfer_work_needed = resolve_clear_needed; + if (!transfer_work_needed) { + for (uint32_t i = 0; i < render_target_count; ++i) { + if (render_targets[i] && !render_target_transfers[i].empty()) { + transfer_work_needed = true; + break; + } + } + } + uint32_t gpu_timestamp_query = + transfer_work_needed + ? command_processor_.BeginGpuTimestampedDraw(rex::perf::DrawBucket::kRtTransfer) + : UINT32_MAX; + D3D12_RECT clear_rect; if (resolve_clear_needed) { // Assuming the rectangle is already clamped by the setup function from the @@ -4223,6 +4241,7 @@ void D3D12RenderTargetCache::PerformTransfersAndResolveClears( current_temporary_descriptors_gpu_.resize(descriptor_count); if (!command_processor_.RequestOneUseSingleViewDescriptors( descriptor_count, current_temporary_descriptors_gpu_.data())) { + command_processor_.EndGpuTimestampedDraw(gpu_timestamp_query); return; } for (uint32_t i = 0; i < descriptor_count; ++i) { @@ -4849,6 +4868,8 @@ void D3D12RenderTargetCache::PerformTransfersAndResolveClears( } } } + + command_processor_.EndGpuTimestampedDraw(gpu_timestamp_query); } void D3D12RenderTargetCache::SetCommandListRenderTargets( diff --git a/src/graphics/d3d12/texture_cache.cpp b/src/graphics/d3d12/texture_cache.cpp index c5783a6..b597141 100644 --- a/src/graphics/d3d12/texture_cache.cpp +++ b/src/graphics/d3d12/texture_cache.cpp @@ -1721,6 +1721,9 @@ bool D3D12TextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, return false; } uint32_t descriptor_write_index = 0; + // Time the load dispatches for the GPU profile breakdown. + uint32_t gpu_timestamp_query = + command_processor_.BeginGpuTimestampedDraw(rex::perf::DrawBucket::kTextureUpload); command_processor_.SetExternalPipeline(pipeline); command_list.D3DSetComputeRootSignature(load_root_signature_.Get()); // Set up the destination descriptor. @@ -1782,6 +1785,7 @@ bool D3D12TextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, is_base ? d3d12_texture.GetGuestBaseSize() : d3d12_texture.GetGuestMipsSize(); if (!MakeScaledResolveRangeCurrent(guest_address, guest_size_unscaled, load_shader_info.source_bpe_log2)) { + command_processor_.EndGpuTimestampedDraw(gpu_timestamp_query); command_processor_.ReleaseScratchGPUBuffer(copy_buffer, copy_buffer_state); return false; } @@ -1933,6 +1937,8 @@ bool D3D12TextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, } } + command_processor_.EndGpuTimestampedDraw(gpu_timestamp_query); + command_processor_.ReleaseScratchGPUBuffer(copy_buffer, copy_buffer_state); return true; diff --git a/src/graphics/pipeline/render_target/cache.cpp b/src/graphics/pipeline/render_target/cache.cpp index 382b0b2..2743728 100644 --- a/src/graphics/pipeline/render_target/cache.cpp +++ b/src/graphics/pipeline/render_target/cache.cpp @@ -1423,4 +1423,45 @@ void RenderTargetCache::ChangeOwnership(RenderTargetKey dest, uint32_t start_til } } +bool RenderTargetCache::IsOwnershipConfinedToRange(RenderTargetKey key, + uint32_t start_tiles_base_relative, + uint32_t length_tiles) const { + if (key.IsEmpty()) { + return false; + } + // Convert to up to two absolute extents the same way as ChangeOwnership. + uint32_t start_tiles = (key.base_tiles + start_tiles_base_relative) & (xenos::kEdramTileCount - 1); + uint32_t end_tiles = start_tiles + length_tiles; + uint32_t extent_starts[2] = {start_tiles, 0}; + uint32_t extent_ends[2] = {std::min(end_tiles, xenos::kEdramTileCount), 0}; + uint32_t extent_count = 1; + if (end_tiles > xenos::kEdramTileCount) { + extent_ends[extent_count++] = std::min(end_tiles & (xenos::kEdramTileCount - 1), start_tiles); + } + bool host_depth_encoding_different = key.is_depth && GetPath() == Path::kHostRenderTargets && + IsHostDepthEncodingDifferent(key.GetDepthFormat()); + for (const auto& range_pair : ownership_ranges_) { + uint32_t range_start = range_pair.first; + uint32_t range_end = range_pair.second.end_tiles; + bool references_key = range_pair.second.render_target == key; + if (!references_key && host_depth_encoding_different) { + references_key = range_pair.second.GetHostDepthRenderTarget(key.GetDepthFormat()) == key; + } + if (!references_key) { + continue; + } + bool contained = false; + for (uint32_t i = 0; i < extent_count; ++i) { + if (range_start >= extent_starts[i] && range_end <= extent_ends[i]) { + contained = true; + break; + } + } + if (!contained) { + return false; + } + } + return true; +} + } // namespace rex::graphics diff --git a/src/graphics/pipeline/texture/cache.cpp b/src/graphics/pipeline/texture/cache.cpp index 25f4d50..acd9e29 100644 --- a/src/graphics/pipeline/texture/cache.cpp +++ b/src/graphics/pipeline/texture/cache.cpp @@ -725,6 +725,7 @@ void TextureCache::RequestTextures(uint32_t used_texture_mask) { } PERF_counter_add(kTextureBindingsChanged, changed_binding_count); #endif + ++texture_bindings_generation_; UpdateTextureBindingsImpl(bindings_changed); } DebugLogScaledTextureBindings(used_texture_mask); @@ -837,6 +838,16 @@ bool TextureCache::DebugIsTeamProfileBackgroundTexture(const Texture& texture) c if (!DebugIsTeamProfileBackgroundCandidateKey(key) || key.scaled_resolve) { return false; } + // Identifying the texture requires hashing up to 32 KB of guest memory, and + // candidate keys (128x256 DXT4_5) can be bound across many draws per frame. + // Never pay that cost unless one of the team-profile-background debug logs + // is armed. The draws cvar is defined in the Vulkan command processor, so it + // is read through the registry rather than REXCVAR_GET. + if (REXCVAR_GET(vulkan_debug_log_team_profile_background_candidates_remaining) <= 0 && + REXCVAR_GET(vulkan_debug_log_team_profile_background_bindings_remaining) <= 0 && + rex::cvar::Query("vulkan_debug_log_team_profile_background_draws_remaining") <= 0) { + return false; + } const uint8_t* data = shared_memory().DebugTranslatePhysical(key.base_page << 12); const uint32_t sample_length = std::min(texture.GetGuestBaseSize(), UINT32_C(0x8000)); if (data == nullptr || sample_length == 0) { @@ -1671,8 +1682,11 @@ void TextureCache::ResetTextureBindings(bool from_destructor) { bindings_reset |= UINT32_C(1) << i; } texture_bindings_in_sync_ &= ~bindings_reset; - if (!from_destructor && bindings_reset) { - UpdateTextureBindingsImpl(bindings_reset); + if (bindings_reset) { + ++texture_bindings_generation_; + if (!from_destructor) { + UpdateTextureBindingsImpl(bindings_reset); + } } } diff --git a/src/graphics/vulkan/command_processor.cpp b/src/graphics/vulkan/command_processor.cpp index 274dea0..c59ac1f 100644 --- a/src/graphics/vulkan/command_processor.cpp +++ b/src/graphics/vulkan/command_processor.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -46,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -120,6 +122,12 @@ REXCVAR_DEFINE_BOOL(vulkan_submit_on_primary_buffer_end, true, "GPU/Vulkan", "Submit command buffer when PM4 primary buffer ends") .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_INT32(vulkan_gpu_timestamp_trace_frames, 0, "GPU/Vulkan", + "Log every GPU timestamp bucket interval (start/end/duration) for this many " + "frames, for auditing the time attribution") + .range(0, 100) + .lifecycle(rex::cvar::Lifecycle::kHotReload); + REXCVAR_DEFINE_BOOL(vulkan_dynamic_rendering, true, "GPU/Vulkan", "Use VK_KHR_dynamic_rendering for Vulkan GPU emulation when supported by the " "device (falls back to render passes otherwise)") @@ -129,6 +137,28 @@ REXCVAR_DEFINE_BOOL(vulkan_skip_inert_no_pixel_draws, false, "GPU/Vulkan", "occlusion side effects") .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_BOOL(vulkan_reuse_transient_texture_descriptors, true, "GPU/Vulkan", + "Reuse texture/sampler descriptor sets within a frame when the bindings are " + "unchanged instead of allocating and writing new ones for every draw") + .lifecycle(rex::cvar::Lifecycle::kHotReload); + +REXCVAR_DEFINE_BOOL(vulkan_push_constants_descriptors, true, "GPU/Vulkan", + "Use VK_KHR_push_descriptor for the guest constant buffer descriptor set " + "instead of allocating and writing a transient descriptor set whenever " + "constants change (requires driver support; applied at startup)"); + +REXCVAR_DEFINE_BOOL(vulkan_reuse_unchanged_draw_bindings, true, "GPU/Vulkan", + "Skip the per-draw sampler cache walks and texture descriptor " + "gathering/hashing entirely when the previous draw in the same submission " + "used the same shaders with identical sampler parameters and texture " + "binding views") + .lifecycle(rex::cvar::Lifecycle::kHotReload); + +REXCVAR_DEFINE_BOOL(vulkan_gpu_timestamp_buckets, false, "GPU/Vulkan", + "Measure per-bucket GPU times (guest draws, resolves, render target dumps) " + "with timestamp queries and report them in the FPS overlay") + .lifecycle(rex::cvar::Lifecycle::kHotReload); + // Skate 3 ultrawide cvars. These mirror the Direct3D 12 backend's definitions // so the Hor+ classifier behaves identically on Vulkan; the D3D12 translation // unit is not compiled on this platform, so without these the queries below @@ -273,6 +303,18 @@ Skate3UltrawideDrawState SnapshotSkate3UltrawideDrawState(uint64_t frame) { return cached_state; } +// Frame-cached read of the cross-TU draw trace flag - the registry Query is a +// mutex + string hash, too expensive to run for every draw. +bool QuerySkate3UltrawideTraceDraws(uint64_t frame) { + thread_local uint64_t cached_frame = UINT64_MAX; + thread_local bool cached_value = false; + if (cached_frame != frame) { + cached_value = rex::cvar::Query("skate3_ultrawide_trace_draws"); + cached_frame = frame; + } + return cached_value; +} + bool IsSkate3UltrawideDisplayViewport(const draw_util::ViewportInfo& viewport_info) { const uint32_t width = viewport_info.xy_extent[0]; const uint32_t height = viewport_info.xy_extent[1]; @@ -1162,6 +1204,17 @@ bool VulkanCommandProcessor::SetupContext() { return false; } // Guest draw constants. + // With VK_KHR_push_descriptor, the constant buffer descriptors are pushed + // directly into the command buffer instead of allocating and writing a + // transient descriptor set whenever any constant buffer changes (which is + // nearly every draw due to guest float constant updates). + constants_push_descriptors_used_ = vulkan_device->extensions().ext_KHR_push_descriptor && + REXCVAR_GET(vulkan_push_constants_descriptors); + if (constants_push_descriptors_used_) { + REXGPU_INFO( + "VulkanCommandProcessor: using push descriptors for guest constant " + "buffers"); + } VkDescriptorSetLayoutBinding descriptor_set_layout_bindings_constants[SpirvShaderTranslator::kConstantBufferCount] = {}; for (uint32_t i = 0; i < SpirvShaderTranslator::kConstantBufferCount; ++i) { @@ -1187,6 +1240,10 @@ bool VulkanCommandProcessor::SetupContext() { descriptor_set_layout_create_info.bindingCount = uint32_t(rex::countof(descriptor_set_layout_bindings_constants)); descriptor_set_layout_create_info.pBindings = descriptor_set_layout_bindings_constants; + if (constants_push_descriptors_used_) { + descriptor_set_layout_create_info.flags |= + VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR; + } if (dfn.vkCreateDescriptorSetLayout(device, &descriptor_set_layout_create_info, nullptr, &descriptor_set_layout_constants_) != VK_SUCCESS) { REXGPU_ERROR( @@ -1194,6 +1251,8 @@ bool VulkanCommandProcessor::SetupContext() { "constant buffers"); return false; } + descriptor_set_layout_create_info.flags &= + ~VkDescriptorSetLayoutCreateFlags(VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR); // Transient: storage buffer for compute shaders. VkDescriptorSetLayoutBinding descriptor_set_layout_binding_transient; descriptor_set_layout_binding_transient.binding = 0; @@ -3492,11 +3551,26 @@ bool VulkanCommandProcessor::SubmitBarriers(bool force_end_render_pass) { void VulkanCommandProcessor::SubmitBarriersAndEnterRenderTargetCacheRenderPass( VkRenderPass render_pass, const VulkanRenderTargetCache::Framebuffer* framebuffer) { - SubmitBarriers(false); const ui::vulkan::VulkanDevice* vulkan_device = GetVulkanDevice(); bool use_dynamic_rendering = REXCVAR_GET(vulkan_dynamic_rendering) && vulkan_device->properties().dynamicRendering; + // Time genuine render pass entries (pending barriers including image layout + // transitions, pass end/begin and deferred resolve clear replay) separately + // - reported in the FPS overlay's "other" line. Don't open a region on the + // no-op path (same pass, no barriers), it runs for every draw. + bool pass_entry_work = + !pending_barriers_.empty() || current_pending_barrier_.src_stage_mask || + current_pending_barrier_.dst_stage_mask || + !(in_render_pass_ && current_framebuffer_ == framebuffer && + (use_dynamic_rendering ? current_render_pass_ == VK_NULL_HANDLE + : current_render_pass_ == render_pass)); + if (pass_entry_work) { + BeginGpuTimestampedRegion(rex::perf::DrawBucket::kRenderPassEntry); + } + + SubmitBarriers(false); + if (use_dynamic_rendering) { if (in_render_pass_ && current_framebuffer_ == framebuffer && current_render_pass_ == VK_NULL_HANDLE) { @@ -3561,6 +3635,11 @@ void VulkanCommandProcessor::SubmitBarriersAndEnterRenderTargetCacheRenderPass( VK_SUBPASS_CONTENTS_INLINE); } in_render_pass_ = true; + + // Replay resolve clears deferred to the next render pass binding their + // render targets - they ride the render pass entered for guest draws anyway + // instead of requiring their own render passes during resolves. + render_target_cache_->FlushDeferredResolveClearsIntoEnteredRenderPass(framebuffer->host_extent); } void VulkanCommandProcessor::SubmitBarriersAndEnterRenderTargetCacheRenderPass( @@ -4050,6 +4129,8 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t const ui::vulkan::VulkanDevice::Properties& device_properties = GetVulkanDevice()->properties(); + rex::perf::ScopedCounterTimer analyze_stage_timer(rex::perf::CounterId::kDrawStageAnalyzeUs); + memexport_ranges_.clear(); // Vertex shader analysis. @@ -4131,6 +4212,8 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t VulkanShader::VulkanTranslation* vertex_shader_translation; VulkanShader::VulkanTranslation* pixel_shader_translation; bool memexport_writes_possible = memexport_used_vertex || memexport_used_pixel; + bool draw_samplers_reused = false; + analyze_stage_timer.Stop(); // Two iterations because a submission (even the current one - in which case // it needs to be ended, and a new one must be started) may need to be awaited @@ -4168,6 +4251,8 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t // Tessellation and rectangle expansion variants for rasterization are // produced by the primitive processor and are handled by the Vulkan // pipeline cache. + rex::perf::ScopedCounterTimer translate_stage_timer( + rex::perf::CounterId::kDrawStageTranslateUs); Shader::HostVertexShaderType host_vertex_shader_type = primitive_processing_result.host_vertex_shader_type; if (host_vertex_shader_type != Shader::HostVertexShaderType::kVertex && @@ -4199,16 +4284,53 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t pixel_shader_translation)) { return draw_fail("shader_translation"); } + translate_stage_timer.Stop(); + rex::perf::ScopedCounterTimer samplers_stage_timer( + rex::perf::CounterId::kDrawStageSamplersUs); // Obtain the samplers. Note that the bindings don't depend on the shader // modification, so if on the second iteration of this loop it becomes // different for some reason (like a race condition with the guest in index // buffer processing in the primitive processor resulting in different host // vertex shader types), the bindings will stay the same. - // TODO(Triang3l): Sampler caching and reuse for adjacent draws within one - // submission. + // Sampler reuse for adjacent draws within one submission (the upstream + // "TODO(Triang3l): Sampler caching and reuse" here): if the previous draw + // in this same submission used the same shaders and the sampler + // parameters derived from the fetch constants are unchanged, + // current_samplers_* already hold valid VkSamplers whose last-usage + // submission indices are current - skip the per-sampler cache lookups. + draw_samplers_reused = false; + if (!i && REXCVAR_GET(vulkan_reuse_unchanged_draw_bindings) && draw_binding_state_valid_ && + draw_binding_submission_ == GetCurrentSubmission() && + draw_binding_vertex_shader_ == vertex_shader && + draw_binding_pixel_shader_ == pixel_shader) { + draw_samplers_reused = true; + for (uint32_t j = 0; draw_samplers_reused && j < 2; ++j) { + const std::vector>& + shader_samplers = j ? current_samplers_pixel_ : current_samplers_vertex_; + const VulkanShader* shader = j ? pixel_shader : vertex_shader; + if (!shader) { + draw_samplers_reused = shader_samplers.empty(); + continue; + } + const std::vector& shader_sampler_bindings = + shader->GetSamplerBindingsAfterTranslation(); + if (shader_samplers.size() != shader_sampler_bindings.size()) { + draw_samplers_reused = false; + break; + } + for (size_t k = 0; k < shader_sampler_bindings.size(); ++k) { + if (shader_samplers[k].second == VK_NULL_HANDLE || + shader_samplers[k].first != + texture_cache_->GetSamplerParameters(shader_sampler_bindings[k])) { + draw_samplers_reused = false; + break; + } + } + } + } uint32_t samplers_overflowed_count = 0; - for (uint32_t j = 0; j < 2; ++j) { + for (uint32_t j = 0; !draw_samplers_reused && j < 2; ++j) { std::vector>& shader_samplers = j ? current_samplers_pixel_ : current_samplers_vertex_; if (!i) { @@ -4251,6 +4373,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t } } } + samplers_stage_timer.Stop(); if (!samplers_overflowed_count) { break; } @@ -4267,6 +4390,12 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t CheckSubmissionFenceAndDeviceLoss(sampler_overflow_await_submission); } + // Record the state the binding fast path of the next draw compares against. + draw_binding_state_valid_ = true; + draw_binding_submission_ = GetCurrentSubmission(); + draw_binding_vertex_shader_ = vertex_shader; + draw_binding_pixel_shader_ = pixel_shader; + uint32_t normalized_color_mask = pixel_shader ? draw_util::GetNormalizedColorMask(regs, pixel_shader->writes_color_targets()) : 0; @@ -4310,9 +4439,29 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageTextureUs); texture_cache_->RequestTextures(used_texture_mask); } - const bool debug_team_profile_bg_draw = texture_cache_->DebugFindTeamProfileBackgroundBinding( - used_texture_mask, debug_team_profile_bg_fetch_index, debug_team_profile_bg_signed_view, - &debug_team_profile_bg_texture_base, &debug_team_profile_bg_texture_length); + // With the samplers reused and the texture cache's binding views unchanged, + // UpdateBindings can keep the currently recorded texture descriptor sets + // without regathering and rehashing the image infos. + { + uint64_t texture_bindings_generation = texture_cache_->texture_bindings_generation(); + texture_bindings_unchanged_this_draw_ = + draw_samplers_reused && + draw_binding_texture_generation_ == texture_bindings_generation; + draw_binding_texture_generation_ = texture_bindings_generation; + if (draw_samplers_reused) { + rex::perf::cpu_profile::CountEvent(rex::perf::CounterId::kDrawSamplerFastPathDraws); + } + if (texture_bindings_unchanged_this_draw_) { + rex::perf::cpu_profile::CountEvent(rex::perf::CounterId::kDrawTextureFastPathDraws); + } + } + // The binding scan hashes guest texture memory to identify the texture, so + // only perform it while draw logging is actually armed. + const bool debug_team_profile_bg_draw = + REXCVAR_GET(vulkan_debug_log_team_profile_background_draws_remaining) > 0 && + texture_cache_->DebugFindTeamProfileBackgroundBinding( + used_texture_mask, debug_team_profile_bg_fetch_index, debug_team_profile_bg_signed_view, + &debug_team_profile_bg_texture_base, &debug_team_profile_bg_texture_length); const VulkanPipelineCache::PipelineLayoutProvider* pipeline_layout_provider; // Set up the render targets - this may perform dispatches and draws. @@ -4381,6 +4530,14 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t descriptor_sets_kept = std::min( descriptor_sets_kept, uint32_t(SpirvShaderTranslator::kDescriptorSetTexturesPixel)); } + // Sets at the first incompatible index and above are disturbed by + // binding a pipeline with the new layout and must be rebound. This was + // previously masked by the texture descriptor sets being rewritten and + // rebound on every draw. + if (descriptor_sets_kept < uint32_t(SpirvShaderTranslator::kDescriptorSetCount)) { + current_graphics_descriptor_sets_bound_up_to_date_ &= + (UINT32_C(1) << descriptor_sets_kept) - 1; + } } else { // No or unknown pipeline layout previously bound - all bindings are in an // indeterminate state. @@ -4486,11 +4643,14 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t } // Update system constants before uploading them. - UpdateSystemConstantValues(primitive_polygonal, primitive_processing_result, - shader_32bit_index_dma, 0, viewport_info, used_texture_mask, - vertex_shader->ucode_data_hash(), - pixel_shader ? pixel_shader->ucode_data_hash() : 0, - normalized_depth_control, normalized_color_mask); + { + rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageSystemConstantsUs); + UpdateSystemConstantValues(primitive_polygonal, primitive_processing_result, + shader_32bit_index_dma, 0, viewport_info, used_texture_mask, + vertex_shader->ucode_data_hash(), + pixel_shader ? pixel_shader->ucode_data_hash() : 0, + normalized_depth_control, normalized_color_mask); + } // Update uniform buffers and descriptor sets after binding the pipeline with // the new layout. @@ -4646,6 +4806,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t render_target_cache_->last_update_framebuffer()); } + rex::perf::ScopedCounterTimer pre_draw_stage_timer(rex::perf::CounterId::kDrawStagePreDrawUs); const uint32_t host_draw_vertex_count = primitive_processing_result.host_draw_vertex_count; const uint32_t host_draw_primitive_count = draw_util::EstimatePrimitiveCount( primitive_processing_result.host_primitive_type, host_draw_vertex_count); @@ -4693,7 +4854,9 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t } } bool skate3_ultrawide_submit_draw = true; - if (rex::cvar::Query("skate3_ultrawide_trace_draws")) { + // Frame-cached: the registry Query takes the registry mutex and hashes the + // name, far too expensive for a per-draw debug-flag check. + if (QuerySkate3UltrawideTraceDraws(frame_current_)) { ultrawide_debug::DrawFingerprint fingerprint; fingerprint.bucket = memexport_writes_possible @@ -4753,6 +4916,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t if (!skate3_ultrawide_submit_draw) { return true; } + pre_draw_stage_timer.Stop(); // Draw. uint32_t gpu_timestamp_start = BeginGpuTimestampedDraw(draw_bucket); @@ -5781,7 +5945,9 @@ bool VulkanCommandProcessor::InitializeGpuTimestampResources() { if (!ui::vulkan::util::CreateDedicatedAllocationBuffer( vulkan_device, - sizeof(uint64_t) * kMaxGpuTimestampQueriesPerFrame * kMaxFramesInFlight, + // Two uint64s per query - the timestamp value and the availability + // word. + sizeof(uint64_t) * 2 * kMaxGpuTimestampQueriesPerFrame * kMaxFramesInFlight, VK_BUFFER_USAGE_TRANSFER_DST_BIT, ui::vulkan::util::MemoryPurpose::kReadback, gpu_timestamp_readback_buffer_, gpu_timestamp_readback_memory_, &gpu_timestamp_readback_memory_type_, &gpu_timestamp_readback_memory_size_)) { @@ -5820,19 +5986,32 @@ void VulkanCommandProcessor::ShutdownGpuTimestampResources() { gpu_timestamp_readback_memory_type_ = UINT32_MAX; gpu_timestamp_readback_memory_size_ = 0; gpu_timestamp_resources_available_ = false; + gpu_timestamp_draws_enabled_ = false; for (GpuTimestampFrame& frame : gpu_timestamp_frames_) { frame = {}; } } void VulkanCommandProcessor::BeginGpuTimestampFrame() { + const bool timestamp_diagnostics_enabled = + rex::perf::IsCaptureRecording() || REXCVAR_GET(vulkan_gpu_timestamp_buckets) || + REXCVAR_GET(vulkan_gpu_timestamp_trace_frames) > 0; + gpu_timestamp_draws_enabled_ = timestamp_diagnostics_enabled; if (!gpu_timestamp_resources_available_ || gpu_timestamp_query_pool_ == VK_NULL_HANDLE) { return; } + const bool presenter_stats_enabled = + graphics_system_ && graphics_system_->presenter() && + graphics_system_->presenter()->GuestFrameStatsEnabled(); + if (!timestamp_diagnostics_enabled && !presenter_stats_enabled) { + return; + } + GpuTimestampFrame& frame = gpu_timestamp_frames_[frame_current_ % kMaxFramesInFlight]; frame.frame = frame_current_; frame.submission = 0; + frame.first_submission = GetCurrentSubmission(); frame.query_base = uint32_t((frame_current_ % kMaxFramesInFlight) * kMaxGpuTimestampQueriesPerFrame); frame.query_count = 0; @@ -5846,7 +6025,12 @@ void VulkanCommandProcessor::BeginGpuTimestampFrame() { frame.bucket_start_queries.clear(); frame.bucket_end_queries.clear(); - if (!rex::perf::IsCaptureRecording()) { + // The frame-level start/end pair is measured only while a visible overlay or + // explicit timestamp diagnostic needs it. + const ui::vulkan::VulkanDevice::Properties& device_properties = + GetVulkanDevice()->properties(); + if (!device_properties.timestampComputeAndGraphics || + device_properties.timestampPeriod <= 0.0f) { return; } @@ -5900,10 +6084,15 @@ void VulkanCommandProcessor::ResolveGpuTimestampFrame(VkCommandBuffer command_bu } const ui::vulkan::VulkanDevice::Functions& dfn = GetVulkanDevice()->functions(); + // Copying with the availability word instead of VK_QUERY_RESULT_WAIT_BIT: + // waiting hangs the device forever if any query in the range never gets its + // timestamp written (this happened with bucket timestamps at 3x resolution + // scale), while an unavailable query with the availability flag simply + // yields availability == 0 and the sample is skipped at processing. dfn.vkCmdCopyQueryPoolResults(command_buffer, gpu_timestamp_query_pool_, frame.query_base, frame.query_count, gpu_timestamp_readback_buffer_, - sizeof(uint64_t) * frame.query_base, sizeof(uint64_t), - VK_QUERY_RESULT_64_BIT); + sizeof(uint64_t) * 2 * frame.query_base, sizeof(uint64_t) * 2, + VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT); } void VulkanCommandProcessor::ProcessGpuTimestampResults() { @@ -5937,7 +6126,7 @@ void VulkanCommandProcessor::ProcessGpuTimestampResults() { memory_range.memory = gpu_timestamp_readback_memory_; memory_range.offset = 0; memory_range.size = std::min( - rex::round_up(VkDeviceSize(sizeof(uint64_t) * kMaxGpuTimestampQueriesPerFrame * + rex::round_up(VkDeviceSize(sizeof(uint64_t) * 2 * kMaxGpuTimestampQueriesPerFrame * kMaxFramesInFlight), properties.nonCoherentAtomSize), gpu_timestamp_readback_memory_size_); @@ -5949,71 +6138,133 @@ void VulkanCommandProcessor::ProcessGpuTimestampResults() { if (!frame.pending || frame.submission == 0 || frame.submission > submission_completed_) { continue; } - const uint64_t* timestamps = gpu_timestamp_readback_mapping_ + frame.query_base; + // Two uint64s per query - the timestamp value and the availability word. + const uint64_t* timestamps = gpu_timestamp_readback_mapping_ + size_t(frame.query_base) * 2; auto timestamp_delta_us = [&](uint32_t start_query, uint32_t end_query) -> int64_t { if (start_query >= frame.query_count || end_query >= frame.query_count) { return 0; } - uint64_t start = timestamps[start_query]; - uint64_t end = timestamps[end_query]; + if (!timestamps[size_t(start_query) * 2 + 1] || !timestamps[size_t(end_query) * 2 + 1]) { + // Query results unavailable - skip the sample. + return 0; + } + uint64_t start = timestamps[size_t(start_query) * 2]; + uint64_t end = timestamps[size_t(end_query) * 2]; if (end <= start) { return 0; } return int64_t(double(end - start) * timestamp_period_ns / 1000.0); }; + int64_t frame_span_us = 0; if (frame.frame_start_query != UINT32_MAX && frame.frame_end_query != UINT32_MAX) { int64_t elapsed_us = timestamp_delta_us(frame.frame_start_query, frame.frame_end_query); if (elapsed_us > 0) { + frame_span_us = elapsed_us; rex::perf::IncrementCounter(rex::perf::CounterId::kGpuCommandProcessorFrameUs, elapsed_us); rex::perf::IncrementCounter(rex::perf::CounterId::kGpuTimestampedFrames); + if (ui::Presenter* presenter = graphics_system_->presenter()) { + presenter->SetGuestFrameGpuSpanMicroseconds(uint64_t(elapsed_us)); + static bool gpu_span_logged = false; + if (!gpu_span_logged) { + gpu_span_logged = true; + REXGPU_INFO("VulkanCommandProcessor: GPU frame span reporting active ({} us)", + elapsed_us); + } + } } } + // Attribution audit trace - logs every bucket interval of the frame. + int32_t trace_remaining = REXCVAR_GET(vulkan_gpu_timestamp_trace_frames); + bool trace_this_frame = trace_remaining > 0 && frame_span_us > 0; + uint64_t trace_frame_start_ticks = 0; + if (trace_this_frame) { + REXCVAR_SET(vulkan_gpu_timestamp_trace_frames, trace_remaining - 1); + trace_frame_start_ticks = timestamps[size_t(frame.frame_start_query) * 2]; + REXGPU_INFO("GPU timestamp trace: frame={} queries={}/{} span_us={} intervals={}", + frame.frame, frame.query_count, kMaxGpuTimestampQueriesPerFrame, frame_span_us, + frame.buckets.size()); + } + auto trace_ticks_to_us = [&](uint64_t ticks) -> int64_t { + return int64_t(double(int64_t(ticks - trace_frame_start_ticks)) * timestamp_period_ns / + 1000.0); + }; + + uint64_t bucket_draw_us = 0; + uint64_t bucket_resolve_us = 0; + uint64_t bucket_dump_us = 0; for (uint32_t i = 0; i < frame.buckets.size() && i < frame.bucket_start_queries.size() && i < frame.bucket_end_queries.size(); ++i) { uint32_t start_query = frame.bucket_start_queries[i]; uint32_t end_query = frame.bucket_end_queries[i]; if (end_query == UINT32_MAX) { + if (trace_this_frame) { + REXGPU_INFO(" [{}] {} START-ONLY (no end query - budget exhausted or frame end)", i, + rex::perf::DrawBucketName(frame.buckets[i])); + } continue; } int64_t elapsed_us = timestamp_delta_us(start_query, end_query); + if (trace_this_frame && start_query < frame.query_count && end_query < frame.query_count) { + REXGPU_INFO(" [{}] {} start={} end={} dur={}{}", i, + rex::perf::DrawBucketName(frame.buckets[i]), + trace_ticks_to_us(timestamps[size_t(start_query) * 2]), + trace_ticks_to_us(timestamps[size_t(end_query) * 2]), elapsed_us, + (!timestamps[size_t(start_query) * 2 + 1] || + !timestamps[size_t(end_query) * 2 + 1]) + ? " UNAVAILABLE" + : ""); + } if (elapsed_us <= 0) { continue; } + if (size_t(frame.buckets[i]) < size_t(rex::perf::DrawBucket::kCount)) { + gpu_profile_bucket_us_[size_t(frame.buckets[i])] += uint64_t(elapsed_us); + } rex::perf::CounterId counter_id; switch (frame.buckets[i]) { case rex::perf::DrawBucket::kMainColorDepth: counter_id = rex::perf::CounterId::kGpuMainUs; + bucket_draw_us += uint64_t(elapsed_us); break; case rex::perf::DrawBucket::kDepthOnly: counter_id = rex::perf::CounterId::kGpuDepthUs; + bucket_draw_us += uint64_t(elapsed_us); break; case rex::perf::DrawBucket::kCopyResolve: counter_id = rex::perf::CounterId::kGpuCopyUs; + bucket_resolve_us += uint64_t(elapsed_us); break; case rex::perf::DrawBucket::kMemexport: counter_id = rex::perf::CounterId::kGpuMemexportUs; + bucket_draw_us += uint64_t(elapsed_us); break; case rex::perf::DrawBucket::kNoPixelShader: counter_id = rex::perf::CounterId::kGpuNoPixelShaderUs; + bucket_draw_us += uint64_t(elapsed_us); break; case rex::perf::DrawBucket::kCopyDump: counter_id = rex::perf::CounterId::kGpuCopyDumpUs; + bucket_dump_us += uint64_t(elapsed_us); break; case rex::perf::DrawBucket::kCopyResolveShader: counter_id = rex::perf::CounterId::kGpuCopyResolveUs; + bucket_resolve_us += uint64_t(elapsed_us); break; case rex::perf::DrawBucket::kCopyResolveFast32: counter_id = rex::perf::CounterId::kGpuCopyResolveFast32Us; + bucket_resolve_us += uint64_t(elapsed_us); break; case rex::perf::DrawBucket::kCopyResolveFull32: counter_id = rex::perf::CounterId::kGpuCopyResolveFull32Us; + bucket_resolve_us += uint64_t(elapsed_us); break; case rex::perf::DrawBucket::kResolveDownscale: counter_id = rex::perf::CounterId::kGpuResolveDownscaleUs; + bucket_resolve_us += uint64_t(elapsed_us); break; default: continue; @@ -6021,6 +6272,47 @@ void VulkanCommandProcessor::ProcessGpuTimestampResults() { rex::perf::IncrementCounter(counter_id, elapsed_us); rex::perf::IncrementCounter(rex::perf::CounterId::kGpuTimestampedDraws); } + if (ui::Presenter* presenter = graphics_system_->presenter()) { + // Zeros when bucket timestamps are disabled, which hides the breakdown + // in the FPS overlay. + presenter->SetGuestFrameGpuBreakdownMicroseconds(bucket_draw_us, bucket_resolve_us, + bucket_dump_us); + } + + // Periodic full attribution log - the per-bucket sums were accumulated + // above for every bucket including ones not shown in the FPS overlay. + if (frame_span_us > 0 && REXCVAR_GET(vulkan_gpu_timestamp_buckets)) { + gpu_profile_span_us_ += uint64_t(frame_span_us); + if (frame.submission >= frame.first_submission) { + gpu_profile_submissions_ += frame.submission - frame.first_submission + 1; + } + if (++gpu_profile_frames_ >= kGpuProfileLogFrameInterval) { + uint64_t buckets_sum_us = 0; + std::string breakdown; + for (size_t bucket = 0; bucket < size_t(rex::perf::DrawBucket::kCount); ++bucket) { + uint64_t total_us = gpu_profile_bucket_us_[bucket]; + buckets_sum_us += total_us; + if (!total_us) { + continue; + } + breakdown += fmt::format(" {}={}", rex::perf::DrawBucketName(rex::perf::DrawBucket(bucket)), + total_us / gpu_profile_frames_); + } + uint64_t span_avg_us = gpu_profile_span_us_ / gpu_profile_frames_; + uint64_t sum_avg_us = buckets_sum_us / gpu_profile_frames_; + REXGPU_INFO( + "Vulkan GPU profile (avg us/frame over {} frames): span={} buckets={} untimed={} " + "submissions={} queries={}/{} |{}", + gpu_profile_frames_, span_avg_us, sum_avg_us, + span_avg_us > sum_avg_us ? span_avg_us - sum_avg_us : 0, + gpu_profile_submissions_ / gpu_profile_frames_, frame.query_count, + kMaxGpuTimestampQueriesPerFrame, breakdown); + std::memset(gpu_profile_bucket_us_, 0, sizeof(gpu_profile_bucket_us_)); + gpu_profile_span_us_ = 0; + gpu_profile_submissions_ = 0; + gpu_profile_frames_ = 0; + } + } frame.pending = false; frame.active_bucket_valid = false; @@ -6033,7 +6325,7 @@ void VulkanCommandProcessor::ProcessGpuTimestampResults() { uint32_t VulkanCommandProcessor::BeginGpuTimestampedDraw(rex::perf::DrawBucket bucket) { if (!gpu_timestamp_resources_available_ || gpu_timestamp_query_pool_ == VK_NULL_HANDLE || - !rex::perf::IsCaptureRecording() || !frame_open_) { + !gpu_timestamp_draws_enabled_ || !frame_open_) { return UINT32_MAX; } @@ -6342,9 +6634,21 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(uint64_t await_su { rex::perf::ScopedCounterTimer fence_wait_timer( rex::perf::CounterId::kCpuD3D12FenceWaitUs); + rex::perf::ScopedCounterTimer gpu_thread_wait_timer( + rex::perf::CounterId::kGpuThreadFenceWaitUs, true); + // Also reported through the presenter so the FPS overlay can show the + // blocked time in builds without perf counters - cheap around a + // blocking wait. + const std::chrono::steady_clock::time_point wait_start = std::chrono::steady_clock::now(); wait_result = dfn.vkWaitForFences(device, uint32_t(await_submission - submission_completed_), submissions_in_flight_fences_.data(), VK_TRUE, UINT64_MAX); + if (ui::Presenter* wait_presenter = graphics_system_->presenter()) { + wait_presenter->AddGuestFrameWaitMicroseconds(uint64_t( + std::chrono::duration_cast(std::chrono::steady_clock::now() - + wait_start) + .count())); + } } if (wait_result == VK_SUCCESS) { fences_awaited += await_submission - submission_completed_; @@ -6598,6 +6902,11 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) { shared_memory_and_edram_descriptor_set_; current_graphics_descriptor_set_values_up_to_date_ = UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetSharedMemoryAndEdram; + // Sets cached for reuse belong to the previous frame and may be recycled + // once it completes. + texture_transient_descriptor_set_reuse_cache_.clear(); + current_texture_descriptor_set_hash_valid_[0] = false; + current_texture_descriptor_set_hash_valid_[1] = false; // Reclaim pool pages - no need to do this every small submission since some // may be reused. @@ -6952,6 +7261,11 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) { } void VulkanCommandProcessor::ClearTransientDescriptorPools() { + texture_transient_descriptor_set_reuse_cache_.clear(); + current_texture_descriptor_set_hash_valid_[0] = false; + current_texture_descriptor_set_hash_valid_[1] = false; + draw_binding_state_valid_ = false; + texture_bindings_unchanged_this_draw_ = false; texture_transient_descriptor_sets_free_.clear(); texture_transient_descriptor_sets_used_.clear(); transient_descriptor_allocator_textures_.Reset(); @@ -8052,70 +8366,190 @@ bool VulkanCommandProcessor::UpdateBindings(const VulkanShader* vertex_shader, sampler_count_pixel = 0; texture_count_pixel = 0; } - // TODO(Triang3l): Reuse texture and sampler bindings if not changed. - current_graphics_descriptor_set_values_up_to_date_ &= - ~((UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetTexturesVertex) | - (UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetTexturesPixel)); + // Fill the texture and sampler image infos. These are also the change + // detection key for descriptor set reuse, so they're gathered whenever the + // shaders use any textures or samplers, not only when writing. + bool have_vertex_textures = texture_count_vertex || sampler_count_vertex; + bool have_pixel_textures = texture_count_pixel || sampler_count_pixel; + // Fast path: IssueDraw established that this draw's shaders, samplers and + // texture binding views are identical to the previous draw's in this + // submission, and the recorded texture descriptor sets still hold valid + // values - keep them without regathering and rehashing the image infos. + const bool texture_descriptor_fast_path = + texture_bindings_unchanged_this_draw_ && + REXCVAR_GET(vulkan_reuse_transient_texture_descriptors) && + (!have_vertex_textures || + (current_texture_descriptor_set_hash_valid_[0] && + (current_graphics_descriptor_set_values_up_to_date_ & + (UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetTexturesVertex)))) && + (!have_pixel_textures || + (current_texture_descriptor_set_hash_valid_[1] && + (current_graphics_descriptor_set_values_up_to_date_ & + (UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetTexturesPixel)))); + size_t vertex_texture_image_info_offset = 0; + size_t vertex_sampler_image_info_offset = 0; + size_t pixel_texture_image_info_offset = 0; + size_t pixel_sampler_image_info_offset = 0; + if (!texture_descriptor_fast_path) { + descriptor_write_image_info_.clear(); + descriptor_write_image_info_.reserve( + (have_vertex_textures ? texture_count_vertex + sampler_count_vertex : 0) + + (have_pixel_textures ? texture_count_pixel + sampler_count_pixel : 0)); + vertex_texture_image_info_offset = descriptor_write_image_info_.size(); + if (have_vertex_textures && texture_count_vertex) { + for (const VulkanShader::TextureBinding& texture_binding : textures_vertex) { + VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back(); + descriptor_image_info.imageView = texture_cache_->GetActiveBindingOrNullImageView( + texture_binding.fetch_constant, texture_binding.dimension, + bool(texture_binding.is_signed)); + descriptor_image_info.imageLayout = descriptor_image_info.imageView != VK_NULL_HANDLE + ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + : VK_IMAGE_LAYOUT_UNDEFINED; + } + } + vertex_sampler_image_info_offset = descriptor_write_image_info_.size(); + if (have_vertex_textures && sampler_count_vertex) { + for (const std::pair& sampler_pair : + current_samplers_vertex_) { + VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back(); + descriptor_image_info.sampler = sampler_pair.second; + } + } + pixel_texture_image_info_offset = descriptor_write_image_info_.size(); + if (have_pixel_textures && texture_count_pixel) { + for (const VulkanShader::TextureBinding& texture_binding : *textures_pixel) { + VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back(); + descriptor_image_info.imageView = texture_cache_->GetActiveBindingOrNullImageView( + texture_binding.fetch_constant, texture_binding.dimension, + bool(texture_binding.is_signed)); + descriptor_image_info.imageLayout = descriptor_image_info.imageView != VK_NULL_HANDLE + ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL + : VK_IMAGE_LAYOUT_UNDEFINED; + } + } + pixel_sampler_image_info_offset = descriptor_write_image_info_.size(); + if (have_pixel_textures && sampler_count_pixel) { + for (const std::pair& sampler_pair : + current_samplers_pixel_) { + VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back(); + descriptor_image_info.sampler = sampler_pair.second; + } + } + } + + // Texture/sampler descriptor set reuse (resolving the upstream + // "TODO(Triang3l): Reuse texture and sampler bindings if not changed"): + // hash the would-be contents, then either keep the currently recorded set, + // adopt a set written with identical contents earlier this frame, or fall + // through to allocating and writing a new one. The image info structs are + // value-initialized, so hashing only the meaningful fields is stable. + const bool reuse_texture_descriptor_sets = + REXCVAR_GET(vulkan_reuse_transient_texture_descriptors); + auto hash_texture_bindings = [](bool is_vertex, uint32_t texture_count, uint32_t sampler_count, + const VkDescriptorImageInfo* texture_image_info, + const VkDescriptorImageInfo* sampler_image_info) -> uint64_t { + XXH3_state_t hash_state; + XXH3_64bits_reset(&hash_state); + const uint32_t layout_dimensions[3] = {uint32_t(is_vertex), texture_count, sampler_count}; + XXH3_64bits_update(&hash_state, layout_dimensions, sizeof(layout_dimensions)); + for (uint32_t i = 0; i < texture_count; ++i) { + XXH3_64bits_update(&hash_state, &texture_image_info[i].imageView, sizeof(VkImageView)); + } + for (uint32_t i = 0; i < sampler_count; ++i) { + XXH3_64bits_update(&hash_state, &sampler_image_info[i].sampler, sizeof(VkSampler)); + } + return XXH3_64bits_digest(&hash_state); + }; + // Indexed [0] = vertex, [1] = pixel. + uint64_t texture_descriptor_set_hashes[2] = {}; + bool texture_descriptor_set_contents_current[2] = {}; + VkDescriptorSet texture_descriptor_set_cached[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE}; + if (texture_descriptor_fast_path) { + // The recorded sets hold exactly this draw's contents; the stored hashes + // also remain accurate for later slow-path draws. + texture_descriptor_set_contents_current[0] = have_vertex_textures; + texture_descriptor_set_contents_current[1] = have_pixel_textures; + } else if (reuse_texture_descriptor_sets) { + if (have_vertex_textures) { + texture_descriptor_set_hashes[0] = hash_texture_bindings( + true, texture_count_vertex, sampler_count_vertex, + descriptor_write_image_info_.data() + vertex_texture_image_info_offset, + descriptor_write_image_info_.data() + vertex_sampler_image_info_offset); + if (current_texture_descriptor_set_hash_valid_[0] && + current_texture_descriptor_set_hashes_[0] == texture_descriptor_set_hashes[0]) { + texture_descriptor_set_contents_current[0] = true; + } else { + auto reuse_it = + texture_transient_descriptor_set_reuse_cache_.find(texture_descriptor_set_hashes[0]); + if (reuse_it != texture_transient_descriptor_set_reuse_cache_.end()) { + texture_descriptor_set_cached[0] = reuse_it->second; + } + } + } + if (have_pixel_textures) { + texture_descriptor_set_hashes[1] = hash_texture_bindings( + false, texture_count_pixel, sampler_count_pixel, + descriptor_write_image_info_.data() + pixel_texture_image_info_offset, + descriptor_write_image_info_.data() + pixel_sampler_image_info_offset); + if (current_texture_descriptor_set_hash_valid_[1] && + current_texture_descriptor_set_hashes_[1] == texture_descriptor_set_hashes[1]) { + texture_descriptor_set_contents_current[1] = true; + } else { + auto reuse_it = + texture_transient_descriptor_set_reuse_cache_.find(texture_descriptor_set_hashes[1]); + if (reuse_it != texture_transient_descriptor_set_reuse_cache_.end()) { + texture_descriptor_set_cached[1] = reuse_it->second; + } + } + } + } else { + current_texture_descriptor_set_hash_valid_[0] = false; + current_texture_descriptor_set_hash_valid_[1] = false; + } + + constexpr uint32_t kTexturesVertexBit = UINT32_C(1) + << SpirvShaderTranslator::kDescriptorSetTexturesVertex; + constexpr uint32_t kTexturesPixelBit = UINT32_C(1) + << SpirvShaderTranslator::kDescriptorSetTexturesPixel; + if (texture_descriptor_set_contents_current[0]) { + // The recorded set already holds these contents (also recovers the value + // validity if a previous draw failed after invalidating it). + current_graphics_descriptor_set_values_up_to_date_ |= kTexturesVertexBit; + } else if (texture_descriptor_set_cached[0] != VK_NULL_HANDLE) { + current_graphics_descriptor_sets_[SpirvShaderTranslator::kDescriptorSetTexturesVertex] = + texture_descriptor_set_cached[0]; + current_graphics_descriptor_set_values_up_to_date_ |= kTexturesVertexBit; + // A different set object must be rebound. + current_graphics_descriptor_sets_bound_up_to_date_ &= ~kTexturesVertexBit; + current_texture_descriptor_set_hashes_[0] = texture_descriptor_set_hashes[0]; + current_texture_descriptor_set_hash_valid_[0] = true; + } else { + current_graphics_descriptor_set_values_up_to_date_ &= ~kTexturesVertexBit; + } + if (texture_descriptor_set_contents_current[1]) { + current_graphics_descriptor_set_values_up_to_date_ |= kTexturesPixelBit; + } else if (texture_descriptor_set_cached[1] != VK_NULL_HANDLE) { + current_graphics_descriptor_sets_[SpirvShaderTranslator::kDescriptorSetTexturesPixel] = + texture_descriptor_set_cached[1]; + current_graphics_descriptor_set_values_up_to_date_ |= kTexturesPixelBit; + current_graphics_descriptor_sets_bound_up_to_date_ &= ~kTexturesPixelBit; + current_texture_descriptor_set_hashes_[1] = texture_descriptor_set_hashes[1]; + current_texture_descriptor_set_hash_valid_[1] = true; + } else { + current_graphics_descriptor_set_values_up_to_date_ &= ~kTexturesPixelBit; + } // Make sure new descriptor sets are bound to the command buffer. current_graphics_descriptor_sets_bound_up_to_date_ &= current_graphics_descriptor_set_values_up_to_date_; - // Fill the texture and sampler write image infos. - bool write_vertex_textures = - (texture_count_vertex || sampler_count_vertex) && - !(current_graphics_descriptor_set_values_up_to_date_ & - (UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetTexturesVertex)); + have_vertex_textures && + !(current_graphics_descriptor_set_values_up_to_date_ & kTexturesVertexBit); bool write_pixel_textures = - (texture_count_pixel || sampler_count_pixel) && - !(current_graphics_descriptor_set_values_up_to_date_ & - (UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetTexturesPixel)); - descriptor_write_image_info_.clear(); - descriptor_write_image_info_.reserve( - (write_vertex_textures ? texture_count_vertex + sampler_count_vertex : 0) + - (write_pixel_textures ? texture_count_pixel + sampler_count_pixel : 0)); - size_t vertex_texture_image_info_offset = descriptor_write_image_info_.size(); - if (write_vertex_textures && texture_count_vertex) { - for (const VulkanShader::TextureBinding& texture_binding : textures_vertex) { - VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back(); - descriptor_image_info.imageView = texture_cache_->GetActiveBindingOrNullImageView( - texture_binding.fetch_constant, texture_binding.dimension, - bool(texture_binding.is_signed)); - descriptor_image_info.imageLayout = descriptor_image_info.imageView != VK_NULL_HANDLE - ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL - : VK_IMAGE_LAYOUT_UNDEFINED; - } - } - size_t vertex_sampler_image_info_offset = descriptor_write_image_info_.size(); - if (write_vertex_textures && sampler_count_vertex) { - for (const std::pair& sampler_pair : - current_samplers_vertex_) { - VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back(); - descriptor_image_info.sampler = sampler_pair.second; - } - } - size_t pixel_texture_image_info_offset = descriptor_write_image_info_.size(); - if (write_pixel_textures && texture_count_pixel) { - for (const VulkanShader::TextureBinding& texture_binding : *textures_pixel) { - VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back(); - descriptor_image_info.imageView = texture_cache_->GetActiveBindingOrNullImageView( - texture_binding.fetch_constant, texture_binding.dimension, - bool(texture_binding.is_signed)); - descriptor_image_info.imageLayout = descriptor_image_info.imageView != VK_NULL_HANDLE - ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL - : VK_IMAGE_LAYOUT_UNDEFINED; - } - } - size_t pixel_sampler_image_info_offset = descriptor_write_image_info_.size(); - if (write_pixel_textures && sampler_count_pixel) { - for (const std::pair& sampler_pair : - current_samplers_pixel_) { - VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back(); - descriptor_image_info.sampler = sampler_pair.second; - } - } + have_pixel_textures && + !(current_graphics_descriptor_set_values_up_to_date_ & kTexturesPixelBit); // Write the new descriptor sets. @@ -8129,8 +8563,15 @@ bool VulkanCommandProcessor::UpdateBindings(const VulkanShader* vertex_shader, assert_not_zero(current_graphics_descriptor_set_values_up_to_date_ & (UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetSharedMemoryAndEdram)); // Constant buffers. - if (!(current_graphics_descriptor_set_values_up_to_date_ & - (UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetConstants))) { + if (constants_push_descriptors_used_) { + // The buffer infos are complete at this point; the descriptors themselves + // are pushed into the command buffer right before binding (where the + // bound-up-to-date bit, cleared above when any constant buffer was + // rewritten or by pipeline layout/command buffer changes, is checked). + current_graphics_descriptor_set_values_up_to_date_ |= + UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetConstants; + } else if (!(current_graphics_descriptor_set_values_up_to_date_ & + (UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetConstants))) { VkDescriptorSet constants_descriptor_set; if (!constants_transient_descriptors_free_.empty()) { constants_descriptor_set = constants_transient_descriptors_free_.back(); @@ -8207,6 +8648,26 @@ bool VulkanCommandProcessor::UpdateBindings(const VulkanShader* vertex_shader, // Only make valid if all descriptor sets have been allocated and written // successfully. current_graphics_descriptor_set_values_up_to_date_ |= write_descriptor_set_bits; + // Register the newly written texture sets for reuse by later draws in this + // frame. This must happen only after the vkUpdateDescriptorSets call - a + // set staged for writing but abandoned by an early return would otherwise + // be reused with undefined contents. + if (reuse_texture_descriptor_sets) { + if (write_vertex_textures) { + current_texture_descriptor_set_hashes_[0] = texture_descriptor_set_hashes[0]; + current_texture_descriptor_set_hash_valid_[0] = true; + texture_transient_descriptor_set_reuse_cache_.emplace( + texture_descriptor_set_hashes[0], + current_graphics_descriptor_sets_[SpirvShaderTranslator::kDescriptorSetTexturesVertex]); + } + if (write_pixel_textures) { + current_texture_descriptor_set_hashes_[1] = texture_descriptor_set_hashes[1]; + current_texture_descriptor_set_hash_valid_[1] = true; + texture_transient_descriptor_set_reuse_cache_.emplace( + texture_descriptor_set_hashes[1], + current_graphics_descriptor_sets_[SpirvShaderTranslator::kDescriptorSetTexturesPixel]); + } + } // Bind the new descriptor sets. uint32_t descriptor_sets_needed = (UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetCount) - 1; @@ -8216,6 +8677,21 @@ bool VulkanCommandProcessor::UpdateBindings(const VulkanShader* vertex_shader, if (!texture_count_pixel && !sampler_count_pixel) { descriptor_sets_needed &= ~(UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetTexturesPixel); } + if (constants_push_descriptors_used_) { + // The constants set index uses push descriptors - record the push when + // its contents or the command buffer binding state changed, and exclude + // it from the regular vkCmdBindDescriptorSets ranges. + constexpr uint32_t kConstantsDescriptorBit = + UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetConstants; + if (!(current_graphics_descriptor_sets_bound_up_to_date_ & kConstantsDescriptorBit)) { + deferred_command_buffer_.CmdVkPushUniformBufferDescriptorSet( + VK_PIPELINE_BIND_POINT_GRAPHICS, + current_guest_graphics_pipeline_layout_->GetPipelineLayout(), + SpirvShaderTranslator::kDescriptorSetConstants, + SpirvShaderTranslator::kConstantBufferCount, current_constant_buffer_infos_); + current_graphics_descriptor_sets_bound_up_to_date_ |= kConstantsDescriptorBit; + } + } uint32_t descriptor_sets_remaining = descriptor_sets_needed & ~current_graphics_descriptor_sets_bound_up_to_date_; uint32_t descriptor_set_index; diff --git a/src/graphics/vulkan/deferred_command_buffer.cpp b/src/graphics/vulkan/deferred_command_buffer.cpp index 51215f2..1ad1712 100644 --- a/src/graphics/vulkan/deferred_command_buffer.cpp +++ b/src/graphics/vulkan/deferred_command_buffer.cpp @@ -9,6 +9,7 @@ * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime */ +#include #include #include #include @@ -138,6 +139,16 @@ void DeferredCommandBuffer::Execute(VkCommandBuffer command_buffer) { rex::align(sizeof(ArgsVkClearColorImage), alignof(VkImageSubresourceRange)))); } break; + case Command::kVkClearDepthStencilImage: { + auto& args = *reinterpret_cast(stream); + dfn.vkCmdClearDepthStencilImage( + command_buffer, args.image, args.image_layout, &args.depth_stencil, args.range_count, + reinterpret_cast( + reinterpret_cast(stream) + + rex::align(sizeof(ArgsVkClearDepthStencilImage), + alignof(VkImageSubresourceRange)))); + } break; + case Command::kVkCopyBuffer: { auto& args = *reinterpret_cast(stream); dfn.vkCmdCopyBuffer(command_buffer, args.src_buffer, args.dst_buffer, args.region_count, @@ -266,6 +277,34 @@ void DeferredCommandBuffer::Execute(VkCommandBuffer command_buffer) { reinterpret_cast(stream) + sizeof(ArgsVkPushConstants)); } break; + case Command::kVkPushUniformBufferDescriptorSet: { + auto& args = *reinterpret_cast(stream); + size_t offset_bytes = rex::align(sizeof(ArgsVkPushUniformBufferDescriptorSet), + alignof(VkDescriptorBufferInfo)); + const VkDescriptorBufferInfo* buffer_infos = reinterpret_cast( + reinterpret_cast(stream) + offset_bytes); + constexpr uint32_t kMaxPushedUniformBuffers = 8; + assert_true(args.buffer_count <= kMaxPushedUniformBuffers); + VkWriteDescriptorSet writes[kMaxPushedUniformBuffers]; + uint32_t write_count = std::min(args.buffer_count, kMaxPushedUniformBuffers); + for (uint32_t i = 0; i < write_count; ++i) { + VkWriteDescriptorSet& write = writes[i]; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.pNext = nullptr; + // dstSet is ignored for push descriptors. + write.dstSet = VK_NULL_HANDLE; + write.dstBinding = i; + write.dstArrayElement = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + write.pImageInfo = nullptr; + write.pBufferInfo = &buffer_infos[i]; + write.pTexelBufferView = nullptr; + } + dfn.vkCmdPushDescriptorSetKHR(command_buffer, args.pipeline_bind_point, args.layout, + args.set, write_count, writes); + } break; + case Command::kVkResetQueryPool: { auto& args = *reinterpret_cast(stream); dfn.vkCmdResetQueryPool(command_buffer, args.query_pool, args.first_query, diff --git a/src/graphics/vulkan/pipeline_cache.cpp b/src/graphics/vulkan/pipeline_cache.cpp index c7086cc..557bb2b 100644 --- a/src/graphics/vulkan/pipeline_cache.cpp +++ b/src/graphics/vulkan/pipeline_cache.cpp @@ -3025,12 +3025,12 @@ bool VulkanPipelineCache::EnsurePipelineCreated(const PipelineCreationArguments& // be created from the disk storage. if (creation_arguments.pixel_shader) { - REXGPU_INFO("Creating graphics pipeline state with VS {:016X}, PS {:016X}", - creation_arguments.vertex_shader->shader().ucode_data_hash(), - creation_arguments.pixel_shader->shader().ucode_data_hash()); + REXGPU_DEBUG("Creating graphics pipeline state with VS {:016X}, PS {:016X}", + creation_arguments.vertex_shader->shader().ucode_data_hash(), + creation_arguments.pixel_shader->shader().ucode_data_hash()); } else { - REXGPU_INFO("Creating graphics pipeline state with VS {:016X}", - creation_arguments.vertex_shader->shader().ucode_data_hash()); + REXGPU_DEBUG("Creating graphics pipeline state with VS {:016X}", + creation_arguments.vertex_shader->shader().ucode_data_hash()); } const PipelineDescription& description = creation_arguments.pipeline->first; diff --git a/src/graphics/vulkan/render_target_cache.cpp b/src/graphics/vulkan/render_target_cache.cpp index b8630db..ff3744f 100644 --- a/src/graphics/vulkan/render_target_cache.cpp +++ b/src/graphics/vulkan/render_target_cache.cpp @@ -43,6 +43,19 @@ REXCVAR_DEFINE_STRING(render_target_path_vulkan, "", "GPU/Vulkan", "Vulkan render target implementation path") .lifecycle(rex::cvar::Lifecycle::kInitOnly); +REXCVAR_DEFINE_BOOL(vulkan_direct_resolve_fast32, true, "GPU/Vulkan", + "Resolve single-sampled 32bpp color render targets with a single compute " + "dispatch reading the host render target directly, instead of the render " + "target dump -> EDRAM buffer -> copy round trip (requires " + "direct_host_resolve)") + .lifecycle(rex::cvar::Lifecycle::kHotReload); + +REXCVAR_DEFINE_BOOL(vulkan_deferred_resolve_clears, true, "GPU/Vulkan", + "Defer resolve clears of host render targets and replay them inside the next " + "render pass binding the render target, instead of a dedicated render pass " + "with an attachment clear per resolve") + .lifecycle(rex::cvar::Lifecycle::kHotReload); + // DEFINE_string( // render_target_path_vulkan, "", // "Render target emulation path to use on Vulkan.\n" @@ -990,6 +1003,7 @@ void VulkanRenderTargetCache::Shutdown(bool from_destructor) { // may happen if shutting down the VulkanRenderTargetCache by destroying it, // so ShutdownCommon is called by the RenderTargetCache destructor, when it's // already too late. + deferred_clear_render_targets_.clear(); DestroyAllRenderTargets(true); ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyPipeline, device, @@ -1023,6 +1037,13 @@ void VulkanRenderTargetCache::Shutdown(bool from_destructor) { } } direct_resolve_pipelines_.clear(); + for (size_t i = 0; i < 2; ++i) { + for (size_t j = 0; j < 3; ++j) { + ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyPipeline, device, + direct_resolve_fast32_pipelines_[i][j]); + direct_resolve_fast32_pipelines_attempted_[i][j] = false; + } + } ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyPipelineLayout, device, direct_resolve_pipeline_layout_depth_); ui::vulkan::util::DestroyAndNullHandle(dfn.vkDestroyPipelineLayout, device, @@ -1122,6 +1143,9 @@ void VulkanRenderTargetCache::ClearCache() { } render_passes_.clear(); + // The common ClearCache destroys all render targets. + deferred_clear_render_targets_.clear(); + RenderTargetCache::ClearCache(); } @@ -1407,8 +1431,9 @@ bool VulkanRenderTargetCache::Resolve(const memory::Memory& memory, bool direct_resolved = false; if (GetPath() == Path::kHostRenderTargets) { if (REXCVAR_GET(direct_host_resolve)) { - direct_resolved = - TryResolveCopyDirectly(resolve_info, copy_shader, draw_resolution_scaled); + direct_resolved = TryResolveCopyDirectly(resolve_info, copy_shader, + draw_resolution_scaled, shared_memory, + copy_shader_constants); if (direct_resolved) { ++direct_resolve_success_count_; } else { @@ -1430,6 +1455,17 @@ bool VulkanRenderTargetCache::Resolve(const memory::Memory& memory, } } + if (direct_resolved) { + // The destination has been fully written by the direct resolve + // dispatches. + texture_cache.MarkRangeAsResolved(resolve_info.copy_dest_extent_start, + resolve_info.copy_dest_extent_length); + written_address_out = resolve_info.copy_dest_extent_start; + written_length_out = resolve_info.copy_dest_extent_length; + copied = true; + } else { + // Note: this block is kept at its original indentation to minimize the + // diff against the EDRAM-buffer copy path. uint32_t copy_dest_range_unscaled = resolve_info.copy_dest_extent_start - resolve_info.copy_dest_base + resolve_info.copy_dest_extent_length; @@ -1596,6 +1632,7 @@ bool VulkanRenderTargetCache::Resolve(const memory::Memory& memory, copied = true; } } + } // !direct_resolved } } else { copied = true; @@ -1608,6 +1645,9 @@ bool VulkanRenderTargetCache::Resolve(const memory::Memory& memory, if (clear_depth || clear_color) { switch (GetPath()) { case Path::kHostRenderTargets: { + // Timed as kCopyDump (the "dump" line of the FPS overlay breakdown) to + // separate the clear cost from the copy cost within resolves. + command_processor_.BeginGpuTimestampedRegion(rex::perf::DrawBucket::kCopyDump); Transfer::Rectangle clear_rectangle; RenderTarget* clear_render_targets[2]; // If PrepareHostRenderTargetsResolveClear returns false, may be just an @@ -1619,8 +1659,31 @@ bool VulkanRenderTargetCache::Resolve(const memory::Memory& memory, clear_values[0] = resolve_info.rb_depth_clear; clear_values[1] = resolve_info.rb_color_clear | (uint64_t(resolve_info.rb_color_clear_lo) << 32); - PerformTransfersAndResolveClears(2, clear_render_targets, clear_transfers_, clear_values, - &clear_rectangle); + if (REXCVAR_GET(vulkan_deferred_resolve_clears) && clear_transfers_[0].empty() && + clear_transfers_[1].empty()) { + // Defer the clears to the next render pass binding these render + // targets instead of clearing in a dedicated render pass now - + // render pass churn around resolve clears is a large fixed cost + // per resolve on some hosts. Only possible when no ownership + // transfers are pending at all - transfer draws would need the + // render pass anyway, and transfers into one of the render targets + // may read the other one, which must already be cleared then. + for (uint32_t i = 0; i < 2; ++i) { + if (clear_render_targets[i]) { + DeferResolveClear(*static_cast(clear_render_targets[i]), + clear_rectangle, clear_values[i]); + } + } + // If this resolve recorded no commands (clear-only resolve), the + // current render pass may still be open - end it so the next draw + // re-enters it and replays the deferred clears at the entry. + command_processor_.EndRenderPass(); + } else if (clear_render_targets[0] || clear_render_targets[1]) { + deferred_clear_stat_immediate_ += uint32_t(clear_render_targets[0] != nullptr) + + uint32_t(clear_render_targets[1] != nullptr); + PerformTransfersAndResolveClears(2, clear_render_targets, clear_transfers_, + clear_values, &clear_rectangle); + } } cleared = true; } break; @@ -1677,6 +1740,11 @@ bool VulkanRenderTargetCache::Resolve(const memory::Memory& memory, cleared = true; } + // Time from here until the next instrumented point (the next draw's setup + // work or the draw itself) separately, so the trailing drain of the resolve + // work doesn't pollute the resolve buckets. + command_processor_.BeginGpuTimestampedRegion(rex::perf::DrawBucket::kResolveGap); + return copied && cleared; } @@ -2183,7 +2251,8 @@ RenderTargetCache::RenderTarget* VulkanRenderTargetCache::CreateRenderTarget(Ren image_create_info.samples = VkSampleCountFlagBits(uint32_t(1) << uint32_t(key.msaa_samples)); } image_create_info.tiling = VK_IMAGE_TILING_OPTIMAL; - image_create_info.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + // TRANSFER_DST is for whole-image resolve clears via the transfer engine. + image_create_info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; image_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; image_create_info.queueFamilyIndexCount = 0; image_create_info.pQueueFamilyIndices = nullptr; @@ -4865,6 +4934,23 @@ void VulkanRenderTargetCache::PerformTransfersAndResolveClears( DeferredCommandBuffer& command_buffer = command_processor_.deferred_command_buffer(); bool resolve_clear_needed = render_target_resolve_clear_values && resolve_clear_rectangle; + + // Attribute the GPU time of ownership transfers and resolve clears (and the + // barriers around them) to the transfer profiling bucket. This is called + // with nothing to do for most draws - only open a profiling region when + // there's actual work, to not exhaust the per-frame timestamp query budget. + bool transfer_work_needed = resolve_clear_needed; + if (!transfer_work_needed) { + for (uint32_t i = 0; i < render_target_count; ++i) { + if (render_targets[i] && !render_target_transfers[i].empty()) { + transfer_work_needed = true; + break; + } + } + } + if (transfer_work_needed) { + command_processor_.BeginGpuTimestampedRegion(rex::perf::DrawBucket::kRtTransfer); + } VkClearRect resolve_clear_rect; if (resolve_clear_needed) { // Assuming the rectangle is already clamped by the setup function from the @@ -4881,6 +4967,33 @@ void VulkanRenderTargetCache::PerformTransfersAndResolveClears( resolve_clear_rect.layerCount = 1; } + // Flush deferred resolve clears of all the involved render targets first - + // the destinations will be transferred into and cleared with proper ordering + // here, and the sources must already contain their cleared contents when + // read. + if (!deferred_clear_render_targets_.empty()) { + for (uint32_t i = 0; i < render_target_count; ++i) { + RenderTarget* dest_rt = render_targets[i]; + if (!dest_rt) { + continue; + } + const std::vector& dest_transfers = render_target_transfers[i]; + if (!resolve_clear_needed && dest_transfers.empty()) { + continue; + } + FlushDeferredResolveClears(*static_cast(dest_rt)); + for (const Transfer& transfer : dest_transfers) { + if (transfer.source) { + FlushDeferredResolveClears(*static_cast(transfer.source)); + } + if (transfer.host_depth_source) { + FlushDeferredResolveClears( + *static_cast(transfer.host_depth_source)); + } + } + } + } + // Do host depth storing for the depth destination (assuming there can be only // one depth destination) where depth destination == host depth source. bool host_depth_store_set_up = false; @@ -5594,120 +5707,12 @@ void VulkanRenderTargetCache::PerformTransfersAndResolveClears( if (dest_rt_key.is_depth) { resolve_clear_attachment.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; - uint32_t depth_guest_clear_value = (uint32_t(clear_value) >> 8) & 0xFFFFFF; - switch (dest_rt_key.GetDepthFormat()) { - case xenos::DepthRenderTargetFormat::kD24S8: - resolve_clear_attachment.clearValue.depthStencil.depth = - xenos::UNorm24To32(depth_guest_clear_value); - break; - case xenos::DepthRenderTargetFormat::kD24FS8: - // Taking [0, 2) -> [0, 1) remapping into account. - resolve_clear_attachment.clearValue.depthStencil.depth = - xenos::Float20e4To32(depth_guest_clear_value) * 0.5f; - break; - } - resolve_clear_attachment.clearValue.depthStencil.stencil = uint32_t(clear_value) & 0xFF; + resolve_clear_attachment.clearValue.depthStencil = + GetResolveClearDepthStencilValue(dest_rt_key, clear_value); } else { resolve_clear_attachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - bool dest_color_transfer_is_uint = false; - GetColorOwnershipTransferVulkanFormat( - dest_rt_key.GetColorFormat(), dest_rt_key.msaa_samples, &dest_color_transfer_is_uint); - switch (dest_rt_key.GetColorFormat()) { - case xenos::ColorRenderTargetFormat::k_8_8_8_8: - case xenos::ColorRenderTargetFormat::k_8_8_8_8_GAMMA: { - for (uint32_t j = 0; j < 4; ++j) { - resolve_clear_attachment.clearValue.color.float32[j] = - ((clear_value >> (j * 8)) & 0xFF) * (1.0f / 0xFF); - } - if (dest_rt_key.GetColorFormat() == xenos::ColorRenderTargetFormat::k_8_8_8_8_GAMMA && - gamma_render_target_as_unorm16_) { - // 8_8_8_8_GAMMA is represented by linear stored in - // R16G16B16A16_UNORM. - for (uint32_t j = 0; j < 3; ++j) { - resolve_clear_attachment.clearValue.color.float32[j] = - xenos::PWLGammaToLinear(resolve_clear_attachment.clearValue.color.float32[j]); - } - } - } break; - case xenos::ColorRenderTargetFormat::k_2_10_10_10: - case xenos::ColorRenderTargetFormat::k_2_10_10_10_AS_10_10_10_10: { - for (uint32_t j = 0; j < 3; ++j) { - resolve_clear_attachment.clearValue.color.float32[j] = - ((clear_value >> (j * 10)) & 0x3FF) * (1.0f / 0x3FF); - } - resolve_clear_attachment.clearValue.color.float32[3] = - ((clear_value >> 30) & 0x3) * (1.0f / 0x3); - } break; - case xenos::ColorRenderTargetFormat::k_2_10_10_10_FLOAT: - case xenos::ColorRenderTargetFormat::k_2_10_10_10_FLOAT_AS_16_16_16_16: { - for (uint32_t j = 0; j < 3; ++j) { - resolve_clear_attachment.clearValue.color.float32[j] = - xenos::Float7e3To32((clear_value >> (j * 10)) & 0x3FF); - } - resolve_clear_attachment.clearValue.color.float32[3] = - ((clear_value >> 30) & 0x3) * (1.0f / 0x3); - } break; - case xenos::ColorRenderTargetFormat::k_16_16: - case xenos::ColorRenderTargetFormat::k_16_16_FLOAT: { - // Using uint for transfers and clears of both. Disregarding the - // current -32...32 vs. -1...1 settings for consistency with color - // clear via depth aliasing. - for (uint32_t j = 0; j < 2; ++j) { - uint16_t component = uint16_t((clear_value >> (j * 16)) & 0xFFFF); - if (dest_color_transfer_is_uint) { - resolve_clear_attachment.clearValue.color.uint32[j] = component; - } else if (IsColor16FormatFloatLike(dest_rt_key.GetColorFormat())) { - resolve_clear_attachment.clearValue.color.float32[j] = - rex::xenos_half_to_float(component); - } else { - resolve_clear_attachment.clearValue.color.float32[j] = - std::max(float(int16_t(component)) * (1.0f / 32767.0f), -1.0f); - } - } - } break; - case xenos::ColorRenderTargetFormat::k_16_16_16_16: - case xenos::ColorRenderTargetFormat::k_16_16_16_16_FLOAT: { - // Using uint for transfers and clears of both. Disregarding the - // current -32...32 vs. -1...1 settings for consistency with color - // clear via depth aliasing. - for (uint32_t j = 0; j < 4; ++j) { - uint16_t component = uint16_t((clear_value >> (j * 16)) & 0xFFFF); - if (dest_color_transfer_is_uint) { - resolve_clear_attachment.clearValue.color.uint32[j] = component; - } else if (IsColor16FormatFloatLike(dest_rt_key.GetColorFormat())) { - resolve_clear_attachment.clearValue.color.float32[j] = - rex::xenos_half_to_float(component); - } else { - resolve_clear_attachment.clearValue.color.float32[j] = - std::max(float(int16_t(component)) * (1.0f / 32767.0f), -1.0f); - } - } - } break; - case xenos::ColorRenderTargetFormat::k_32_FLOAT: { - uint32_t component = uint32_t(clear_value); - if (dest_color_transfer_is_uint) { - // Using uint for proper denormal and NaN handling. - resolve_clear_attachment.clearValue.color.uint32[0] = component; - } else { - std::memcpy(&resolve_clear_attachment.clearValue.color.float32[0], &component, - sizeof(component)); - } - } break; - case xenos::ColorRenderTargetFormat::k_32_32_FLOAT: { - uint32_t component_0 = uint32_t(clear_value); - uint32_t component_1 = uint32_t(clear_value >> 32); - if (dest_color_transfer_is_uint) { - // Using uint for proper denormal and NaN handling. - resolve_clear_attachment.clearValue.color.uint32[0] = component_0; - resolve_clear_attachment.clearValue.color.uint32[1] = component_1; - } else { - std::memcpy(&resolve_clear_attachment.clearValue.color.float32[0], &component_0, - sizeof(component_0)); - std::memcpy(&resolve_clear_attachment.clearValue.color.float32[1], &component_1, - sizeof(component_1)); - } - } break; - } + resolve_clear_attachment.clearValue.color = + GetResolveClearColorValue(dest_rt_key, clear_value, true); } command_buffer.CmdVkClearAttachments(1, &resolve_clear_attachment, 1, &resolve_clear_rect); } @@ -6278,14 +6283,319 @@ VkPipeline VulkanRenderTargetCache::GetDirectResolvePipeline(DirectResolvePipeli return pipeline; } -bool VulkanRenderTargetCache::TryResolveCopyDirectly(const draw_util::ResolveInfo& resolve_info, - draw_util::ResolveCopyShaderIndex copy_shader, - bool draw_resolution_scaled) { +namespace { + +// Replicates the fast 32bpp resolve copy (raw sample copy plus destination +// endian swap), but reads the host render target image directly instead of +// the EDRAM buffer that DumpRenderTargets would have to fill first. One +// dispatch covers one ResolveCopyDumpRectangle::Dispatch tile block; threads +// are EDRAM samples (which equal pixels - single-sampled sources only). The +// packed bitfield layouts and the tiled offset function must match +// draw_util::ResolveEdramInfo / ResolveCoordinateInfo / +// ResolveCopyDestCoordinateInfo / reg::RB_COPY_DEST_INFO and +// texture_util::GetTiledOffset2D. +const char kDirectResolveFast32ShaderSource[] = R"(#version 450 +#extension GL_EXT_samplerless_texture_functions : require +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; +layout(push_constant) uniform XePushConstants { + uint edram_info; + uint coordinate_info; + uint dest_info; + uint dest_coordinate_info; + uint dest_base; + uint source_base_tiles; + uint source_pitch_tiles; + uint dispatch_first_tile; + uint height_pixels; + uint source_format_case; +} xe; +layout(set = 0, binding = 0) writeonly buffer XeResolveDest { uint xe_resolve_dest[]; }; +#if XE_SOURCE_MSAA_SAMPLES > 1 +layout(set = 1, binding = 0) uniform texture2DMS xe_resolve_source; +#else +layout(set = 1, binding = 0) uniform texture2D xe_resolve_source; +#endif + +int XeTiledOffset32bpp(int x, int y, int pitch) { + int macro_part = ((x >> 5) + (y >> 5) * (pitch >> 5)) << 9; + int micro_part = ((x & 7) + ((y & 0xE) << 2)) << 2; + int offset_part = macro_part + ((micro_part & ~0xF) << 1) + (micro_part & 0xF) + ((y & 1) << 4); + return ((offset_part & ~0x1FF) << 3) + ((y & 16) << 7) + ((offset_part & 0x1C0) << 2) + + (((((y & 8) >> 2) + (x >> 3)) & 3) << 6) + (offset_part & 0x3F); +} + +void main() { + uint sx = gl_GlobalInvocationID.x; + uint sy = gl_GlobalInvocationID.y; + uint region_pitch_tiles = xe.edram_info & 1023u; + uint region_base_tiles = (xe.edram_info >> 13u) & 2047u; + uint global_tile = xe.dispatch_first_tile + (sy >> 4u) * region_pitch_tiles + sx / 80u; + uint intratile_x = sx % 80u; + uint intratile_y = sy & 15u; + uint region_tile = global_tile - region_base_tiles; + uvec2 region_pixel = uvec2((region_tile % region_pitch_tiles) * 80u + intratile_x, + (region_tile / region_pitch_tiles) * 16u + intratile_y); + ivec2 dest_pixel = + ivec2(region_pixel) - ivec2(int((xe.coordinate_info & 15u) << 3u), + int(((xe.coordinate_info >> 4u) & 1u) << 3u)); + uint width_pixels = ((xe.coordinate_info >> 5u) & 2047u) << 3u; + if (dest_pixel.x < 0 || dest_pixel.y < 0 || uint(dest_pixel.x) >= width_pixels || + uint(dest_pixel.y) >= xe.height_pixels) { + return; + } + uint rt_tile = global_tile - xe.source_base_tiles; + ivec2 rt_pixel = ivec2(int((rt_tile % xe.source_pitch_tiles) * 80u + intratile_x), + int((rt_tile / xe.source_pitch_tiles) * 16u + intratile_y)); +#if XE_SOURCE_MSAA_SAMPLES == 4 + // 4x MSAA owners: samples interleave 2x2 per pixel, host sample index is + // (x & 1) | ((y & 1) << 1). + vec4 source_color = + texelFetch(xe_resolve_source, ivec2(rt_pixel.x >> 1, rt_pixel.y >> 1), + int((uint(rt_pixel.x) & 1u) | ((uint(rt_pixel.y) & 1u) << 1u))); +#elif XE_SOURCE_MSAA_SAMPLES == 2 + // EDRAM sample rows of a native 2x MSAA owner interleave the two samples + // vertically; guest vertical sample 0 is host sample 1 and vice versa. + vec4 source_color = texelFetch(xe_resolve_source, ivec2(rt_pixel.x, rt_pixel.y >> 1), + int((uint(rt_pixel.y) & 1u) ^ 1u)); +#else + vec4 source_color = texelFetch(xe_resolve_source, rt_pixel, 0); +#endif + uint texel; + if (xe.source_format_case == 1u) { + texel = uint(round(clamp(source_color.r, 0.0, 1.0) * 1023.0)) | + (uint(round(clamp(source_color.g, 0.0, 1.0) * 1023.0)) << 10u) | + (uint(round(clamp(source_color.b, 0.0, 1.0) * 1023.0)) << 20u) | + (uint(round(clamp(source_color.a, 0.0, 1.0) * 3.0)) << 30u); + } else { + texel = packUnorm4x8(source_color); + } + if (((xe.dest_info >> 24u) & 1u) != 0u) { + // copy_dest_swap: red/blue swap within the guest dword. + if (xe.source_format_case == 1u) { + texel = (texel & 0xC00FFC00u) | ((texel & 0x3FFu) << 20u) | ((texel >> 20u) & 0x3FFu); + } else { + texel = (texel & 0xFF00FF00u) | ((texel & 0xFFu) << 16u) | ((texel >> 16u) & 0xFFu); + } + } + uint dest_endian = xe.dest_info & 7u; + if (dest_endian == 1u) { + texel = ((texel & 0x00FF00FFu) << 8u) | ((texel & 0xFF00FF00u) >> 8u); + } else if (dest_endian == 2u) { + texel = ((texel & 0x00FF00FFu) << 8u) | ((texel & 0xFF00FF00u) >> 8u); + texel = (texel << 16u) | (texel >> 16u); + } else if (dest_endian == 3u) { + texel = (texel << 16u) | (texel >> 16u); + } + int dest_pitch = int((xe.dest_coordinate_info & 1023u) << 5u); + ivec2 dest_tiled_pixel = + dest_pixel + ivec2(int(((xe.dest_coordinate_info >> 20u) & 15u) << 3u), + int(((xe.dest_coordinate_info >> 24u) & 15u) << 3u)); + uint dest_address = xe.dest_base + uint(XeTiledOffset32bpp(dest_tiled_pixel.x, + dest_tiled_pixel.y, dest_pitch)); + xe_resolve_dest[dest_address >> 2u] = texel; +} +)"; + +// Depth variant of the direct fast 32bpp resolve: packs the guest D24S8 dword +// from the host depth and stencil views (only valid when the host depth +// format is a native 24-bit unorm, so the bits match the guest exactly). +const char kDirectResolveFast32DepthShaderSource[] = R"(#version 450 +#extension GL_EXT_samplerless_texture_functions : require +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; +layout(push_constant) uniform XePushConstants { + uint edram_info; + uint coordinate_info; + uint dest_info; + uint dest_coordinate_info; + uint dest_base; + uint source_base_tiles; + uint source_pitch_tiles; + uint dispatch_first_tile; + uint height_pixels; + uint source_format_case; +} xe; +layout(set = 0, binding = 0) writeonly buffer XeResolveDest { uint xe_resolve_dest[]; }; +#if XE_SOURCE_MSAA_SAMPLES > 1 +layout(set = 1, binding = 0) uniform texture2DMS xe_resolve_source; +layout(set = 1, binding = 1) uniform utexture2DMS xe_resolve_source_stencil; +#else +layout(set = 1, binding = 0) uniform texture2D xe_resolve_source; +layout(set = 1, binding = 1) uniform utexture2D xe_resolve_source_stencil; +#endif + +int XeTiledOffset32bpp(int x, int y, int pitch) { + int macro_part = ((x >> 5) + (y >> 5) * (pitch >> 5)) << 9; + int micro_part = ((x & 7) + ((y & 0xE) << 2)) << 2; + int offset_part = macro_part + ((micro_part & ~0xF) << 1) + (micro_part & 0xF) + ((y & 1) << 4); + return ((offset_part & ~0x1FF) << 3) + ((y & 16) << 7) + ((offset_part & 0x1C0) << 2) + + (((((y & 8) >> 2) + (x >> 3)) & 3) << 6) + (offset_part & 0x3F); +} + +void main() { + uint sx = gl_GlobalInvocationID.x; + uint sy = gl_GlobalInvocationID.y; + uint region_pitch_tiles = xe.edram_info & 1023u; + uint region_base_tiles = (xe.edram_info >> 13u) & 2047u; + uint global_tile = xe.dispatch_first_tile + (sy >> 4u) * region_pitch_tiles + sx / 80u; + uint intratile_x = sx % 80u; + uint intratile_y = sy & 15u; + uint region_tile = global_tile - region_base_tiles; + uvec2 region_pixel = uvec2((region_tile % region_pitch_tiles) * 80u + intratile_x, + (region_tile / region_pitch_tiles) * 16u + intratile_y); + ivec2 dest_pixel = + ivec2(region_pixel) - ivec2(int((xe.coordinate_info & 15u) << 3u), + int(((xe.coordinate_info >> 4u) & 1u) << 3u)); + uint width_pixels = ((xe.coordinate_info >> 5u) & 2047u) << 3u; + if (dest_pixel.x < 0 || dest_pixel.y < 0 || uint(dest_pixel.x) >= width_pixels || + uint(dest_pixel.y) >= xe.height_pixels) { + return; + } + uint rt_tile = global_tile - xe.source_base_tiles; + ivec2 rt_pixel = ivec2(int((rt_tile % xe.source_pitch_tiles) * 80u + intratile_x), + int((rt_tile / xe.source_pitch_tiles) * 16u + intratile_y)); +#if XE_SOURCE_MSAA_SAMPLES == 4 + ivec2 rt_ms_pixel = ivec2(rt_pixel.x >> 1, rt_pixel.y >> 1); + int rt_ms_sample = int((uint(rt_pixel.x) & 1u) | ((uint(rt_pixel.y) & 1u) << 1u)); + float depth = texelFetch(xe_resolve_source, rt_ms_pixel, rt_ms_sample).r; + uint stencil_value = texelFetch(xe_resolve_source_stencil, rt_ms_pixel, rt_ms_sample).r & 0xFFu; +#elif XE_SOURCE_MSAA_SAMPLES == 2 + ivec2 rt_ms_pixel = ivec2(rt_pixel.x, rt_pixel.y >> 1); + int rt_ms_sample = int((uint(rt_pixel.y) & 1u) ^ 1u); + float depth = texelFetch(xe_resolve_source, rt_ms_pixel, rt_ms_sample).r; + uint stencil_value = texelFetch(xe_resolve_source_stencil, rt_ms_pixel, rt_ms_sample).r & 0xFFu; +#else + float depth = texelFetch(xe_resolve_source, rt_pixel, 0).r; + uint stencil_value = texelFetch(xe_resolve_source_stencil, rt_pixel, 0).r & 0xFFu; +#endif + uint texel = (uint(round(clamp(depth, 0.0, 1.0) * 16777215.0)) << 8u) | stencil_value; + uint dest_endian = xe.dest_info & 7u; + if (dest_endian == 1u) { + texel = ((texel & 0x00FF00FFu) << 8u) | ((texel & 0xFF00FF00u) >> 8u); + } else if (dest_endian == 2u) { + texel = ((texel & 0x00FF00FFu) << 8u) | ((texel & 0xFF00FF00u) >> 8u); + texel = (texel << 16u) | (texel >> 16u); + } else if (dest_endian == 3u) { + texel = (texel << 16u) | (texel >> 16u); + } + int dest_pitch = int((xe.dest_coordinate_info & 1023u) << 5u); + ivec2 dest_tiled_pixel = + dest_pixel + ivec2(int(((xe.dest_coordinate_info >> 20u) & 15u) << 3u), + int(((xe.dest_coordinate_info >> 24u) & 15u) << 3u)); + uint dest_address = xe.dest_base + uint(XeTiledOffset32bpp(dest_tiled_pixel.x, + dest_tiled_pixel.y, dest_pitch)); + xe_resolve_dest[dest_address >> 2u] = texel; +} +)"; + +} // namespace + +VkPipeline VulkanRenderTargetCache::GetDirectResolveFast32Pipeline( + bool is_depth, xenos::MsaaSamples source_msaa_samples) { + uint32_t msaa_index = uint32_t(source_msaa_samples); + if (msaa_index > 2) { + return VK_NULL_HANDLE; + } + VkPipeline& pipeline = direct_resolve_fast32_pipelines_[is_depth ? 1 : 0][msaa_index]; + bool& attempted = direct_resolve_fast32_pipelines_attempted_[is_depth ? 1 : 0][msaa_index]; + if (attempted) { + return pipeline; + } + attempted = true; + VkPipelineLayout pipeline_layout = + is_depth ? direct_resolve_pipeline_layout_depth_ : direct_resolve_pipeline_layout_color_; + if (pipeline_layout == VK_NULL_HANDLE) { + return VK_NULL_HANDLE; + } + std::string source = + is_depth ? kDirectResolveFast32DepthShaderSource : kDirectResolveFast32ShaderSource; + // Inject the sample count define after the #version line. + size_t version_line_end = source.find('\n'); + if (version_line_end == std::string::npos) { + return VK_NULL_HANDLE; + } + static const char* const kSampleCountDefines[3] = { + "#define XE_SOURCE_MSAA_SAMPLES 1\n", + "#define XE_SOURCE_MSAA_SAMPLES 2\n", + "#define XE_SOURCE_MSAA_SAMPLES 4\n", + }; + source.insert(version_line_end + 1, kSampleCountDefines[msaa_index]); + std::vector spirv; + std::string compile_error; + if (!command_processor_.CompileGlslToSpirv(VK_SHADER_STAGE_COMPUTE_BIT, source, spirv, + compile_error)) { + REXGPU_WARN( + "VulkanRenderTargetCache: Failed to compile the direct fast 32bpp resolve shader " + "(depth={}, msaa={}): {}", + is_depth, msaa_index, compile_error); + return VK_NULL_HANDLE; + } + pipeline = ui::vulkan::util::CreateComputePipeline(command_processor_.GetVulkanDevice(), + pipeline_layout, spirv.data(), + sizeof(uint32_t) * spirv.size()); + if (pipeline == VK_NULL_HANDLE) { + REXGPU_WARN( + "VulkanRenderTargetCache: Failed to create the direct fast 32bpp resolve pipeline " + "(depth={}, msaa={})", + is_depth, msaa_index); + } else { + REXGPU_INFO( + "VulkanRenderTargetCache: Direct fast 32bpp resolve pipeline created (depth={}, msaa={})", + is_depth, msaa_index); + } + return pipeline; +} + +bool VulkanRenderTargetCache::TryResolveCopyDirectly( + const draw_util::ResolveInfo& resolve_info, draw_util::ResolveCopyShaderIndex copy_shader, + bool draw_resolution_scaled, VulkanSharedMemory& shared_memory, + const draw_util::ResolveCopyShaderConstants& copy_shader_constants) { ++direct_resolve_attempt_count_; - (void)copy_shader; - (void)draw_resolution_scaled; + // Diagnostics for the first fallbacks to tell which gate rejects the + // resolves of a particular game. + static int direct_resolve_fallback_logs_remaining = 16; + auto log_fallback = [&](const char* reason) { + if (direct_resolve_fallback_logs_remaining <= 0) { + return; + } + --direct_resolve_fallback_logs_remaining; + REXGPU_INFO( + "VulkanRenderTargetCache: Direct resolve fallback ({}): copy_shader={}, scaled={}, " + "depth={}, dest_array={}, source_msaa={}, source_format={}, dest_format={}, " + "dest_number={}, exp_bias={}, sample_select={}, dest={:08X}+{:X}", + reason, uint32_t(copy_shader), draw_resolution_scaled, resolve_info.IsCopyingDepth(), + uint32_t(resolve_info.copy_dest_info.copy_dest_array), + uint32_t(resolve_info.color_edram_info.msaa_samples), + resolve_info.IsCopyingDepth() ? uint32_t(resolve_info.depth_edram_info.format) + : uint32_t(resolve_info.color_edram_info.format), + uint32_t(resolve_info.copy_dest_info.copy_dest_format), + uint32_t(resolve_info.copy_dest_info.copy_dest_number), + int32_t(resolve_info.copy_dest_info.copy_dest_exp_bias), + uint32_t(resolve_info.copy_dest_coordinate_info.copy_sample_select), + resolve_info.copy_dest_extent_start, resolve_info.copy_dest_extent_length); + }; if (direct_resolve_pipeline_layout_color_ == VK_NULL_HANDLE || - direct_resolve_pipeline_layout_depth_ == VK_NULL_HANDLE) { + !REXCVAR_GET(vulkan_direct_resolve_fast32)) { + log_fallback(direct_resolve_pipeline_layout_color_ == VK_NULL_HANDLE ? "no-pipeline-layout" + : "cvar-disabled"); + return false; + } + // Narrow prototype: raw (fast) copies of single-sampled 32bpp color or + // unorm24 depth render targets to a non-array destination at 1x resolution + // scale. Everything else falls back to the dump -> EDRAM buffer -> copy + // path. + const bool is_depth_copy = resolve_info.IsCopyingDepth(); + const draw_util::ResolveEdramInfo& source_edram_info = + is_depth_copy ? resolve_info.depth_edram_info : resolve_info.color_edram_info; + if (copy_shader != draw_util::ResolveCopyShaderIndex::kFast32bpp1x2xMSAA || + draw_resolution_scaled || resolve_info.copy_dest_info.copy_dest_array != 0 || + source_edram_info.msaa_samples != xenos::MsaaSamples::k1X) { + log_fallback("narrow-case-gates"); + return false; + } + if (is_depth_copy && !depth_unorm24_vulkan_format_supported()) { + // With a float32 host depth format the bits don't match the guest unorm24 + // encoding directly. + log_fallback("depth-host-encoding"); return false; } @@ -6294,36 +6604,548 @@ bool VulkanRenderTargetCache::TryResolveCopyDirectly(const draw_util::ResolveInf uint32_t dump_rows; uint32_t dump_pitch; resolve_info.GetCopyEdramTileSpan(dump_base, dump_row_length_used, dump_rows, dump_pitch); - GetResolveCopyDispatchesToDump(dump_base, dump_row_length_used, dump_rows, dump_pitch, - dump_rectangles_, direct_resolve_dispatches_); - if (direct_resolve_dispatches_.empty()) { + if (!dump_rows || !dump_pitch || + uint64_t(dump_base) + uint64_t(dump_rows - 1) * dump_pitch + dump_row_length_used > + xenos::kEdramTileCount) { + // EDRAM addressing wrapping isn't handled by the direct path. + log_fallback("edram-wrap"); return false; } + GetResolveCopyRectanglesToDump(dump_base, dump_row_length_used, dump_rows, dump_pitch, + dump_rectangles_); + if (dump_rectangles_.empty()) { + log_fallback("no-rectangles"); + return false; + } + + // The direct resolve reads the current render target contents - deferred + // resolve clears overlapping the read range must land first. + FlushDeferredResolveClearsBeforeEdramRead(dump_base, dump_rows * dump_pitch); for (const ResolveCopyDumpRectangle& rectangle : dump_rectangles_) { const auto* render_target = static_cast(rectangle.render_target); if (render_target == nullptr) { + log_fallback("null-render-target"); return false; } - DumpPipelineKey dump_pipeline_key; - dump_pipeline_key.msaa_samples = render_target->key().msaa_samples; - dump_pipeline_key.resource_format = render_target->key().resource_format; - dump_pipeline_key.is_depth = render_target->key().is_depth; - if (GetDumpPipeline(dump_pipeline_key) == VK_NULL_HANDLE) { + RenderTargetKey rt_key = render_target->key(); + if (rt_key.is_depth != (is_depth_copy ? 1 : 0) || rt_key.Is64bpp()) { + log_fallback("rect-kind-or-64bpp"); return false; } - DirectResolvePipelineKey direct_pipeline_key; - direct_pipeline_key.dump_pipeline_key = dump_pipeline_key; - direct_pipeline_key.copy_shader = copy_shader; - direct_pipeline_key.draw_resolution_scaled = draw_resolution_scaled; - if (GetDirectResolvePipeline(direct_pipeline_key) == VK_NULL_HANDLE) { + // Single-sampled and 4x MSAA owners are read directly; native 2x MSAA + // through the two-sample shader variant. 2x-emulated-as-4x isn't handled + // (different sample placement). + if (rt_key.msaa_samples == xenos::MsaaSamples::k2X && !msaa_2x_attachments_supported_) { + log_fallback("rect-msaa"); + return false; + } + if (is_depth_copy) { + if (rt_key.GetDepthFormat() != xenos::DepthRenderTargetFormat::kD24S8) { + log_fallback("rect-depth-format"); + return false; + } + } else { + switch (rt_key.GetColorFormat()) { + case xenos::ColorRenderTargetFormat::k_8_8_8_8: + case xenos::ColorRenderTargetFormat::k_2_10_10_10: + case xenos::ColorRenderTargetFormat::k_2_10_10_10_AS_10_10_10_10: + break; + case xenos::ColorRenderTargetFormat::k_8_8_8_8_GAMMA: + // With an sRGB host view, sampling would linearize the value. + if (!gamma_render_target_as_unorm16_) { + log_fallback("gamma-srgb-view"); + return false; + } + break; + default: + log_fallback("rect-format"); + return false; + } + } + } + + VkPipelineLayout pipeline_layout = is_depth_copy ? direct_resolve_pipeline_layout_depth_ + : direct_resolve_pipeline_layout_color_; + if (pipeline_layout == VK_NULL_HANDLE) { + log_fallback("pipeline-unavailable"); + return false; + } + // Make sure the pipeline variants needed by all the rectangles exist before + // modifying any state. + for (const ResolveCopyDumpRectangle& rectangle : dump_rectangles_) { + xenos::MsaaSamples rect_msaa_samples = + static_cast(rectangle.render_target)->key().msaa_samples; + if (GetDirectResolveFast32Pipeline(is_depth_copy, rect_msaa_samples) == VK_NULL_HANDLE) { + log_fallback("pipeline-unavailable"); return false; } } - // Dedicated direct resolve dispatches are staged behind the same preflight; - // keep using the existing dump path until source-image direct shaders land. - return DumpRenderTargets(dump_base, dump_row_length_used, dump_rows, dump_pitch); + // Prepare the destination range in the shared memory (1x resolution scale + // only in this path). + if (!shared_memory.RequestRange(resolve_info.copy_dest_extent_start, + resolve_info.copy_dest_extent_length)) { + return false; + } + uint32_t copy_dest_range_unscaled = resolve_info.copy_dest_extent_start - + resolve_info.copy_dest_base + + resolve_info.copy_dest_extent_length; + VkDescriptorSet descriptor_set_dest = command_processor_.AllocateSingleTransientDescriptor( + VulkanCommandProcessor::SingleTransientDescriptorLayout::kStorageBufferCompute); + if (descriptor_set_dest == VK_NULL_HANDLE) { + return false; + } + const ui::vulkan::VulkanDevice* const vulkan_device = command_processor_.GetVulkanDevice(); + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); + VkDescriptorBufferInfo dest_buffer_info; + dest_buffer_info.buffer = shared_memory.buffer(); + dest_buffer_info.offset = resolve_info.copy_dest_base; + dest_buffer_info.range = copy_dest_range_unscaled; + VkWriteDescriptorSet dest_descriptor_write; + dest_descriptor_write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + dest_descriptor_write.pNext = nullptr; + dest_descriptor_write.dstSet = descriptor_set_dest; + dest_descriptor_write.dstBinding = 0; + dest_descriptor_write.dstArrayElement = 0; + dest_descriptor_write.descriptorCount = 1; + dest_descriptor_write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + dest_descriptor_write.pImageInfo = nullptr; + dest_descriptor_write.pBufferInfo = &dest_buffer_info; + dest_descriptor_write.pTexelBufferView = nullptr; + dfn.vkUpdateDescriptorSets(vulkan_device->device(), 1, &dest_descriptor_write, 0, nullptr); + + // Transition the source render targets for compute shader reads (like + // DumpRenderTargets does) and the destination for compute shader writes. + for (const ResolveCopyDumpRectangle& rectangle : dump_rectangles_) { + auto& vulkan_rt = *static_cast(rectangle.render_target); + command_processor_.PushImageMemoryBarrier( + vulkan_rt.image(), + ui::vulkan::util::InitializeSubresourceRange( + is_depth_copy ? (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT) + : VK_IMAGE_ASPECT_COLOR_BIT), + vulkan_rt.current_stage_mask(), VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + vulkan_rt.current_access_mask(), VK_ACCESS_SHADER_READ_BIT, vulkan_rt.current_layout(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + vulkan_rt.SetUsage(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + } + shared_memory.Use(VulkanSharedMemory::Usage::kComputeWrite, + std::pair(resolve_info.copy_dest_extent_start, + resolve_info.copy_dest_extent_length)); + + command_processor_.BeginGpuTimestampedRegion(rex::perf::DrawBucket::kCopyResolveFast32); + DeferredCommandBuffer& command_buffer = command_processor_.deferred_command_buffer(); + bool dest_descriptor_set_bound = false; + command_processor_.SubmitBarriers(true); + + DirectResolvePushConstants push_constants = {}; + push_constants.resolve = copy_shader_constants; + assert_true(copy_shader_constants.dest_base >= uint32_t(dest_buffer_info.offset)); + push_constants.resolve.dest_base = + copy_shader_constants.dest_base - uint32_t(dest_buffer_info.offset); + push_constants.height_pixels = resolve_info.height_div_8 << 3; + + VkDescriptorSet last_source_descriptor_set = VK_NULL_HANDLE; + VkPipeline last_pipeline = VK_NULL_HANDLE; + for (const ResolveCopyDumpRectangle& rectangle : dump_rectangles_) { + auto& vulkan_rt = *static_cast(rectangle.render_target); + RenderTargetKey rt_key = vulkan_rt.key(); + VkPipeline rect_pipeline = GetDirectResolveFast32Pipeline(is_depth_copy, rt_key.msaa_samples); + if (rect_pipeline != last_pipeline) { + last_pipeline = rect_pipeline; + command_processor_.BindExternalComputePipeline(rect_pipeline); + // BindExternalComputePipeline may invalidate the descriptor bindings. + if (!dest_descriptor_set_bound) { + command_buffer.CmdVkBindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_layout, 0, + 1, &descriptor_set_dest, 0, nullptr); + dest_descriptor_set_bound = true; + } + } + VkDescriptorSet source_descriptor_set = vulkan_rt.GetDescriptorSetTransferSource(); + if (source_descriptor_set != last_source_descriptor_set) { + last_source_descriptor_set = source_descriptor_set; + command_buffer.CmdVkBindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, pipeline_layout, 1, 1, + &source_descriptor_set, 0, nullptr); + } + push_constants.source_base_tiles = rt_key.base_tiles; + push_constants.source_pitch_tiles = rt_key.GetPitchTiles(); + push_constants.source_format_case = 0; + if (!is_depth_copy) { + bool format_is_2_10_10_10 = + rt_key.GetColorFormat() == xenos::ColorRenderTargetFormat::k_2_10_10_10 || + rt_key.GetColorFormat() == xenos::ColorRenderTargetFormat::k_2_10_10_10_AS_10_10_10_10; + push_constants.source_format_case = format_is_2_10_10_10 ? 1 : 0; + } + ResolveCopyDumpRectangle::Dispatch dispatches[ResolveCopyDumpRectangle::kMaxDispatches]; + uint32_t dispatch_count = + rectangle.GetDispatches(dump_pitch, dump_row_length_used, dispatches); + for (uint32_t i = 0; i < dispatch_count; ++i) { + const ResolveCopyDumpRectangle::Dispatch& dispatch = dispatches[i]; + push_constants.dispatch_first_tile = dump_base + dispatch.offset; + command_buffer.CmdVkPushConstants(pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, + sizeof(push_constants), &push_constants); + // 8x8 threads per group; a 32bpp tile is 80x16 single-sampled pixels, so + // a tile is covered by exactly 10x2 groups. + command_buffer.CmdVkDispatch(dispatch.width_tiles * (xenos::kEdramTileWidthSamples / 8), + dispatch.height_tiles * (xenos::kEdramTileHeightSamples / 8), + 1); + } + } + + static bool direct_resolve_logged = false; + if (!direct_resolve_logged) { + direct_resolve_logged = true; + REXGPU_INFO( + "VulkanRenderTargetCache: First direct fast 32bpp resolve performed " + "(dest {:08X}+{:X})", + resolve_info.copy_dest_extent_start, resolve_info.copy_dest_extent_length); + } + return true; +} + +VkClearDepthStencilValue VulkanRenderTargetCache::GetResolveClearDepthStencilValue( + RenderTargetKey key, uint64_t clear_value) const { + VkClearDepthStencilValue depth_stencil_value = {}; + uint32_t depth_guest_clear_value = (uint32_t(clear_value) >> 8) & 0xFFFFFF; + switch (key.GetDepthFormat()) { + case xenos::DepthRenderTargetFormat::kD24S8: + depth_stencil_value.depth = xenos::UNorm24To32(depth_guest_clear_value); + break; + case xenos::DepthRenderTargetFormat::kD24FS8: + // Taking [0, 2) -> [0, 1) remapping into account. + depth_stencil_value.depth = xenos::Float20e4To32(depth_guest_clear_value) * 0.5f; + break; + } + depth_stencil_value.stencil = uint32_t(clear_value) & 0xFF; + return depth_stencil_value; +} + +VkClearColorValue VulkanRenderTargetCache::GetResolveClearColorValue( + RenderTargetKey key, uint64_t clear_value, bool through_transfer_view) const { + VkClearColorValue color_value = {}; + bool value_as_uint = false; + if (through_transfer_view) { + GetColorOwnershipTransferVulkanFormat(key.GetColorFormat(), key.msaa_samples, &value_as_uint); + } + switch (key.GetColorFormat()) { + case xenos::ColorRenderTargetFormat::k_8_8_8_8: + case xenos::ColorRenderTargetFormat::k_8_8_8_8_GAMMA: { + for (uint32_t j = 0; j < 4; ++j) { + color_value.float32[j] = ((clear_value >> (j * 8)) & 0xFF) * (1.0f / 0xFF); + } + if (key.GetColorFormat() == xenos::ColorRenderTargetFormat::k_8_8_8_8_GAMMA && + gamma_render_target_as_unorm16_) { + // 8_8_8_8_GAMMA is represented by linear stored in + // R16G16B16A16_UNORM. + for (uint32_t j = 0; j < 3; ++j) { + color_value.float32[j] = xenos::PWLGammaToLinear(color_value.float32[j]); + } + } + } break; + case xenos::ColorRenderTargetFormat::k_2_10_10_10: + case xenos::ColorRenderTargetFormat::k_2_10_10_10_AS_10_10_10_10: { + for (uint32_t j = 0; j < 3; ++j) { + color_value.float32[j] = ((clear_value >> (j * 10)) & 0x3FF) * (1.0f / 0x3FF); + } + color_value.float32[3] = ((clear_value >> 30) & 0x3) * (1.0f / 0x3); + } break; + case xenos::ColorRenderTargetFormat::k_2_10_10_10_FLOAT: + case xenos::ColorRenderTargetFormat::k_2_10_10_10_FLOAT_AS_16_16_16_16: { + for (uint32_t j = 0; j < 3; ++j) { + color_value.float32[j] = xenos::Float7e3To32((clear_value >> (j * 10)) & 0x3FF); + } + color_value.float32[3] = ((clear_value >> 30) & 0x3) * (1.0f / 0x3); + } break; + case xenos::ColorRenderTargetFormat::k_16_16: + case xenos::ColorRenderTargetFormat::k_16_16_FLOAT: + case xenos::ColorRenderTargetFormat::k_16_16_16_16: + case xenos::ColorRenderTargetFormat::k_16_16_16_16_FLOAT: { + // Using uint for transfers and clears when the transfer view is uint. + // Disregarding the current -32...32 vs. -1...1 settings for consistency + // with color clear via depth aliasing. + uint32_t component_count = + (key.GetColorFormat() == xenos::ColorRenderTargetFormat::k_16_16 || + key.GetColorFormat() == xenos::ColorRenderTargetFormat::k_16_16_FLOAT) + ? 2 + : 4; + for (uint32_t j = 0; j < component_count; ++j) { + uint16_t component = uint16_t((clear_value >> (j * 16)) & 0xFFFF); + if (value_as_uint) { + color_value.uint32[j] = component; + } else if (IsColor16FormatFloatLike(key.GetColorFormat())) { + color_value.float32[j] = rex::xenos_half_to_float(component); + } else { + color_value.float32[j] = std::max(float(int16_t(component)) * (1.0f / 32767.0f), -1.0f); + } + } + } break; + case xenos::ColorRenderTargetFormat::k_32_FLOAT: { + uint32_t component = uint32_t(clear_value); + if (value_as_uint) { + // Using uint for proper denormal and NaN handling. + color_value.uint32[0] = component; + } else { + std::memcpy(&color_value.float32[0], &component, sizeof(component)); + } + } break; + case xenos::ColorRenderTargetFormat::k_32_32_FLOAT: { + uint32_t component_0 = uint32_t(clear_value); + uint32_t component_1 = uint32_t(clear_value >> 32); + if (value_as_uint) { + // Using uint for proper denormal and NaN handling. + color_value.uint32[0] = component_0; + color_value.uint32[1] = component_1; + } else { + std::memcpy(&color_value.float32[0], &component_0, sizeof(component_0)); + std::memcpy(&color_value.float32[1], &component_1, sizeof(component_1)); + } + } break; + } + return color_value; +} + +void VulkanRenderTargetCache::DeferResolveClear(VulkanRenderTarget& render_target, + const Transfer::Rectangle& clear_rectangle, + uint64_t clear_value) { + assert_true(GetPath() == Path::kHostRenderTargets); + RenderTargetKey rt_key = render_target.key(); + VulkanRenderTarget::DeferredClear deferred_clear; + deferred_clear.rectangle = clear_rectangle; + deferred_clear.clear_value = clear_value; + // Conservative full-tile-row coverage of the rectangle for read + // intersection checks. + uint32_t tile_row_height_pixels = xenos::kEdramTileHeightSamples >> + uint32_t(rt_key.msaa_samples >= xenos::MsaaSamples::k2X); + uint32_t row_first = clear_rectangle.y_pixels / tile_row_height_pixels; + uint32_t row_last = + (clear_rectangle.y_pixels + std::max(clear_rectangle.height_pixels, uint32_t(1)) - 1) / + tile_row_height_pixels; + uint32_t pitch_tiles = rt_key.GetPitchTiles(); + deferred_clear.tiles_start = + (rt_key.base_tiles + row_first * pitch_tiles) & (xenos::kEdramTileCount - 1); + deferred_clear.tiles_length = + std::min((row_last - row_first + 1) * pitch_tiles, xenos::kEdramTileCount); + if (render_target.deferred_clears().empty()) { + deferred_clear_render_targets_.push_back(&render_target); + } + render_target.deferred_clears().push_back(deferred_clear); + static bool deferred_clear_logged = false; + if (!deferred_clear_logged) { + deferred_clear_logged = true; + REXGPU_INFO("VulkanRenderTargetCache: First resolve clear deferred ({})", + rt_key.GetDebugName()); + } + ++deferred_clear_stat_deferred_; + if (!(deferred_clear_stat_deferred_ % 8192)) { + REXGPU_INFO( + "VulkanRenderTargetCache: deferred resolve clear stats: deferred={}, pass-entry " + "flushed={}, standalone flushed={}, immediate={}", + deferred_clear_stat_deferred_, deferred_clear_stat_pass_entry_flushed_, + deferred_clear_stat_standalone_flushed_, deferred_clear_stat_immediate_); + } +} + +void VulkanRenderTargetCache::FlushDeferredResolveClears(VulkanRenderTarget& render_target) { + std::vector& deferred_clears = + render_target.deferred_clears(); + if (deferred_clears.empty()) { + return; + } + command_processor_.BeginGpuTimestampedRegion(rex::perf::DrawBucket::kRtTransfer); + RenderTargetKey dest_rt_key = render_target.key(); + + // The same render pass and framebuffer the ownership transfer and resolve + // clear path uses for this render target. + RenderPassKey transfer_render_pass_key; + transfer_render_pass_key.msaa_samples = dest_rt_key.msaa_samples; + if (dest_rt_key.is_depth) { + transfer_render_pass_key.depth_and_color_used = 0b1; + transfer_render_pass_key.depth_format = dest_rt_key.GetDepthFormat(); + } else { + transfer_render_pass_key.depth_and_color_used = 0b1 << 1; + transfer_render_pass_key.color_0_view_format = dest_rt_key.GetColorFormat(); + transfer_render_pass_key.color_rts_use_transfer_formats = 1; + } + VkRenderPass transfer_render_pass = GetHostRenderTargetsRenderPass(transfer_render_pass_key); + const Framebuffer* transfer_framebuffer = nullptr; + if (transfer_render_pass != VK_NULL_HANDLE) { + const RenderTarget* transfer_framebuffer_render_targets[1 + xenos::kMaxColorRenderTargets] = {}; + transfer_framebuffer_render_targets[dest_rt_key.is_depth ? 0 : 1] = &render_target; + transfer_framebuffer = + GetHostRenderTargetsFramebuffer(transfer_render_pass_key, dest_rt_key.pitch_tiles_at_32bpp, + transfer_framebuffer_render_targets); + } + if (!transfer_framebuffer) { + REXGPU_ERROR( + "VulkanRenderTargetCache: Failed to get the render pass or framebuffer for flushing {} " + "deferred resolve clears of {}, dropping them", + deferred_clears.size(), dest_rt_key.GetDebugName()); + } else { + VkPipelineStageFlags dest_stage_mask; + VkAccessFlags dest_access_mask; + VkImageLayout dest_layout; + render_target.GetDrawUsage(&dest_stage_mask, &dest_access_mask, &dest_layout); + command_processor_.PushImageMemoryBarrier( + render_target.image(), + ui::vulkan::util::InitializeSubresourceRange( + dest_rt_key.is_depth ? (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT) + : VK_IMAGE_ASPECT_COLOR_BIT), + render_target.current_stage_mask(), dest_stage_mask, render_target.current_access_mask(), + dest_access_mask, render_target.current_layout(), dest_layout); + render_target.SetUsage(dest_stage_mask, dest_access_mask, dest_layout); + command_processor_.SubmitBarriersAndEnterRenderTargetCacheRenderPass( + transfer_render_pass, transfer_framebuffer, + dest_rt_key.is_depth ? render_target.view_depth_stencil() + : render_target.view_color_transfer(), + dest_rt_key.is_depth != 0); + DeferredCommandBuffer& command_buffer = command_processor_.deferred_command_buffer(); + for (const VulkanRenderTarget::DeferredClear& deferred_clear : deferred_clears) { + VkClearAttachment clear_attachment; + clear_attachment.colorAttachment = 0; + std::memset(&clear_attachment.clearValue, 0, sizeof(clear_attachment.clearValue)); + if (dest_rt_key.is_depth) { + clear_attachment.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + clear_attachment.clearValue.depthStencil = + GetResolveClearDepthStencilValue(dest_rt_key, deferred_clear.clear_value); + } else { + clear_attachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + clear_attachment.clearValue.color = + GetResolveClearColorValue(dest_rt_key, deferred_clear.clear_value, true); + } + VkClearRect clear_rect; + clear_rect.rect.offset.x = + int32_t(deferred_clear.rectangle.x_pixels * draw_resolution_scale_x()); + clear_rect.rect.offset.y = + int32_t(deferred_clear.rectangle.y_pixels * draw_resolution_scale_y()); + clear_rect.rect.extent.width = + deferred_clear.rectangle.width_pixels * draw_resolution_scale_x(); + clear_rect.rect.extent.height = + deferred_clear.rectangle.height_pixels * draw_resolution_scale_y(); + clear_rect.baseArrayLayer = 0; + clear_rect.layerCount = 1; + command_buffer.CmdVkClearAttachments(1, &clear_attachment, 1, &clear_rect); + } + } + deferred_clear_stat_standalone_flushed_ += deferred_clears.size(); + deferred_clears.clear(); + auto registry_it = std::find(deferred_clear_render_targets_.begin(), + deferred_clear_render_targets_.end(), &render_target); + if (registry_it != deferred_clear_render_targets_.end()) { + deferred_clear_render_targets_.erase(registry_it); + } +} + +void VulkanRenderTargetCache::FlushDeferredResolveClearsBeforeEdramRead(uint32_t base_tiles, + uint32_t length_tiles) { + if (deferred_clear_render_targets_.empty() || !length_tiles) { + return; + } + // Whether two tile ranges in the circular EDRAM addressing space intersect. + auto ranges_intersect = [](uint32_t a_start, uint32_t a_length, uint32_t b_start, + uint32_t b_length) { + a_start &= xenos::kEdramTileCount - 1; + b_start &= xenos::kEdramTileCount - 1; + uint64_t a_end = uint64_t(a_start) + std::min(a_length, xenos::kEdramTileCount); + uint64_t b_end = uint64_t(b_start) + std::min(b_length, xenos::kEdramTileCount); + for (int32_t period = -1; period <= 1; ++period) { + int64_t shift = int64_t(period) * xenos::kEdramTileCount; + if (int64_t(a_start) < int64_t(b_end) + shift && + int64_t(b_start) + shift < int64_t(a_end)) { + return true; + } + } + return false; + }; + // Flushing modifies the registry - gather first. + flush_deferred_clear_temp_.clear(); + for (VulkanRenderTarget* render_target : deferred_clear_render_targets_) { + for (const VulkanRenderTarget::DeferredClear& deferred_clear : + render_target->deferred_clears()) { + if (ranges_intersect(deferred_clear.tiles_start, deferred_clear.tiles_length, base_tiles, + length_tiles)) { + flush_deferred_clear_temp_.push_back(render_target); + break; + } + } + } + for (VulkanRenderTarget* render_target : flush_deferred_clear_temp_) { + FlushDeferredResolveClears(*render_target); + } +} + +void VulkanRenderTargetCache::FlushDeferredResolveClearsIntoEnteredRenderPass( + const VkExtent2D& render_area_extent) { + if (deferred_clear_render_targets_.empty()) { + return; + } + RenderPassKey render_pass_key = last_update_render_pass_key_; + RenderTarget* const* render_targets = last_update_accumulated_render_targets(); + DeferredCommandBuffer& command_buffer = command_processor_.deferred_command_buffer(); + for (uint32_t i = 0; i < 1 + xenos::kMaxColorRenderTargets; ++i) { + if (!(render_pass_key.depth_and_color_used & (uint32_t(1) << i)) || !render_targets[i]) { + continue; + } + auto& vulkan_rt = *static_cast(render_targets[i]); + std::vector& deferred_clears = vulkan_rt.deferred_clears(); + if (deferred_clears.empty()) { + continue; + } + RenderTargetKey rt_key = vulkan_rt.key(); + size_t emitted_count = 0; + for (const VulkanRenderTarget::DeferredClear& deferred_clear : deferred_clears) { + VkClearRect clear_rect; + clear_rect.rect.offset.x = + int32_t(deferred_clear.rectangle.x_pixels * draw_resolution_scale_x()); + clear_rect.rect.offset.y = + int32_t(deferred_clear.rectangle.y_pixels * draw_resolution_scale_y()); + clear_rect.rect.extent.width = + deferred_clear.rectangle.width_pixels * draw_resolution_scale_x(); + clear_rect.rect.extent.height = + deferred_clear.rectangle.height_pixels * draw_resolution_scale_y(); + clear_rect.baseArrayLayer = 0; + clear_rect.layerCount = 1; + if (uint32_t(clear_rect.rect.offset.x) + clear_rect.rect.extent.width > + render_area_extent.width || + uint32_t(clear_rect.rect.offset.y) + clear_rect.rect.extent.height > + render_area_extent.height) { + // Not contained in the render area of this render pass - keep this and + // the following clears (to preserve the order) deferred for a later + // flush. + break; + } + VkClearAttachment clear_attachment; + std::memset(&clear_attachment.clearValue, 0, sizeof(clear_attachment.clearValue)); + if (i == 0) { + clear_attachment.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + clear_attachment.colorAttachment = 0; + clear_attachment.clearValue.depthStencil = + GetResolveClearDepthStencilValue(rt_key, deferred_clear.clear_value); + } else { + clear_attachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + // Both the guest render pass objects and the dynamic rendering + // attachment lists are indexed by the guest color render target slot. + clear_attachment.colorAttachment = i - 1; + // Guest render passes use the image's own format for the attachments, + // not the ownership transfer format. + clear_attachment.clearValue.color = + GetResolveClearColorValue(rt_key, deferred_clear.clear_value, false); + } + command_buffer.CmdVkClearAttachments(1, &clear_attachment, 1, &clear_rect); + ++emitted_count; + } + deferred_clear_stat_pass_entry_flushed_ += emitted_count; + if (emitted_count == deferred_clears.size()) { + deferred_clears.clear(); + auto registry_it = std::find(deferred_clear_render_targets_.begin(), + deferred_clear_render_targets_.end(), &vulkan_rt); + if (registry_it != deferred_clear_render_targets_.end()) { + deferred_clear_render_targets_.erase(registry_it); + } + } else if (emitted_count) { + deferred_clears.erase(deferred_clears.begin(), deferred_clears.begin() + emitted_count); + } + } } bool VulkanRenderTargetCache::DumpRenderTargets(uint32_t dump_base, uint32_t dump_row_length_used, @@ -6338,6 +7160,10 @@ bool VulkanRenderTargetCache::DumpRenderTargets(uint32_t dump_base, uint32_t dum return true; } + // The dump reads the current render target contents - deferred resolve + // clears overlapping the dumped range must land first. + FlushDeferredResolveClearsBeforeEdramRead(dump_base, dump_rows * dump_pitch); + // Clear previously set temporary indices. for (const ResolveCopyDumpRectangle& rectangle : dump_rectangles_) { static_cast(rectangle.render_target)->SetTemporarySortIndex(UINT32_MAX); diff --git a/src/graphics/vulkan/shared_memory.cpp b/src/graphics/vulkan/shared_memory.cpp index e0c3603..805b1f8 100644 --- a/src/graphics/vulkan/shared_memory.cpp +++ b/src/graphics/vulkan/shared_memory.cpp @@ -345,6 +345,9 @@ bool VulkanSharedMemory::UploadRanges( if (upload_page_ranges.empty()) { return true; } + // Attribute the GPU time of the upload copies (and the barriers around + // them) to the shared memory upload profiling bucket. + command_processor_.BeginGpuTimestampedRegion(rex::perf::DrawBucket::kSharedMemoryUpload); // upload_page_ranges are sorted, use them to determine the range for the // ordering barrier. Use(Usage::kTransferDestination, diff --git a/src/graphics/vulkan/texture_cache.cpp b/src/graphics/vulkan/texture_cache.cpp index 13785b6..f066477 100644 --- a/src/graphics/vulkan/texture_cache.cpp +++ b/src/graphics/vulkan/texture_cache.cpp @@ -41,12 +41,25 @@ REXCVAR_DEFINE_BOOL(vulkan_scaled_resolve_write_barrier_overlap_only, false, "GP "not overlap") .lifecycle(rex::cvar::Lifecycle::kHotReload); -REXCVAR_DEFINE_INT32(vulkan_debug_log_3d_as_2d_remaining, 256, "GPU/Vulkan", +REXCVAR_DEFINE_INT32(vulkan_debug_log_3d_as_2d_remaining, 0, "GPU/Vulkan", "Log scaled 3D-as-2D texture wrapper usage") .range(0, 10000) .lifecycle(rex::cvar::Lifecycle::kHotReload) .debug_only(); +REXCVAR_DEFINE_BOOL(vulkan_texel_buffer_texture_loads, true, "GPU/Vulkan", + "Read guest texture data through uniform texel buffers instead of raw " + "storage buffer loads where a texel-buffer variant of the load shader is " + "available - much faster for the tiled access pattern on some hosts") + .lifecycle(rex::cvar::Lifecycle::kHotReload); + +REXCVAR_DEFINE_BOOL(vulkan_storage_image_texture_loads, true, "GPU/Vulkan", + "Write guest texture data directly from the load shader through a storage " + "image where a variant of the load shader is available, instead of going " + "through an intermediate buffer and a buffer-to-image copy - the copy is " + "several times slower than device bandwidth on some hosts") + .lifecycle(rex::cvar::Lifecycle::kHotReload); + namespace rex::graphics::vulkan { namespace { @@ -125,6 +138,235 @@ namespace { constexpr VkFormat kInvalidTextureFetchFallbackFormat = VK_FORMAT_R8G8B8A8_UNORM; const VkClearColorValue kInvalidTextureFetchFallbackColor = {{0.0f, 0.0f, 0.0f, 0.0f}}; +// Texel-buffer-source port of texture_load_32bpb_scaled_cs. The body is an +// exact transcription of the spirv-cross decompilation of the shipped SPIR-V +// (only variables renamed and the source storage buffer reads replaced with +// texelFetch from an RGBA32_UINT uniform texel buffer, which is an identical +// uvec4 per element). The push constant block matches LoadConstants. +// Storage-image-destination port of texture_load_32bpb_scaled_cs: the same +// transcription, but the destination is written directly to mip 0 of the +// texture through an R32_UINT storage image view (one packed 32bpp texel per +// store, byte-identical to the buffer path plus buffer-to-image copy), with +// explicit bounds checks replacing the padded destination buffer pitch. Host +// pitch and offset constants are ignored. 2D, single-level, single-layer +// textures only. +const char kTextureLoad32bpbScaledStorageDestShaderSource[] = R"(#version 450 +layout(local_size_x = 4, local_size_y = 32, local_size_z = 1) in; +layout(set = 1, binding = 0, std430) readonly buffer XeTextureLoadSource { + uvec4 data[]; +} xe_src; +layout(set = 0, binding = 0, r32ui) uniform writeonly uimage2D xe_dest; +layout(push_constant, std430) uniform XeTextureLoadConstants { + uint info; + uint guest_offset; + uint guest_pitch_aligned; + uint guest_z_stride_block_rows_aligned; + uvec3 size_blocks; + uint host_offset; + uint host_pitch; + uint height_texels; +} xe; +void main() { + uint endian = (xe.info >> 2u) & 3u; + uvec2 scale = (uvec2(xe.info) >> uvec2(4u, 7u)) & uvec2(7u); + uvec3 block_index = gl_GlobalInvocationID << uvec3(3u, 0u, 0u); + if (any(greaterThanEqual(block_index.xy, xe.size_blocks.xy))) { + return; + } + uint block_x = block_index.x; + uint block_x_div_4 = block_x >> 2u; + uvec2 block_xy_div_scale_pre = uvec2(block_x_div_4, block_index.y); + uvec2 block_xy_unscaled = block_xy_div_scale_pre / scale; + uvec3 guest_block = uvec3(block_xy_unscaled.x << 2u, block_xy_unscaled.y, block_index.z); + uint guest_block_offset; + if ((xe.info & 2u) != 0u) { + ivec3 b = ivec3(guest_block); + int by = b.y; + int bz = b.z; + int bz_div_4 = bz >> 2; + int bx = b.x; + int t0 = (((bx & 7) + ((by & 6) << 2)) << int(8u)) >> 6; + int t1 = ((by >> 3) + bz_div_4) & 1; + int t2 = t1 + ((((bx >> 3) + (t1 << 1)) & 3) << 1); + int t3 = (((((((((bx >> 5) + + (((by >> 4) + (bz_div_4 * int(xe.guest_z_stride_block_rows_aligned >> 4u))) * + int(xe.guest_pitch_aligned >> 5u))) << int(8u)) & 268435455) << 1) + + (t0 & (-16))) << 1) + (t0 & 15)) + ((bz & 3) << int(8u))) + ((by & 1) << 4); + guest_block_offset = + uint(((((((((t2 & 1) << 3) + ((t3 >> 6) & 7)) << 3) + (t2 & (-2))) << 2) + + (t3 & (-512))) << 3) + (t3 & 63)); + } else { + ivec2 b = ivec2(guest_block.xy); + int bx = b.x; + int by = b.y; + int t0 = ((bx & 7) + ((by & 14) << 2)) << int(2u); + int t1 = (((((bx >> 5) + ((by >> 5) * int(xe.guest_pitch_aligned >> 5u))) << int(9u)) + + ((t0 & (-16)) << 1)) + (t0 & 15)) + ((by & 1) << 4); + guest_block_offset = + uint((((((t1 & (-512)) << 3) + ((by & 16) << 7)) + ((t1 & 448) << 2)) + + (((((by & 8) >> 2) + (bx >> 3)) & 3) << 6)) + (t1 & 63)); + } + uvec2 scale_remainder = block_xy_div_scale_pre - (block_xy_unscaled * scale); + uint scale_x = scale.x; + uint scale_y = scale.y; + uint source_index = + (xe.guest_offset + + ((guest_block_offset * (scale_x * scale_y)) + + (((((scale_remainder.x * scale_y) + scale_remainder.y) << 2u) + (block_x & 3u)) + << 2u))) >> 4u; + bool endian_8in16 = endian == 2u; + bool endian_swap_8 = (endian == 1u) || endian_8in16; + bool endian_swap_16 = endian_8in16 || (endian == 3u); + uvec4 value_0 = xe_src.data[source_index]; + if (endian_swap_8) { + value_0 = ((value_0 & uvec4(16711935u)) << uvec4(8u)) | + ((value_0 & uvec4(4278255360u)) >> uvec4(8u)); + } + if (endian_swap_16) { + value_0 = (value_0 << uvec4(16u)) | (value_0 >> uvec4(16u)); + } + int dest_y = int(block_index.y); + for (uint j = 0u; j < 4u; ++j) { + uint dest_x = block_x + j; + if (dest_x < xe.size_blocks.x) { + imageStore(xe_dest, ivec2(int(dest_x), dest_y), uvec4(value_0[j], 0u, 0u, 0u)); + } + } + uint second_advance_bytes; + if (scale_x > 1u) { + uint x_unscaled_remainder = block_x_div_4 - ((block_x_div_4 / scale_x) * scale_x); + if ((x_unscaled_remainder + 1u) == scale_x) { + second_advance_bytes = (32u * scale_x) - (x_unscaled_remainder << 4u); + } else { + second_advance_bytes = 16u; + } + } else { + second_advance_bytes = 32u; + } + uint source_index_1 = source_index + ((second_advance_bytes * scale_y) >> 4u); + uvec4 value_1 = xe_src.data[source_index_1]; + if (endian_swap_8) { + value_1 = ((value_1 & uvec4(16711935u)) << uvec4(8u)) | + ((value_1 & uvec4(4278255360u)) >> uvec4(8u)); + } + if (endian_swap_16) { + value_1 = (value_1 << uvec4(16u)) | (value_1 >> uvec4(16u)); + } + for (uint j = 0u; j < 4u; ++j) { + uint dest_x = block_x + 4u + j; + if (dest_x < xe.size_blocks.x) { + imageStore(xe_dest, ivec2(int(dest_x), dest_y), uvec4(value_1[j], 0u, 0u, 0u)); + } + } +} +)"; + +const char kTextureLoad32bpbScaledTexelBufferShaderSource[] = R"(#version 450 +layout(local_size_x = 4, local_size_y = 32, local_size_z = 1) in; +layout(set = 1, binding = 0) uniform utextureBuffer xe_src; +layout(set = 0, binding = 0, std430) writeonly buffer XeTextureLoadDest { + uvec4 data[]; +} xe_dest; +layout(push_constant, std430) uniform XeTextureLoadConstants { + uint info; + uint guest_offset; + uint guest_pitch_aligned; + uint guest_z_stride_block_rows_aligned; + uvec3 size_blocks; + uint host_offset; + uint host_pitch; + uint height_texels; +} xe; +void main() { + uint endian = (xe.info >> 2u) & 3u; + uvec2 scale = (uvec2(xe.info) >> uvec2(4u, 7u)) & uvec2(7u); + uvec3 block_index = gl_GlobalInvocationID << uvec3(3u, 0u, 0u); + if (any(greaterThanEqual(block_index.xy, xe.size_blocks.xy))) { + return; + } + ivec3 block_index_int = ivec3(block_index); + uint dest_index = + (uint((block_index_int.x * 4) + + (((block_index_int.z * int(xe.size_blocks.y)) + block_index_int.y) * + int(xe.host_pitch))) + + xe.host_offset) >> 4u; + uint block_x = block_index.x; + uint block_x_div_4 = block_x >> 2u; + uvec2 block_xy_div_scale_pre = uvec2(block_x_div_4, block_index.y); + uvec2 block_xy_unscaled = block_xy_div_scale_pre / scale; + uvec3 guest_block = uvec3(block_xy_unscaled.x << 2u, block_xy_unscaled.y, block_index.z); + uint guest_block_offset; + if ((xe.info & 2u) != 0u) { + ivec3 b = ivec3(guest_block); + int by = b.y; + int bz = b.z; + int bz_div_4 = bz >> 2; + int bx = b.x; + int t0 = (((bx & 7) + ((by & 6) << 2)) << int(8u)) >> 6; + int t1 = ((by >> 3) + bz_div_4) & 1; + int t2 = t1 + ((((bx >> 3) + (t1 << 1)) & 3) << 1); + int t3 = (((((((((bx >> 5) + + (((by >> 4) + (bz_div_4 * int(xe.guest_z_stride_block_rows_aligned >> 4u))) * + int(xe.guest_pitch_aligned >> 5u))) << int(8u)) & 268435455) << 1) + + (t0 & (-16))) << 1) + (t0 & 15)) + ((bz & 3) << int(8u))) + ((by & 1) << 4); + guest_block_offset = + uint(((((((((t2 & 1) << 3) + ((t3 >> 6) & 7)) << 3) + (t2 & (-2))) << 2) + + (t3 & (-512))) << 3) + (t3 & 63)); + } else { + ivec2 b = ivec2(guest_block.xy); + int bx = b.x; + int by = b.y; + int t0 = ((bx & 7) + ((by & 14) << 2)) << int(2u); + int t1 = (((((bx >> 5) + ((by >> 5) * int(xe.guest_pitch_aligned >> 5u))) << int(9u)) + + ((t0 & (-16)) << 1)) + (t0 & 15)) + ((by & 1) << 4); + guest_block_offset = + uint((((((t1 & (-512)) << 3) + ((by & 16) << 7)) + ((t1 & 448) << 2)) + + (((((by & 8) >> 2) + (bx >> 3)) & 3) << 6)) + (t1 & 63)); + } + uvec2 scale_remainder = block_xy_div_scale_pre - (block_xy_unscaled * scale); + uint scale_x = scale.x; + uint scale_y = scale.y; + uint source_index = + (xe.guest_offset + + ((guest_block_offset * (scale_x * scale_y)) + + (((((scale_remainder.x * scale_y) + scale_remainder.y) << 2u) + (block_x & 3u)) + << 2u))) >> 4u; + bool endian_8in16 = endian == 2u; + bool endian_swap_8 = (endian == 1u) || endian_8in16; + uvec4 value_0 = texelFetch(xe_src, int(source_index)); + if (endian_swap_8) { + value_0 = ((value_0 & uvec4(16711935u)) << uvec4(8u)) | + ((value_0 & uvec4(4278255360u)) >> uvec4(8u)); + } + bool endian_swap_16 = endian_8in16 || (endian == 3u); + if (endian_swap_16) { + value_0 = (value_0 << uvec4(16u)) | (value_0 >> uvec4(16u)); + } + xe_dest.data[dest_index] = value_0; + uint second_advance_bytes; + if (scale_x > 1u) { + uint x_unscaled_remainder = block_x_div_4 - ((block_x_div_4 / scale_x) * scale_x); + if ((x_unscaled_remainder + 1u) == scale_x) { + second_advance_bytes = (32u * scale_x) - (x_unscaled_remainder << 4u); + } else { + second_advance_bytes = 16u; + } + } else { + second_advance_bytes = 32u; + } + uint source_index_1 = source_index + ((second_advance_bytes * scale_y) >> 4u); + uvec4 value_1 = texelFetch(xe_src, int(source_index_1)); + if (endian_swap_8) { + value_1 = ((value_1 & uvec4(16711935u)) << uvec4(8u)) | + ((value_1 & uvec4(4278255360u)) >> uvec4(8u)); + } + if (endian_swap_16) { + value_1 = (value_1 << uvec4(16u)) | (value_1 >> uvec4(16u)); + } + xe_dest.data[dest_index + 1u] = value_1; +} +)"; + } // namespace static_assert(VK_FORMAT_UNDEFINED == VkFormat(0), @@ -502,11 +744,50 @@ VulkanTextureCache::~VulkanTextureCache() { if (load_pipeline_layout_ != VK_NULL_HANDLE) { dfn.vkDestroyPipelineLayout(device, load_pipeline_layout_, nullptr); } + for (VkPipeline load_pipeline : load_pipelines_scaled_texel_) { + if (load_pipeline != VK_NULL_HANDLE) { + dfn.vkDestroyPipeline(device, load_pipeline, nullptr); + } + } + load_pipelines_scaled_texel_.fill(VK_NULL_HANDLE); + load_pipelines_scaled_texel_attempted_.fill(false); + for (const auto& binding_pair : load_source_texel_bindings_) { + dfn.vkDestroyBufferView(device, binding_pair.second.buffer_view, nullptr); + } + load_source_texel_bindings_.clear(); + load_source_texel_descriptor_set_pool_.reset(); + if (load_pipeline_layout_texel_ != VK_NULL_HANDLE) { + dfn.vkDestroyPipelineLayout(device, load_pipeline_layout_texel_, nullptr); + load_pipeline_layout_texel_ = VK_NULL_HANDLE; + } + if (load_descriptor_set_layout_source_texel_ != VK_NULL_HANDLE) { + dfn.vkDestroyDescriptorSetLayout(device, load_descriptor_set_layout_source_texel_, nullptr); + load_descriptor_set_layout_source_texel_ = VK_NULL_HANDLE; + } + for (VkPipeline load_pipeline : load_pipelines_scaled_storage_dest_) { + if (load_pipeline != VK_NULL_HANDLE) { + dfn.vkDestroyPipeline(device, load_pipeline, nullptr); + } + } + load_pipelines_scaled_storage_dest_.fill(VK_NULL_HANDLE); + load_pipelines_scaled_storage_dest_attempted_.fill(false); + if (load_pipeline_layout_storage_dest_ != VK_NULL_HANDLE) { + dfn.vkDestroyPipelineLayout(device, load_pipeline_layout_storage_dest_, nullptr); + load_pipeline_layout_storage_dest_ = VK_NULL_HANDLE; + } // Textures memory is allocated using the Vulkan Memory Allocator, destroy all - // textures before destroying VMA. + // textures before destroying VMA. The texture destructors release their + // storage destination descriptor sets, so the pool and its set layout must + // still be alive here. DestroyAllTextures(true); + load_dest_storage_descriptor_set_pool_.reset(); + if (load_descriptor_set_layout_dest_storage_ != VK_NULL_HANDLE) { + dfn.vkDestroyDescriptorSetLayout(device, load_descriptor_set_layout_dest_storage_, nullptr); + load_descriptor_set_layout_dest_storage_ = VK_NULL_HANDLE; + } + if (vma_allocator_ != VK_NULL_HANDLE) { vmaDestroyAllocator(vma_allocator_); } @@ -1179,12 +1460,36 @@ std::unique_ptr VulkanTextureCache::CreateTexture(Texture bool is_3d = key.dimension == xenos::DataDimension::k3D; uint32_t depth_or_array_size = key.GetDepthOrArraySize(); + // Whether to create the image with storage usage and an R32_UINT view + // format, so the load shaders can write it directly (32-bit uncompressed + // host formats of 2D single-layer textures only). + bool storage_capable = false; + if (!is_3d && key.dimension != xenos::DataDimension::kCube && depth_or_array_size == 1) { + switch (formats[0]) { + case VK_FORMAT_R8G8B8A8_UNORM: + case VK_FORMAT_B8G8R8A8_UNORM: + case VK_FORMAT_R8G8B8A8_UINT: + case VK_FORMAT_R16G16_UNORM: + case VK_FORMAT_R16G16_SNORM: + case VK_FORMAT_R16G16_SFLOAT: + case VK_FORMAT_R16G16_UINT: + case VK_FORMAT_R32_SFLOAT: + case VK_FORMAT_R32_UINT: + case VK_FORMAT_A2B10G10R10_UNORM_PACK32: + case VK_FORMAT_B10G11R11_UFLOAT_PACK32: + storage_capable = true; + break; + default: + break; + } + } + VkImageCreateInfo image_create_info; VkImageCreateInfo* image_create_info_last = &image_create_info; image_create_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; image_create_info.pNext = nullptr; image_create_info.flags = 0; - if (formats[1] != VK_FORMAT_UNDEFINED) { + if (formats[1] != VK_FORMAT_UNDEFINED || storage_capable) { image_create_info.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; } if (key.dimension == xenos::DataDimension::kCube) { @@ -1204,19 +1509,29 @@ std::unique_ptr VulkanTextureCache::CreateTexture(Texture 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; + if (storage_capable) { + image_create_info.usage |= VK_IMAGE_USAGE_STORAGE_BIT; + } image_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; image_create_info.queueFamilyIndexCount = 0; image_create_info.pQueueFamilyIndices = nullptr; image_create_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + VkFormat view_formats[3] = {formats[0], VK_FORMAT_UNDEFINED, VK_FORMAT_UNDEFINED}; + uint32_t view_format_count = 1; + if (formats[1] != VK_FORMAT_UNDEFINED) { + view_formats[view_format_count++] = formats[1]; + } + if (storage_capable) { + view_formats[view_format_count++] = VK_FORMAT_R32_UINT; + } VkImageFormatListCreateInfo image_format_list_create_info; - if (formats[1] != VK_FORMAT_UNDEFINED && - vulkan_device->extensions().ext_1_2_KHR_image_format_list) { + if (view_format_count > 1 && vulkan_device->extensions().ext_1_2_KHR_image_format_list) { image_create_info_last->pNext = &image_format_list_create_info; image_create_info_last = reinterpret_cast(&image_format_list_create_info); image_format_list_create_info.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO; image_format_list_create_info.pNext = nullptr; - image_format_list_create_info.viewFormatCount = 2; - image_format_list_create_info.pViewFormats = formats; + image_format_list_create_info.viewFormatCount = view_format_count; + image_format_list_create_info.pViewFormats = view_formats; } VmaAllocationCreateInfo allocation_create_info = {}; @@ -1229,7 +1544,9 @@ std::unique_ptr VulkanTextureCache::CreateTexture(Texture return nullptr; } - return std::unique_ptr(new VulkanTexture(*this, key, image, allocation)); + VulkanTexture* vulkan_texture = new VulkanTexture(*this, key, image, allocation); + vulkan_texture->SetImageStorageCapable(storage_capable); + return std::unique_ptr(vulkan_texture); } bool VulkanTextureCache::EnsureScaledResolveMemoryCommitted(uint32_t start_unscaled, @@ -1256,6 +1573,8 @@ const char* VulkanTextureCache::DebugVulkanTextureUsageName(VulkanTexture::Usage return "undefined"; case VulkanTexture::Usage::kTransferDestination: return "transfer-dst"; + case VulkanTexture::Usage::kStorageWrite: + return "storage-write"; case VulkanTexture::Usage::kGuestShaderSampled: return "guest-sampled"; case VulkanTexture::Usage::kSwapSampled: @@ -1302,6 +1621,145 @@ void VulkanTextureCache::DebugLogTeamProfileBackgroundGpuEvent( uint32_t(signed_format), host_format_pair.swizzle); } +VkPipeline VulkanTextureCache::GetLoadPipelineScaledTexelBuffer(LoadShaderIndex load_shader) { + if (load_pipeline_layout_texel_ == VK_NULL_HANDLE || !load_source_texel_descriptor_set_pool_) { + return VK_NULL_HANDLE; + } + if (load_pipelines_scaled_texel_attempted_[load_shader]) { + return load_pipelines_scaled_texel_[load_shader]; + } + load_pipelines_scaled_texel_attempted_[load_shader] = true; + + const char* source = nullptr; + switch (load_shader) { + case kLoadShaderIndex32bpb: + source = kTextureLoad32bpbScaledTexelBufferShaderSource; + break; + default: + return VK_NULL_HANDLE; + } + + std::vector spirv; + std::string compile_error; + if (!command_processor_.CompileGlslToSpirv(VK_SHADER_STAGE_COMPUTE_BIT, source, spirv, + compile_error)) { + REXGPU_WARN( + "VulkanTextureCache: Failed to compile the texel-buffer load shader (index {}): {}", + uint32_t(load_shader), compile_error); + return VK_NULL_HANDLE; + } + VkPipeline pipeline = ui::vulkan::util::CreateComputePipeline( + command_processor_.GetVulkanDevice(), load_pipeline_layout_texel_, spirv.data(), + sizeof(uint32_t) * spirv.size()); + if (pipeline == VK_NULL_HANDLE) { + REXGPU_WARN("VulkanTextureCache: Failed to create the texel-buffer load pipeline (index {})", + uint32_t(load_shader)); + } else { + REXGPU_INFO("VulkanTextureCache: Texel-buffer load pipeline created (index {})", + uint32_t(load_shader)); + } + load_pipelines_scaled_texel_[load_shader] = pipeline; + return pipeline; +} + +VkPipeline VulkanTextureCache::GetLoadPipelineScaledStorageDest(LoadShaderIndex load_shader) { + if (load_pipeline_layout_storage_dest_ == VK_NULL_HANDLE || + !load_dest_storage_descriptor_set_pool_) { + return VK_NULL_HANDLE; + } + if (load_pipelines_scaled_storage_dest_attempted_[load_shader]) { + return load_pipelines_scaled_storage_dest_[load_shader]; + } + load_pipelines_scaled_storage_dest_attempted_[load_shader] = true; + + const char* source = nullptr; + switch (load_shader) { + case kLoadShaderIndex32bpb: + source = kTextureLoad32bpbScaledStorageDestShaderSource; + break; + default: + return VK_NULL_HANDLE; + } + + std::vector spirv; + std::string compile_error; + if (!command_processor_.CompileGlslToSpirv(VK_SHADER_STAGE_COMPUTE_BIT, source, spirv, + compile_error)) { + REXGPU_WARN( + "VulkanTextureCache: Failed to compile the storage-image load shader (index {}): {}", + uint32_t(load_shader), compile_error); + return VK_NULL_HANDLE; + } + VkPipeline pipeline = ui::vulkan::util::CreateComputePipeline( + command_processor_.GetVulkanDevice(), load_pipeline_layout_storage_dest_, spirv.data(), + sizeof(uint32_t) * spirv.size()); + if (pipeline == VK_NULL_HANDLE) { + REXGPU_WARN("VulkanTextureCache: Failed to create the storage-image load pipeline (index {})", + uint32_t(load_shader)); + } else { + REXGPU_INFO("VulkanTextureCache: Storage-image load pipeline created (index {})", + uint32_t(load_shader)); + } + load_pipelines_scaled_storage_dest_[load_shader] = pipeline; + return pipeline; +} + +VkDescriptorSet VulkanTextureCache::GetLoadSourceTexelBufferDescriptorSet(VkBuffer buffer, + VkDeviceSize offset, + VkDeviceSize range) { + if (!load_source_texel_descriptor_set_pool_) { + return VK_NULL_HANDLE; + } + auto key = std::make_tuple(uint64_t(reinterpret_cast(buffer)), uint64_t(offset), + uint64_t(range)); + auto it = load_source_texel_bindings_.find(key); + if (it != load_source_texel_bindings_.end()) { + return load_source_texel_descriptor_set_pool_->Get(it->second.descriptor_set_index); + } + + const ui::vulkan::VulkanDevice* const vulkan_device = command_processor_.GetVulkanDevice(); + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); + const VkDevice device = vulkan_device->device(); + + VkBufferViewCreateInfo buffer_view_create_info; + buffer_view_create_info.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO; + buffer_view_create_info.pNext = nullptr; + buffer_view_create_info.flags = 0; + buffer_view_create_info.buffer = buffer; + buffer_view_create_info.format = VK_FORMAT_R32G32B32A32_UINT; + buffer_view_create_info.offset = offset; + buffer_view_create_info.range = range; + VkBufferView buffer_view; + if (dfn.vkCreateBufferView(device, &buffer_view_create_info, nullptr, &buffer_view) != + VK_SUCCESS) { + return VK_NULL_HANDLE; + } + size_t descriptor_set_index = load_source_texel_descriptor_set_pool_->Allocate(); + if (descriptor_set_index == SIZE_MAX) { + dfn.vkDestroyBufferView(device, buffer_view, nullptr); + return VK_NULL_HANDLE; + } + VkDescriptorSet descriptor_set = + load_source_texel_descriptor_set_pool_->Get(descriptor_set_index); + VkWriteDescriptorSet write_descriptor_set; + write_descriptor_set.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write_descriptor_set.pNext = nullptr; + write_descriptor_set.dstSet = descriptor_set; + write_descriptor_set.dstBinding = 0; + write_descriptor_set.dstArrayElement = 0; + write_descriptor_set.descriptorCount = 1; + write_descriptor_set.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER; + write_descriptor_set.pImageInfo = nullptr; + write_descriptor_set.pBufferInfo = nullptr; + write_descriptor_set.pTexelBufferView = &buffer_view; + dfn.vkUpdateDescriptorSets(device, 1, &write_descriptor_set, 0, nullptr); + + LoadSourceTexelBinding& binding = load_source_texel_bindings_[key]; + binding.buffer_view = buffer_view; + binding.descriptor_set_index = descriptor_set_index; + return descriptor_set; +} + bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, bool load_base, bool load_mips) { VulkanTexture& vulkan_texture = static_cast(texture); @@ -1327,6 +1785,14 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, if (pipeline == VK_NULL_HANDLE) { return false; } + // Prefer the texel-buffer-source variant where available - reading the + // source through the texture path is much faster for the tiled access + // pattern on some hosts. Falls back to the storage buffer pipeline if the + // source ranges can't be expressed as texel buffer views. + VkPipeline pipeline_texel = VK_NULL_HANDLE; + if (texture_key.scaled_resolve && REXCVAR_GET(vulkan_texel_buffer_texture_loads)) { + pipeline_texel = GetLoadPipelineScaledTexelBuffer(load_shader); + } const LoadShaderInfo& load_shader_info = GetLoadShaderInfo(load_shader); LoadShaderIndex load_shader_float_convert = kLoadShaderIndexUnknown; if (host_format.format == VK_FORMAT_R16G16B16A16_SFLOAT) { @@ -1469,15 +1935,60 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); const VkDevice device = vulkan_device->device(); VulkanSharedMemory& vulkan_shared_memory = static_cast(shared_memory()); + + // Storage-image destination fast path: write the texture directly from the + // load shader, skipping the copy buffer and the buffer-to-image copy (much + // slower than device bandwidth on some hosts). 2D single-layer single-level + // unpacked textures with 32-bit-class host formats only. + VkPipeline pipeline_storage_dest = VK_NULL_HANDLE; + VkDescriptorSet descriptor_set_dest_storage = VK_NULL_HANDLE; + if (texture_key.scaled_resolve && REXCVAR_GET(vulkan_storage_image_texture_loads)) { + static int storage_dest_fallback_logs_remaining = 8; + auto log_storage_fallback = [&](const char* reason) { + if (storage_dest_fallback_logs_remaining > 0) { + --storage_dest_fallback_logs_remaining; + REXGPU_INFO( + "VulkanTextureCache: storage-image load fallback ({}): load_shader={}, " + "storage_capable={}, levels={}..{}, packed={}, array={}, {} more logs", + reason, uint32_t(load_shader), vulkan_texture.image_storage_capable(), level_first, + level_last, level_packed, array_size, storage_dest_fallback_logs_remaining); + } + }; + if (!vulkan_texture.image_storage_capable()) { + log_storage_fallback("image-not-storage-capable"); + } else if (level_first != 0 || level_last != 0 || level_packed == 0) { + log_storage_fallback("levels"); + } else if (array_size != 1) { + log_storage_fallback("array"); + } else { + pipeline_storage_dest = GetLoadPipelineScaledStorageDest(load_shader); + if (pipeline_storage_dest != VK_NULL_HANDLE) { + descriptor_set_dest_storage = vulkan_texture.GetStorageDescriptorSet(); + if (descriptor_set_dest_storage == VK_NULL_HANDLE) { + log_storage_fallback("storage-binding"); + pipeline_storage_dest = VK_NULL_HANDLE; + } + } else { + log_storage_fallback("no-shader-variant"); + } + } + } + const bool use_storage_dest = pipeline_storage_dest != VK_NULL_HANDLE; + if (use_storage_dest) { + // The texel-buffer source variant uses a different pipeline layout. + pipeline_texel = VK_NULL_HANDLE; + } + std::array write_descriptor_sets; uint32_t write_descriptor_set_count = 0; - VkDescriptorSet descriptor_set_dest = command_processor_.AllocateSingleTransientDescriptor( - VulkanCommandProcessor::SingleTransientDescriptorLayout ::kStorageBufferCompute); - if (!descriptor_set_dest) { - return false; - } + VkDescriptorSet descriptor_set_dest = descriptor_set_dest_storage; VkDescriptorBufferInfo write_descriptor_set_dest_buffer_info; - { + if (!use_storage_dest) { + descriptor_set_dest = command_processor_.AllocateSingleTransientDescriptor( + VulkanCommandProcessor::SingleTransientDescriptorLayout ::kStorageBufferCompute); + if (!descriptor_set_dest) { + return false; + } write_descriptor_set_dest_buffer_info.buffer = scratch_buffer; write_descriptor_set_dest_buffer_info.offset = 0; write_descriptor_set_dest_buffer_info.range = host_buffer_size; @@ -1508,7 +2019,73 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, uint64_t debug_source_base_range = 0; uint64_t debug_source_mips_start = 0; uint64_t debug_source_mips_range = 0; - if (level_first == 0) { + // Extra byte offsets folded into the guest offset constant when the texel + // buffer view had to start earlier than the source range for alignment. + uint32_t source_base_texel_extra_offset = 0; + uint32_t source_mips_texel_extra_offset = 0; + if (pipeline_texel != VK_NULL_HANDLE) { + // Try to set up texel buffer views of the source ranges; on any failure, + // use the storage buffer pipeline for this load. + const ui::vulkan::VulkanDevice::Properties& device_properties = vulkan_device->properties(); + VkDeviceSize texel_offset_alignment = + std::max(device_properties.minTexelBufferOffsetAlignment, VkDeviceSize(16)); + uint64_t max_view_bytes = uint64_t(device_properties.maxTexelBufferElements) * 16; + auto acquire_texel_source = [&](uint32_t start_unscaled, uint32_t guest_size, + VkDescriptorSet& descriptor_set_out, + uint32_t& extra_offset_out) -> bool { + uint64_t source_start; + uint64_t source_range; + VkBuffer source_buffer; + if (texture_key.scaled_resolve) { + if (!GetScaledResolveRange(start_unscaled, guest_size, load_shader_info.source_bpe_log2, + source_start, source_range)) { + return false; + } + source_buffer = scaled_resolve_buffer_; + } else { + source_start = start_unscaled; + source_range = rex::align(uint64_t(guest_size), uint64_t(source_length_alignment)); + if (source_range > uint64_t(SharedMemory::kBufferSize) - source_start) { + return false; + } + source_buffer = vulkan_shared_memory.buffer(); + } + uint64_t view_offset = source_start & ~uint64_t(texel_offset_alignment - 1); + uint64_t extra_offset = source_start - view_offset; + // The extra offset is added to byte offsets that the shader divides by + // the 16-byte element size - must keep element indexing exact. + if (extra_offset & 15) { + return false; + } + uint64_t view_range = rex::align(extra_offset + source_range, uint64_t(16)); + if (view_range > max_view_bytes) { + return false; + } + descriptor_set_out = + GetLoadSourceTexelBufferDescriptorSet(source_buffer, view_offset, view_range); + extra_offset_out = uint32_t(extra_offset); + return descriptor_set_out != VK_NULL_HANDLE; + }; + bool texel_sources_ok = true; + if (level_first == 0) { + texel_sources_ok &= + acquire_texel_source(texture_key.base_page << 12, vulkan_texture.GetGuestBaseSize(), + descriptor_set_source_base, source_base_texel_extra_offset); + } + if (texel_sources_ok && level_last != 0) { + texel_sources_ok &= + acquire_texel_source(texture_key.mip_page << 12, vulkan_texture.GetGuestMipsSize(), + descriptor_set_source_mips, source_mips_texel_extra_offset); + } + if (!texel_sources_ok) { + pipeline_texel = VK_NULL_HANDLE; + descriptor_set_source_base = VK_NULL_HANDLE; + descriptor_set_source_mips = VK_NULL_HANDLE; + source_base_texel_extra_offset = 0; + source_mips_texel_extra_offset = 0; + } + } + if (pipeline_texel == VK_NULL_HANDLE && level_first == 0) { descriptor_set_source_base = command_processor_.AllocateSingleTransientDescriptor( VulkanCommandProcessor::SingleTransientDescriptorLayout ::kStorageBufferCompute); if (!descriptor_set_source_base) { @@ -1550,7 +2127,7 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, write_descriptor_set_source_base.pBufferInfo = &write_descriptor_set_source_base_buffer_info; write_descriptor_set_source_base.pTexelBufferView = nullptr; } - if (level_last != 0) { + if (pipeline_texel == VK_NULL_HANDLE && level_last != 0) { descriptor_set_source_mips = command_processor_.AllocateSingleTransientDescriptor( VulkanCommandProcessor::SingleTransientDescriptorLayout ::kStorageBufferCompute); if (!descriptor_set_source_mips) { @@ -1645,6 +2222,10 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, : "none", debug_team_profile_mips_scaled_start, debug_team_profile_mips_scaled_length); } + // Attribute the GPU time of the load dispatches (and the barriers around + // them) to the texture upload profiling bucket. + command_processor_.BeginGpuTimestampedRegion(rex::perf::DrawBucket::kTextureUpload); + if (texture_key.scaled_resolve) { UseScaledResolveBufferForRead(); } else { @@ -1655,9 +2236,38 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, DeferredCommandBuffer& command_buffer = command_processor_.deferred_command_buffer(); - command_processor_.BindExternalComputePipeline(pipeline); + // For direct storage image writes, transition the texture to the writable + // usage before the dispatches (the barrier is flushed before dispatching). + if (use_storage_dest) { + vulkan_texture.MarkAsUsed(); + VulkanTexture::Usage texture_old_usage = + vulkan_texture.SetUsage(VulkanTexture::Usage::kStorageWrite); + if (texture_old_usage != VulkanTexture::Usage::kStorageWrite) { + VkPipelineStageFlags texture_src_stage_mask, texture_dst_stage_mask; + VkAccessFlags texture_src_access_mask, texture_dst_access_mask; + VkImageLayout texture_old_layout, texture_new_layout; + GetTextureUsageMasks(texture_old_usage, texture_src_stage_mask, texture_src_access_mask, + texture_old_layout); + GetTextureUsageMasks(VulkanTexture::Usage::kStorageWrite, texture_dst_stage_mask, + texture_dst_access_mask, texture_new_layout); + command_processor_.PushImageMemoryBarrier( + vulkan_texture.image(), ui::vulkan::util::InitializeSubresourceRange(), + texture_src_stage_mask, texture_dst_stage_mask, texture_src_access_mask, + texture_dst_access_mask, texture_old_layout, texture_new_layout); + } + } - command_buffer.CmdVkBindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, load_pipeline_layout_, + VkPipelineLayout active_load_pipeline_layout = + use_storage_dest + ? load_pipeline_layout_storage_dest_ + : (pipeline_texel != VK_NULL_HANDLE ? load_pipeline_layout_texel_ + : load_pipeline_layout_); + command_processor_.BindExternalComputePipeline( + use_storage_dest ? pipeline_storage_dest + : (pipeline_texel != VK_NULL_HANDLE ? pipeline_texel : pipeline)); + + command_buffer.CmdVkBindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, + active_load_pipeline_layout, kLoadDescriptorSetIndexDestination, 1, &descriptor_set_dest, 0, nullptr); @@ -1681,13 +2291,17 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, is_base ? descriptor_set_source_base : descriptor_set_source_mips; if (descriptor_set_source_current != descriptor_set_source) { descriptor_set_source_current = descriptor_set_source; - command_buffer.CmdVkBindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, load_pipeline_layout_, + command_buffer.CmdVkBindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, + active_load_pipeline_layout, kLoadDescriptorSetIndexSource, 1, &descriptor_set_source, 0, nullptr); } // TODO(Triang3l): guest_offset relative to the storage buffer origin. - load_constants.guest_offset = 0; + // For texel buffer sources, the offset starts at the alignment padding of + // the view rather than 0. + load_constants.guest_offset = + is_base ? source_base_texel_extra_offset : source_mips_texel_extra_offset; if (!is_base) { load_constants.guest_offset += guest_layout.mip_offsets_bytes[level] * (texture_resolution_scale_x * texture_resolution_scale_y); @@ -1738,7 +2352,7 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, load_constants.host_pitch = load_shader_info.bytes_per_host_block * level_host_layout.x_pitch_blocks; - command_buffer.CmdVkPushConstants(load_pipeline_layout_, VK_SHADER_STAGE_COMPUTE_BIT, 0, + command_buffer.CmdVkPushConstants(active_load_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(load_constants), &load_constants); uint32_t level_array_slice_stride_bytes_scaled = @@ -1746,11 +2360,11 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, (texture_resolution_scale_x * texture_resolution_scale_y); for (uint32_t slice = 0; slice < array_size; ++slice) { if (slice != 0) { - command_buffer.CmdVkPushConstants(load_pipeline_layout_, VK_SHADER_STAGE_COMPUTE_BIT, + command_buffer.CmdVkPushConstants(active_load_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, offsetof(LoadConstants, guest_offset), sizeof(load_constants.guest_offset), &load_constants.guest_offset); - command_buffer.CmdVkPushConstants(load_pipeline_layout_, VK_SHADER_STAGE_COMPUTE_BIT, + command_buffer.CmdVkPushConstants(active_load_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, offsetof(LoadConstants, host_offset), sizeof(load_constants.host_offset), &load_constants.host_offset); @@ -1864,7 +2478,16 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, } } + if (use_storage_dest) { + // The texture was written directly by the load dispatches - no copy + // needed. + DebugLogTeamProfileBackgroundGpuEvent("load-exit", UINT32_MAX, texture); + return true; + } + // Submit copying from the copy buffer to the host texture. + // Timed separately from the load dispatches to attribute the upload cost. + command_processor_.BeginGpuTimestampedRegion(rex::perf::DrawBucket::kTextureUploadCopy); command_processor_.PushBufferMemoryBarrier( scratch_buffer, 0, VK_WHOLE_SIZE, scratch_buffer_acquisition.SetStageMask(VK_PIPELINE_STAGE_TRANSFER_BIT), @@ -2034,9 +2657,83 @@ VulkanTextureCache::VulkanTexture::~VulkanTexture() { if (image_view_3d_as_2d_signed_ != VK_NULL_HANDLE) { dfn.vkDestroyImageView(device, image_view_3d_as_2d_signed_, nullptr); } + if (storage_view_ != VK_NULL_HANDLE) { + dfn.vkDestroyImageView(device, storage_view_, nullptr); + } + if (storage_descriptor_set_index_ != SIZE_MAX) { + VulkanTextureCache& mutable_cache = const_cast(vulkan_texture_cache); + if (mutable_cache.load_dest_storage_descriptor_set_pool_) { + mutable_cache.load_dest_storage_descriptor_set_pool_->Free(storage_descriptor_set_index_); + } + } vmaDestroyImage(vulkan_texture_cache.vma_allocator_, image_, allocation_); } +VkDescriptorSet VulkanTextureCache::VulkanTexture::GetStorageDescriptorSet() { + VulkanTextureCache& vulkan_texture_cache = + const_cast(static_cast(texture_cache())); + if (!image_storage_capable_ || !vulkan_texture_cache.load_dest_storage_descriptor_set_pool_) { + return VK_NULL_HANDLE; + } + if (storage_binding_attempted_) { + return storage_descriptor_set_index_ != SIZE_MAX + ? vulkan_texture_cache.load_dest_storage_descriptor_set_pool_->Get( + storage_descriptor_set_index_) + : VK_NULL_HANDLE; + } + storage_binding_attempted_ = true; + + 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(); + + VkImageViewCreateInfo view_create_info; + view_create_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + view_create_info.pNext = nullptr; + view_create_info.flags = 0; + view_create_info.image = image_; + view_create_info.viewType = VK_IMAGE_VIEW_TYPE_2D; + view_create_info.format = VK_FORMAT_R32_UINT; + view_create_info.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; + view_create_info.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; + view_create_info.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; + view_create_info.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; + view_create_info.subresourceRange = + ui::vulkan::util::InitializeSubresourceRange(VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1); + if (dfn.vkCreateImageView(device, &view_create_info, nullptr, &storage_view_) != VK_SUCCESS) { + storage_view_ = VK_NULL_HANDLE; + return VK_NULL_HANDLE; + } + size_t descriptor_set_index = + vulkan_texture_cache.load_dest_storage_descriptor_set_pool_->Allocate(); + if (descriptor_set_index == SIZE_MAX) { + dfn.vkDestroyImageView(device, storage_view_, nullptr); + storage_view_ = VK_NULL_HANDLE; + return VK_NULL_HANDLE; + } + VkDescriptorSet descriptor_set = + vulkan_texture_cache.load_dest_storage_descriptor_set_pool_->Get(descriptor_set_index); + VkDescriptorImageInfo image_info; + image_info.sampler = VK_NULL_HANDLE; + image_info.imageView = storage_view_; + image_info.imageLayout = VK_IMAGE_LAYOUT_GENERAL; + VkWriteDescriptorSet write_descriptor_set; + write_descriptor_set.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write_descriptor_set.pNext = nullptr; + write_descriptor_set.dstSet = descriptor_set; + write_descriptor_set.dstBinding = 0; + write_descriptor_set.dstArrayElement = 0; + write_descriptor_set.descriptorCount = 1; + write_descriptor_set.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + write_descriptor_set.pImageInfo = &image_info; + write_descriptor_set.pBufferInfo = nullptr; + write_descriptor_set.pTexelBufferView = nullptr; + dfn.vkUpdateDescriptorSets(device, 1, &write_descriptor_set, 0, nullptr); + storage_descriptor_set_index_ = descriptor_set_index; + return descriptor_set; +} + VkImageView VulkanTextureCache::VulkanTexture::GetView(bool is_signed, uint32_t host_swizzle, bool is_array) { xenos::DataDimension dimension = key().dimension; @@ -2288,7 +2985,10 @@ bool VulkanTextureCache::InitializeScaledResolveBuffer() { buffer_create_info.pNext = nullptr; buffer_create_info.flags = sparse_flags; buffer_create_info.size = scaled_resolve_buffer_size_; - buffer_create_info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; + // UNIFORM_TEXEL_BUFFER is for the texel-buffer-source texture load + // pipelines. + buffer_create_info.usage = + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT; buffer_create_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; buffer_create_info.queueFamilyIndexCount = 0; buffer_create_info.pQueueFamilyIndices = nullptr; @@ -2323,7 +3023,8 @@ bool VulkanTextureCache::InitializeScaledResolveBuffer() { if (scaled_resolve_buffer_ == VK_NULL_HANDLE) { buffer_create_info.flags = 0; if (!ui::vulkan::util::CreateDedicatedAllocationBuffer( - vulkan_device, scaled_resolve_buffer_size_, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, + vulkan_device, scaled_resolve_buffer_size_, + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT, ui::vulkan::util::MemoryPurpose::kDeviceLocal, scaled_resolve_buffer_, scaled_resolve_buffer_memory_.emplace_back(), &scaled_resolve_buffer_memory_type_)) { REXGPU_ERROR( @@ -2961,6 +3662,110 @@ bool VulkanTextureCache::Initialize() { return false; } + // Texel-buffer-source load pipeline layout (set 0 - storage buffer + // destination, set 1 - uniform texel buffer source) and the descriptor set + // pool for the persistent cached source bindings. Failure is not fatal - + // the storage buffer path is used then. + { + VkDescriptorSetLayoutBinding source_texel_binding; + source_texel_binding.binding = 0; + source_texel_binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER; + source_texel_binding.descriptorCount = 1; + source_texel_binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + source_texel_binding.pImmutableSamplers = nullptr; + VkDescriptorSetLayoutCreateInfo source_texel_layout_create_info; + source_texel_layout_create_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + source_texel_layout_create_info.pNext = nullptr; + source_texel_layout_create_info.flags = 0; + source_texel_layout_create_info.bindingCount = 1; + source_texel_layout_create_info.pBindings = &source_texel_binding; + if (dfn.vkCreateDescriptorSetLayout(device, &source_texel_layout_create_info, nullptr, + &load_descriptor_set_layout_source_texel_) == VK_SUCCESS) { + VkDescriptorSetLayout load_texel_descriptor_set_layouts[kLoadDescriptorSetCount] = {}; + load_texel_descriptor_set_layouts[kLoadDescriptorSetIndexDestination] = + load_descriptor_set_layout_storage_buffer; + load_texel_descriptor_set_layouts[kLoadDescriptorSetIndexSource] = + load_descriptor_set_layout_source_texel_; + VkPipelineLayoutCreateInfo load_texel_pipeline_layout_create_info = + load_pipeline_layout_create_info; + load_texel_pipeline_layout_create_info.pSetLayouts = load_texel_descriptor_set_layouts; + if (dfn.vkCreatePipelineLayout(device, &load_texel_pipeline_layout_create_info, nullptr, + &load_pipeline_layout_texel_) == VK_SUCCESS) { + VkDescriptorPoolSize source_texel_pool_size; + source_texel_pool_size.type = VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER; + source_texel_pool_size.descriptorCount = 1; + load_source_texel_descriptor_set_pool_ = + std::make_unique( + vulkan_device, 256, 1, &source_texel_pool_size, + load_descriptor_set_layout_source_texel_); + } else { + REXGPU_WARN( + "VulkanTextureCache: Failed to create the texel-buffer load pipeline layout - using " + "storage buffer texture loads"); + dfn.vkDestroyDescriptorSetLayout(device, load_descriptor_set_layout_source_texel_, + nullptr); + load_descriptor_set_layout_source_texel_ = VK_NULL_HANDLE; + } + } else { + REXGPU_WARN( + "VulkanTextureCache: Failed to create the texel-buffer load descriptor set layout - " + "using storage buffer texture loads"); + load_descriptor_set_layout_source_texel_ = VK_NULL_HANDLE; + } + } + + // Storage-image-destination load pipeline layout (set 0 - storage image + // destination, set 1 - storage buffer source) and the descriptor set pool + // for the per-texture destination bindings. Failure is not fatal - the + // buffer destination plus copy path is used then. + { + VkDescriptorSetLayoutBinding dest_storage_binding; + dest_storage_binding.binding = 0; + dest_storage_binding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + dest_storage_binding.descriptorCount = 1; + dest_storage_binding.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + dest_storage_binding.pImmutableSamplers = nullptr; + VkDescriptorSetLayoutCreateInfo dest_storage_layout_create_info; + dest_storage_layout_create_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + dest_storage_layout_create_info.pNext = nullptr; + dest_storage_layout_create_info.flags = 0; + dest_storage_layout_create_info.bindingCount = 1; + dest_storage_layout_create_info.pBindings = &dest_storage_binding; + if (dfn.vkCreateDescriptorSetLayout(device, &dest_storage_layout_create_info, nullptr, + &load_descriptor_set_layout_dest_storage_) == VK_SUCCESS) { + VkDescriptorSetLayout load_storage_descriptor_set_layouts[kLoadDescriptorSetCount] = {}; + load_storage_descriptor_set_layouts[kLoadDescriptorSetIndexDestination] = + load_descriptor_set_layout_dest_storage_; + load_storage_descriptor_set_layouts[kLoadDescriptorSetIndexSource] = + load_descriptor_set_layout_storage_buffer; + VkPipelineLayoutCreateInfo load_storage_pipeline_layout_create_info = + load_pipeline_layout_create_info; + load_storage_pipeline_layout_create_info.pSetLayouts = load_storage_descriptor_set_layouts; + if (dfn.vkCreatePipelineLayout(device, &load_storage_pipeline_layout_create_info, nullptr, + &load_pipeline_layout_storage_dest_) == VK_SUCCESS) { + VkDescriptorPoolSize dest_storage_pool_size; + dest_storage_pool_size.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + dest_storage_pool_size.descriptorCount = 1; + load_dest_storage_descriptor_set_pool_ = + std::make_unique( + vulkan_device, 256, 1, &dest_storage_pool_size, + load_descriptor_set_layout_dest_storage_); + } else { + REXGPU_WARN( + "VulkanTextureCache: Failed to create the storage-image load pipeline layout - using " + "buffer texture loads"); + dfn.vkDestroyDescriptorSetLayout(device, load_descriptor_set_layout_dest_storage_, + nullptr); + load_descriptor_set_layout_dest_storage_ = VK_NULL_HANDLE; + } + } else { + REXGPU_WARN( + "VulkanTextureCache: Failed to create the storage-image load descriptor set layout - " + "using buffer texture loads"); + load_descriptor_set_layout_dest_storage_ = VK_NULL_HANDLE; + } + } + // Load pipelines, only the ones needed for the formats that will be used. bool load_shaders_needed[kLoadShaderCount] = {}; @@ -3419,6 +4224,11 @@ void VulkanTextureCache::GetTextureUsageMasks(VulkanTexture::Usage usage, access_mask = VK_ACCESS_TRANSFER_WRITE_BIT; layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; break; + case VulkanTexture::Usage::kStorageWrite: + stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; + access_mask = VK_ACCESS_SHADER_WRITE_BIT; + layout = VK_IMAGE_LAYOUT_GENERAL; + break; case VulkanTexture::Usage::kGuestShaderSampled: stage_mask = guest_shader_pipeline_stages_; access_mask = VK_ACCESS_SHADER_READ_BIT; diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 9da6489..1df1d90 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -20,6 +20,7 @@ set(REXUI_CORE_SOURCES set(REXUI_OVERLAY_SOURCES overlay/debug_overlay.cpp overlay/console_overlay.cpp + overlay/fps_overlay.cpp overlay/acquire_wizard_overlay.cpp overlay/install_wizard_overlay.cpp overlay/simple_settings_overlay.cpp @@ -145,6 +146,11 @@ if(WIN32) target_link_libraries(rexui PUBLIC dwmapi Shcore + # The backend-agnostic Presenter drives a DXGI UI tick thread on Windows + # (display-refresh sync), so DXGI is required regardless of the graphics + # backend selected (e.g. Vulkan-on-Windows builds with D3D12 disabled). + dxgi + dxguid ) elseif(APPLE) target_link_libraries(rexui PUBLIC SDL3::SDL3) @@ -163,11 +169,9 @@ else() ) endif() -# D3D12 backend dependencies +# D3D12 backend dependencies (dxgi/dxguid linked unconditionally on Windows above) if(REXGLUE_USE_D3D12) target_link_libraries(rexui PUBLIC - dxgi - dxguid dxc-headers ) endif() diff --git a/src/ui/imgui_drawer.cpp b/src/ui/imgui_drawer.cpp index d9364b5..b27de00 100644 --- a/src/ui/imgui_drawer.cpp +++ b/src/ui/imgui_drawer.cpp @@ -9,6 +9,7 @@ * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime */ +#include #include #include #include @@ -408,7 +409,8 @@ void ImGuiDrawer::Draw(UIDrawContext& ui_draw_context) { // it now if needed. DetachIfLastDialogRemoved(); - if (!dialogs_.empty()) { + if (std::any_of(dialogs_.cbegin(), dialogs_.cend(), + [](const ImGuiDialog* dialog) { return dialog->WantsContinuousRepaint(); })) { // Repaint (and handle input) continuously if still active. presenter_->RequestUIPaintFromUIThread(); } diff --git a/src/ui/overlay/fps_overlay.cpp b/src/ui/overlay/fps_overlay.cpp new file mode 100644 index 0000000..54620b3 --- /dev/null +++ b/src/ui/overlay/fps_overlay.cpp @@ -0,0 +1,66 @@ +/** + * @file ui/overlay/fps_overlay.cpp + * + * @brief Minimal guest FPS readout overlay. See fps_overlay.h for + * details. + * + * @copyright Copyright (c) 2026 Tom Clay + * All rights reserved. + * + * @license BSD 3-Clause License + * See LICENSE file in the project root for full license text. + */ +#include + +#include + +#include + +namespace rex::ui { + +void FpsOverlayDialog::OnDraw(ImGuiIO& io) { + Presenter::GuestFrameStats stats; + if (presenter_) { + stats = presenter_->GetGuestFrameStats(); + } + + constexpr float kMargin = 10.0f; + ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x - kMargin, kMargin), ImGuiCond_Always, + ImVec2(1.0f, 0.0f)); + ImGui::SetNextWindowBgAlpha(0.4f); + ImGui::PushFont(nullptr, 20.0f); + if (ImGui::Begin("##fps_overlay", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs | + ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_AlwaysAutoResize)) { + if (stats.frame_count > 0 && stats.fps > 0.0) { + ImGui::Text("%.0f FPS", stats.fps); + ImGui::PushFont(nullptr, 14.0f); + ImGui::Text("%.2f ms", stats.frame_time_ms); + // Time the GPU emulation thread spent blocked on host GPU fences last + // frame: near the frame time = GPU-bound, near zero = thread-bound. + ImGui::Text("wait %.2f ms", stats.wait_ms); + if (stats.gpu_ms > 0.0) { + // Device-timeline span of a recent frame (first to last command, + // including idle gaps between submissions). + ImGui::Text("gpu %.2f ms", stats.gpu_ms); + double gpu_known_ms = stats.gpu_draw_ms + stats.gpu_resolve_ms + stats.gpu_dump_ms; + if (gpu_known_ms > 0.0) { + // "other" covers everything not in a bucket: render target + // ownership transfers, barriers, and idle gaps. + ImGui::Text("draw %.2f res %.2f", stats.gpu_draw_ms, stats.gpu_resolve_ms); + ImGui::Text("dump %.2f other %.2f", stats.gpu_dump_ms, + std::max(0.0, stats.gpu_ms - gpu_known_ms)); + } + } + ImGui::PopFont(); + } else { + ImGui::TextUnformatted("-- FPS"); + } + } + ImGui::End(); + ImGui::PopFont(); +} + +} // namespace rex::ui diff --git a/src/ui/presenter.cpp b/src/ui/presenter.cpp index f760f81..6578a52 100644 --- a/src/ui/presenter.cpp +++ b/src/ui/presenter.cpp @@ -585,6 +585,16 @@ bool Presenter::RefreshGuestOutput( return false; } guest_output_active_last_refresh_ = true; + if (GuestFrameStatsEnabled()) { + std::lock_guard stats_lock(guest_frame_stats_mutex_); + guest_frame_timestamps_[guest_frame_timestamp_next_] = std::chrono::steady_clock::now(); + guest_frame_timestamp_next_ = (guest_frame_timestamp_next_ + 1) % kGuestFrameTimestampCount; + guest_frame_timestamp_count_ = + std::min(guest_frame_timestamp_count_ + 1, kGuestFrameTimestampCount); + ++guest_frame_total_count_; + guest_frame_last_wait_us_ = + guest_frame_wait_accumulator_us_.exchange(0, std::memory_order_relaxed); + } } else { // Request presenting a blank image if there was a true image previously, // but not now. @@ -687,6 +697,93 @@ bool Presenter::RefreshGuestOutput( return is_active; } +void Presenter::SetGuestFrameStatsEnabled(bool enabled) { + const bool was_enabled = GuestFrameStatsEnabled(); + if (enabled == was_enabled) { + return; + } + if (!enabled) { + guest_frame_stats_enabled_.store(false, std::memory_order_relaxed); + return; + } + + std::lock_guard stats_lock(guest_frame_stats_mutex_); + guest_frame_timestamps_ = {}; + guest_frame_timestamp_next_ = 0; + guest_frame_timestamp_count_ = 0; + guest_frame_total_count_ = 0; + guest_frame_last_wait_us_ = 0; + guest_frame_wait_accumulator_us_.store(0, std::memory_order_relaxed); + guest_frame_gpu_span_us_.store(0, std::memory_order_relaxed); + guest_frame_gpu_draw_us_.store(0, std::memory_order_relaxed); + guest_frame_gpu_resolve_us_.store(0, std::memory_order_relaxed); + guest_frame_gpu_dump_us_.store(0, std::memory_order_relaxed); + guest_frame_stats_enabled_.store(true, std::memory_order_relaxed); +} + +Presenter::GuestFrameStats Presenter::GetGuestFrameStats() const { + if (!GuestFrameStatsEnabled()) { + return {}; + } + + // Average over up to the last second of frames. Measuring the window from + // the oldest in-window frame to the present time (rather than to the newest + // frame) makes the reported FPS decay naturally when the guest stops + // producing frames instead of freezing at the last value. + constexpr std::chrono::steady_clock::duration kWindow = std::chrono::seconds(1); + const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + const std::chrono::steady_clock::time_point window_start = now - kWindow; + + GuestFrameStats stats; + std::lock_guard stats_lock(guest_frame_stats_mutex_); + stats.frame_count = guest_frame_total_count_; + stats.wait_ms = double(guest_frame_last_wait_us_) / 1000.0; + stats.gpu_ms = double(guest_frame_gpu_span_us_.load(std::memory_order_relaxed)) / 1000.0; + stats.gpu_draw_ms = double(guest_frame_gpu_draw_us_.load(std::memory_order_relaxed)) / 1000.0; + stats.gpu_resolve_ms = + double(guest_frame_gpu_resolve_us_.load(std::memory_order_relaxed)) / 1000.0; + stats.gpu_dump_ms = double(guest_frame_gpu_dump_us_.load(std::memory_order_relaxed)) / 1000.0; + size_t frames_in_window = 0; + std::chrono::steady_clock::time_point oldest_in_window; + std::chrono::steady_clock::time_point newest_in_window; + for (size_t i = 0; i < guest_frame_timestamp_count_; ++i) { + // Walk from the newest sample backwards. + size_t index = + (guest_frame_timestamp_next_ + kGuestFrameTimestampCount - 1 - i) % + kGuestFrameTimestampCount; + const std::chrono::steady_clock::time_point timestamp = guest_frame_timestamps_[index]; + if (timestamp < window_start) { + break; + } + if (!frames_in_window) { + newest_in_window = timestamp; + } + oldest_in_window = timestamp; + ++frames_in_window; + } + if (!frames_in_window) { + return stats; + } + const double span_to_now_seconds = + std::chrono::duration_cast>(now - oldest_in_window).count(); + if (span_to_now_seconds > 0.0) { + stats.fps = double(frames_in_window) / span_to_now_seconds; + } + if (frames_in_window >= 2) { + const double span_between_frames_seconds = + std::chrono::duration_cast>(newest_in_window - + oldest_in_window) + .count(); + if (span_between_frames_seconds > 0.0) { + stats.frame_time_ms = + span_between_frames_seconds * 1000.0 / double(frames_in_window - 1); + } + } else if (stats.fps > 0.0) { + stats.frame_time_ms = 1000.0 / stats.fps; + } + return stats; +} + void Presenter::SetGuestOutputPaintConfigFromUIThread(const GuestOutputPaintConfig& new_config) { // For simplicity, this may be called externally repeatedly. // Lock the mutex only when something has been modified, and also don't diff --git a/src/ui/rex_app.cpp b/src/ui/rex_app.cpp index 8ddf37f..966a369 100644 --- a/src/ui/rex_app.cpp +++ b/src/ui/rex_app.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #if REX_HAS_VULKAN @@ -208,6 +209,9 @@ void StartForcedExitWatchdog(const char* reason) { REXCVAR_DEFINE_BOOL(advanced_settings_overlay_enabled, true, "UI/Advanced", "Enable the developer cvar browser on F4"); +REXCVAR_DEFINE_BOOL(show_fps_counter, false, "UI", "Show the guest FPS counter overlay") + .lifecycle(rex::cvar::Lifecycle::kHotReload); + // --- ReXApp --- ReXApp::~ReXApp() = default; @@ -548,6 +552,31 @@ bool ReXApp::SetupPresentation() { imgui_drawer_ = std::make_unique( window_.get(), 64, [this](ImFontAtlas* atlas) { OnConfigureFonts(atlas); }); imgui_drawer_->SetPresenterAndImmediateDrawer(presenter, immediate_drawer_.get()); + if (!frame_stats_provider_) { + frame_stats_provider_ = [presenter]() { + ui::Presenter::GuestFrameStats stats = presenter->GetGuestFrameStats(); + return ui::FrameStats{stats.frame_time_ms, stats.fps, stats.frame_count}; + }; + } + auto update_guest_frame_stats_enabled = [this, presenter]() { + presenter->SetGuestFrameStatsEnabled(fps_overlay_ != nullptr || debug_overlay_ != nullptr); + }; + rex::ui::RegisterBind("bind_fps_counter", "F2", "Toggle FPS counter", [this, presenter] { + if (fps_overlay_) { + fps_overlay_.reset(); + } else { + fps_overlay_ = + std::make_unique(imgui_drawer_.get(), presenter); + } + REXCVAR_SET(show_fps_counter, fps_overlay_ != nullptr); + presenter->SetGuestFrameStatsEnabled(fps_overlay_ != nullptr || debug_overlay_ != nullptr); + if (!config_path_.empty()) { + rex::cvar::SaveConfigValues(config_path_, {"show_fps_counter"}); + } + }); + if (REXCVAR_GET(show_fps_counter)) { + fps_overlay_ = std::make_unique(imgui_drawer_.get(), presenter); + } rex::ui::RegisterBind("bind_debug_overlay", "F3", "Toggle debug overlay", [this] { if (debug_overlay_) { debug_overlay_.reset(); @@ -555,7 +584,13 @@ bool ReXApp::SetupPresentation() { debug_overlay_ = std::make_unique( imgui_drawer_.get(), frame_stats_provider_, GetBuildStamp()); } + auto* graphics_system = static_cast(config_.graphics.get()); + if (graphics_system && graphics_system->presenter()) { + graphics_system->presenter()->SetGuestFrameStatsEnabled( + fps_overlay_ != nullptr || debug_overlay_ != nullptr); + } }); + update_guest_frame_stats_enabled(); rex::ui::RegisterBind("bind_console", "Backtick", "Toggle console overlay", [this] { if (console_overlay_) { console_overlay_.reset(); diff --git a/src/ui/vulkan/vulkan_device.cpp b/src/ui/vulkan/vulkan_device.cpp index fba470a..72ff3b4 100644 --- a/src/ui/vulkan/vulkan_device.cpp +++ b/src/ui/vulkan/vulkan_device.cpp @@ -25,6 +25,12 @@ REXCVAR_DEFINE_BOOL(vulkan_require_fragment_stores_and_atomics, true, "UI/Vulkan "Deprecated and ignored for parity; fragmentStoresAndAtomics is always " "required for Vulkan GPU emulation") .lifecycle(rex::cvar::Lifecycle::kInitOnly); +REXCVAR_DEFINE_BOOL(vulkan_robust_buffer_access, true, "UI/Vulkan", + "Enable robustBufferAccess (bounds checking on all shader buffer accesses, " + "including the per-vertex guest memory fetches). Disabling can improve GPU " + "performance but out-of-bounds accesses from invalid guest fetch constants " + "then return undefined data and may crash the GPU - experimental") + .lifecycle(rex::cvar::Lifecycle::kInitOnly); REXCVAR_DEFINE_BOOL(vulkan_require_vertex_pipeline_stores_and_atomics, true, "UI/Vulkan", "Deprecated and ignored for parity; vertexPipelineStoresAndAtomics is always " "required for Vulkan GPU emulation") @@ -218,6 +224,8 @@ std::unique_ptr VulkanDevice::CreateIfSupported( XE_UI_VULKAN_LOCAL_PROMOTED_EXTENSION(KHR_sampler_mirror_clamp_to_edge, 1, 2) // #70. Must be enabled for VK_KHR_sampler_ycbcr_conversion. XE_UI_VULKAN_LOCAL_PROMOTED_EXTENSION(KHR_maintenance1, 1, 1) + // #81. Used for guest draw constant buffer descriptors. + XE_UI_VULKAN_STRUCT_EXTENSION(KHR_push_descriptor) // #141. XE_UI_VULKAN_STRUCT_EXTENSION(EXT_shader_stencil_export) // #148. @@ -613,6 +621,8 @@ std::unique_ptr VulkanDevice::CreateIfSupported( XE_UI_VULKAN_LIMIT(maxViewportDimensions[1]) XE_UI_VULKAN_LIMIT(minUniformBufferOffsetAlignment) XE_UI_VULKAN_LIMIT(minStorageBufferOffsetAlignment) + XE_UI_VULKAN_LIMIT(maxTexelBufferElements) + XE_UI_VULKAN_LIMIT(minTexelBufferOffsetAlignment) XE_UI_VULKAN_LIMIT(maxFramebufferWidth) XE_UI_VULKAN_LIMIT(maxFramebufferHeight) XE_UI_VULKAN_ENUM_LIMIT(framebufferColorSampleCounts, SampleCountFlags) @@ -632,6 +642,11 @@ std::unique_ptr VulkanDevice::CreateIfSupported( if (with_gpu_emulation) { XE_UI_VULKAN_FEATURE(robustBufferAccess) + if (!REXCVAR_GET(vulkan_robust_buffer_access)) { + enabled_features.robustBufferAccess = VK_FALSE; + device->properties_.robustBufferAccess = VK_FALSE; + REXLOG_INFO("* robustBufferAccess disabled by vulkan_robust_buffer_access=false"); + } XE_UI_VULKAN_FEATURE(fullDrawIndexUint32) XE_UI_VULKAN_FEATURE(independentBlend) XE_UI_VULKAN_FEATURE(geometryShader) @@ -806,6 +821,9 @@ std::unique_ptr VulkanDevice::CreateIfSupported( if (device->extensions_.ext_KHR_swapchain) { #include } + if (device->extensions_.ext_KHR_push_descriptor) { +#include + } #undef XE_UI_VULKAN_FUNCTION_PROMOTED #undef XE_UI_VULKAN_FUNCTION diff --git a/src/ui/vulkan/vulkan_instance.cpp b/src/ui/vulkan/vulkan_instance.cpp index f85f331..1e05474 100644 --- a/src/ui/vulkan/vulkan_instance.cpp +++ b/src/ui/vulkan/vulkan_instance.cpp @@ -23,7 +23,7 @@ #include #include -REXCVAR_DEFINE_BOOL(vulkan_log_debug_messages, true, "UI/Vulkan", "Log Vulkan debug messages"); +REXCVAR_DEFINE_BOOL(vulkan_log_debug_messages, false, "UI/Vulkan", "Log Vulkan debug messages"); namespace rex { namespace ui {