diff --git a/include/rex/filesystem/devices/null_file.h b/include/rex/filesystem/devices/null_file.h index 786abde..9666f33 100644 --- a/include/rex/filesystem/devices/null_file.h +++ b/include/rex/filesystem/devices/null_file.h @@ -19,11 +19,11 @@ namespace rex::filesystem { -class NullEntry; +class Entry; class NullFile : public File { public: - NullFile(uint32_t file_access, NullEntry* entry); + NullFile(uint32_t file_access, Entry* entry); ~NullFile() override; void Destroy() override; diff --git a/include/rex/filesystem/flags.h b/include/rex/filesystem/flags.h index d1f7f95..7224524 100644 --- a/include/rex/filesystem/flags.h +++ b/include/rex/filesystem/flags.h @@ -15,3 +15,5 @@ REXCVAR_DECLARE(std::string, dump_source); REXCVAR_DECLARE(std::string, dump_path); +REXCVAR_DECLARE(int32_t, filesystem_debug_log_fe_asset_ops_remaining); +REXCVAR_DECLARE(int32_t, filesystem_debug_log_team_profile_background_remaining); diff --git a/include/rex/graphics/command_processor.h b/include/rex/graphics/command_processor.h index 6998a45..21d6946 100644 --- a/include/rex/graphics/command_processor.h +++ b/include/rex/graphics/command_processor.h @@ -45,6 +45,7 @@ enum class ReadbackResolveMode { kFast, kSome, kFull, + kAuto, }; struct SwapState { diff --git a/include/rex/graphics/d3d12/texture_cache.h b/include/rex/graphics/d3d12/texture_cache.h index 4d845c7..a5e58c8 100644 --- a/include/rex/graphics/d3d12/texture_cache.h +++ b/include/rex/graphics/d3d12/texture_cache.h @@ -328,9 +328,6 @@ class D3D12TextureCache final : public TextureCache { ID3D12Resource* resource() const { return resource_.Get(); } D3D12_RESOURCE_STATES SetResourceState(D3D12_RESOURCE_STATES new_state) { D3D12_RESOURCE_STATES old_state = resource_state_; - if (old_state == D3D12_RESOURCE_STATE_UNORDERED_ACCESS) { - uav_barrier_pending_ = false; - } resource_state_ = new_state; return old_state; } @@ -342,6 +339,11 @@ class D3D12TextureCache final : public TextureCache { } // After an aliasing barrier (which is even stronger than an UAV barrier). void ClearUAVBarrierPending() { uav_barrier_pending_ = false; } + bool ConsumeUAVBarrierPending() { + bool pending = uav_barrier_pending_; + uav_barrier_pending_ = false; + return pending; + } private: Microsoft::WRL::ComPtr resource_; diff --git a/include/rex/graphics/flags.h b/include/rex/graphics/flags.h index e0679b8..8db4c07 100644 --- a/include/rex/graphics/flags.h +++ b/include/rex/graphics/flags.h @@ -29,6 +29,10 @@ REXCVAR_DECLARE(int32_t, draw_resolution_scale_x); REXCVAR_DECLARE(int32_t, draw_resolution_scale_y); REXCVAR_DECLARE(bool, resolve_resolution_scale_fill_half_pixel_offset); REXCVAR_DECLARE(bool, draw_resolution_scaled_texture_offsets); +REXCVAR_DECLARE(bool, scaled_resolve_texture_loads_from_shared_memory); +REXCVAR_DECLARE(int32_t, scaled_resolve_texture_loads_from_shared_memory_max_pixels); +REXCVAR_DECLARE(bool, scaled_resolve_texture_loads_update_guest_memory); +REXCVAR_DECLARE(int32_t, scaled_resolve_small_texture_readback_max_length); REXCVAR_DECLARE(std::string, readback_resolve); REXCVAR_DECLARE(bool, readback_resolve_half_pixel_offset); REXCVAR_DECLARE(bool, readback_memexport); @@ -87,6 +91,18 @@ REXCVAR_DECLARE(bool, vulkan_sparse_shared_memory); REXCVAR_DECLARE(bool, vulkan_submit_on_primary_buffer_end); REXCVAR_DECLARE(bool, vulkan_dynamic_rendering); REXCVAR_DECLARE(bool, vulkan_async_skip_incomplete_frames); +REXCVAR_DECLARE(int32_t, vulkan_async_placeholder_max_consecutive_skips); +REXCVAR_DECLARE(int32_t, vulkan_debug_log_frame_summaries_remaining); +REXCVAR_DECLARE(int32_t, vulkan_debug_frame_summary_interval_frames); +REXCVAR_DECLARE(int32_t, vulkan_debug_log_resolve_decisions_remaining); +REXCVAR_DECLARE(int32_t, vulkan_debug_log_team_profile_background_candidates_remaining); +REXCVAR_DECLARE(int32_t, vulkan_debug_log_team_profile_background_bindings_remaining); +REXCVAR_DECLARE(int32_t, vulkan_debug_log_team_profile_background_draws_remaining); +REXCVAR_DECLARE(bool, vulkan_sync_after_resolve); +REXCVAR_DECLARE(bool, vulkan_barrier_after_scaled_resolve); +REXCVAR_DECLARE(int32_t, vulkan_readback_resolve_max_length); +REXCVAR_DECLARE(int32_t, vulkan_readback_resolve_min_address); +REXCVAR_DECLARE(int32_t, vulkan_readback_resolve_max_address); REXCVAR_DECLARE(int32_t, vulkan_pipeline_creation_threads); REXCVAR_DECLARE(bool, vulkan_tessellation_wireframe); REXCVAR_DECLARE(bool, vulkan_force_expand_point_sprites_in_vs); diff --git a/include/rex/graphics/pipeline/texture/cache.h b/include/rex/graphics/pipeline/texture/cache.h index d1580fc..04db0f1 100644 --- a/include/rex/graphics/pipeline/texture/cache.h +++ b/include/rex/graphics/pipeline/texture/cache.h @@ -88,6 +88,11 @@ class TextureCache { uint32_t /*length_scaled_alignment_log2*/ = 0) { return false; } + virtual bool MaterializeScaledResolveToSharedMemory(uint32_t /*start_unscaled*/, + uint32_t /*length_unscaled*/, + uint32_t /*pixel_size_log2*/) { + return false; + } static uint32_t GuestToHostSwizzle(uint32_t guest_swizzle, uint32_t host_format_swizzle); @@ -110,6 +115,16 @@ class TextureCache { } virtual void RequestTextures(uint32_t used_texture_mask); + void DebugRecordResolveReadback(uint32_t start, uint32_t length, bool scaled); + void DebugClearRecentResolveReadbacks(); + void DebugRecordTeamProfileBackgroundResolveRange(uint32_t start, uint32_t length, bool scaled, + uint64_t scaled_start, + uint64_t scaled_length); + bool DebugFindTeamProfileBackgroundBinding(uint32_t used_texture_mask, + uint32_t& fetch_index_out, + bool& signed_view_out, + uint32_t* texture_base_out = nullptr, + uint32_t* texture_length_out = nullptr) const; // "ActiveTexture" means as of the latest RequestTextures call. @@ -228,7 +243,8 @@ class TextureCache { } void MakeUpToDateAndWatch(const std::unique_lock& global_lock); - void WatchCallback(const std::unique_lock& global_lock, bool is_mip); + void WatchCallback(const std::unique_lock& global_lock, bool is_mip, + bool invalidated_by_gpu); // For LRU caching - updates the last usage frame and moves the texture to // the end of the usage queue. Must be called any time the texture is @@ -525,6 +541,10 @@ class TextureCache { // into the texture object. virtual bool LoadTextureDataFromResidentMemoryImpl(Texture& texture, bool load_base, bool load_mips) = 0; + bool DebugTextureOverlapsRecentTeamProfileBackgroundResolve( + const Texture& texture, bool mips, uint32_t& resolve_serial_out, + bool& resolve_scaled_out, uint64_t& resolve_scaled_start_out, + uint64_t& resolve_scaled_length_out) const; // Converts a texture fetch constant to a texture key, normalizing and // validating the values, or creating an invalid key, and also gets the @@ -544,6 +564,7 @@ class TextureCache { // Called when something in a texture binding is changed for the // implementation to update the internal dependencies of the binding. virtual void UpdateTextureBindingsImpl(uint32_t /*fetch_constant_mask*/) {} + bool DebugIsTeamProfileBackgroundTexture(const Texture& texture) const; private: struct PendingTextureLoad { @@ -554,11 +575,21 @@ class TextureCache { struct PendingSharedMemoryRange { uint32_t start = 0; uint32_t length = 0; + bool materialize_scaled_resolve = false; + uint32_t pixel_size_log2 = 0; }; bool PrepareTextureLoad(Texture& texture, PendingTextureLoad& pending_load_out, PendingSharedMemoryRange* pending_ranges_out, size_t& pending_range_count_out); bool CommitPreparedTextureLoad(const PendingTextureLoad& pending_load); + void DebugLogScaledTextureBindings(uint32_t used_texture_mask); + bool DebugRangeOverlapsRecentScaledResolveReadback(uint32_t start, uint32_t length) const; + bool DebugTextureOverlapsRecentResolveReadback(const Texture& texture, bool mips, + uint32_t& readback_serial_out, + bool& readback_scaled_out) const; + static bool DebugIsTeamProfileBackgroundCandidateKey(const TextureKey& key); + void DebugLogTeamProfileBackgroundTextureCandidate(const char* action, uint32_t fetch_index, + const Texture& texture); void UpdateTexturesTotalHostMemoryUsage(uint64_t add, uint64_t subtract); @@ -569,6 +600,8 @@ class TextureCache { // Checks if there are any pages that contain scaled resolve data within the // range. bool IsRangeScaledResolved(uint32_t start_unscaled, uint32_t length_unscaled); + // Checks if every page touched by the range contains scaled resolve data. + bool IsRangeFullyScaledResolved(uint32_t start_unscaled, uint32_t length_unscaled); // Global shared memory invalidation callback for invalidating scaled resolved // texture data. static void ScaledResolveGlobalWatchCallbackThunk( @@ -617,6 +650,32 @@ 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; + + int32_t debug_scaled_texture_bindings_remaining_seen_ = 0; + uint32_t debug_scaled_texture_binding_logs_this_sample_ = 0; + struct DebugResolveReadbackRange { + uint32_t start = 0; + uint32_t length = 0; + uint32_t serial = 0; + bool scaled = false; + }; + std::array debug_recent_resolve_readbacks_; + uint32_t debug_recent_resolve_readback_next_ = 0; + uint32_t debug_recent_resolve_readback_count_ = 0; + uint32_t debug_resolve_readback_serial_ = 0; + struct DebugTeamProfileResolveRange { + uint32_t start = 0; + uint32_t length = 0; + uint32_t serial = 0; + bool scaled = false; + uint64_t scaled_start = 0; + uint64_t scaled_length = 0; + }; + std::array debug_recent_team_profile_resolves_; + uint32_t debug_recent_team_profile_resolve_next_ = 0; + uint32_t debug_recent_team_profile_resolve_count_ = 0; + uint32_t debug_team_profile_resolve_serial_ = 0; + uint32_t debug_team_profile_resolve_texture_logs_remaining_ = 0; }; } // namespace rex::graphics diff --git a/include/rex/graphics/shared_memory.h b/include/rex/graphics/shared_memory.h index b05df6a..ce4f329 100644 --- a/include/rex/graphics/shared_memory.h +++ b/include/rex/graphics/shared_memory.h @@ -80,6 +80,10 @@ class SharedMemory { bool RequestRanges(const std::pair* ranges, size_t count); bool RequestRange(uint32_t start, uint32_t length); + const uint8_t* DebugTranslatePhysical(uint32_t address) const { + return memory_.TranslatePhysical(address); + } + // Marks the range and, if not exact_range, potentially its surroundings // (to up to the first GPU-written page, as an access violation exception // count optimization) as modified by the CPU, also invalidating GPU-written diff --git a/include/rex/graphics/vulkan/command_processor.h b/include/rex/graphics/vulkan/command_processor.h index 40317bd..470bdf2 100644 --- a/include/rex/graphics/vulkan/command_processor.h +++ b/include/rex/graphics/vulkan/command_processor.h @@ -278,6 +278,8 @@ class VulkanCommandProcessor : public CommandProcessor { void InitializeTrace() override; private: + friend class VulkanTextureCache; + struct CommandBuffer { VkCommandPool pool; VkCommandBuffer buffer; @@ -433,9 +435,12 @@ class VulkanCommandProcessor : public CommandProcessor { // Keep primary-buffer-end submit behavior aligned with D3D12: only submit // when immediate submission is safe. bool CanEndSubmissionImmediately() const; + void DebugRecordResolve(uint32_t address, uint32_t length, bool scaled); void ClearTransientDescriptorPools(); bool IssueCopy_ReadbackResolvePath(); + bool DownscaleScaledResolveToSharedMemory(uint32_t start_unscaled, uint32_t length_unscaled, + uint32_t pixel_size_log2); bool IssueDraw_MemexportReadbackFullPath(uint32_t total_size); bool IssueDraw_MemexportReadbackFastPath(uint32_t total_size); @@ -553,6 +558,24 @@ class VulkanCommandProcessor : public CommandProcessor { // Tracks whether any draw in the current frame used an async placeholder // graphics pipeline and may have produced incomplete output. bool frame_used_async_placeholder_pipeline_ = false; + uint32_t async_placeholder_consecutive_frame_skips_ = 0; + bool async_placeholder_skip_fallback_logged_ = false; + uint32_t debug_frame_draws_ = 0; + uint32_t debug_frame_copy_resolves_ = 0; + uint32_t debug_frame_raster_draws_ = 0; + uint32_t debug_frame_color_draws_ = 0; + uint32_t debug_frame_depth_stencil_draws_ = 0; + uint32_t debug_frame_texture_draws_ = 0; + uint32_t debug_frame_placeholder_draws_ = 0; + uint32_t debug_frame_no_effect_draws_ = 0; + uint32_t debug_frame_memexport_draws_ = 0; + uint32_t debug_frame_resolve_ranges_ = 0; + uint32_t debug_frame_scaled_resolve_ranges_ = 0; + uint64_t debug_frame_resolve_bytes_ = 0; + static constexpr uint32_t kDebugFrameResolveRangeSamples = 6; + std::array debug_frame_resolve_addresses_ = {}; + std::array debug_frame_resolve_lengths_ = {}; + std::array debug_frame_resolve_scaled_ = {}; // Guest frame index, since some transient resources can be reused across // submissions. Values updated in the beginning of a frame. uint64_t frame_current_ = 1; diff --git a/include/rex/graphics/vulkan/render_target_cache.h b/include/rex/graphics/vulkan/render_target_cache.h index 0a49681..63dd7ea 100644 --- a/include/rex/graphics/vulkan/render_target_cache.h +++ b/include/rex/graphics/vulkan/render_target_cache.h @@ -108,7 +108,9 @@ class VulkanRenderTargetCache final : public RenderTargetCache { // frame for calling. bool Resolve(const memory::Memory& memory, VulkanSharedMemory& shared_memory, VulkanTextureCache& texture_cache, uint32_t& written_address_out, - uint32_t& written_length_out); + uint32_t& written_length_out, + bool debug_log_team_profile_background_trace = false); + void DebugLogLastUpdateRenderTargetsForTeamProfileBackgroundDraw(uint64_t frame) const; // Returns true if any downloads were submitted to the command processor. bool InitializeTraceSubmitDownloads(); diff --git a/include/rex/graphics/vulkan/texture_cache.h b/include/rex/graphics/vulkan/texture_cache.h index 04856ee..4beeeec 100644 --- a/include/rex/graphics/vulkan/texture_cache.h +++ b/include/rex/graphics/vulkan/texture_cache.h @@ -145,12 +145,19 @@ class VulkanTextureCache final : public TextureCache { bool EnsureScaledResolveMemoryCommitted(uint32_t start_unscaled, uint32_t length_unscaled, uint32_t length_scaled_alignment_log2 = 0) override; + bool MaterializeScaledResolveToSharedMemory(uint32_t start_unscaled, uint32_t length_unscaled, + uint32_t pixel_size_log2) override; bool LoadTextureDataFromResidentMemoryImpl(Texture& texture, bool load_base, bool load_mips) override; void UpdateTextureBindingsImpl(uint32_t fetch_constant_mask) override; + void DebugLogTeamProfileBackgroundGpuEvent(const char* action, uint32_t fetch_index, + const Texture& texture, bool signed_view = false, + uint32_t host_swizzle = 0, + VkImageView image_view = VK_NULL_HANDLE) const; + private: enum LoadDescriptorSetIndex { kLoadDescriptorSetIndexDestination, @@ -201,6 +208,7 @@ class VulkanTextureCache final : public TextureCache { ~VulkanTexture(); VkImage image() const { return image_; } + Usage usage() const { return usage_; } // Doesn't transition (the caller must insert the barrier). Usage SetUsage(Usage new_usage) { @@ -271,6 +279,8 @@ class VulkanTextureCache final : public TextureCache { VkImageView image_view_3d_as_2d_signed_ = VK_NULL_HANDLE; }; + static const char* DebugVulkanTextureUsageName(VulkanTexture::Usage usage); + struct VulkanTextureBinding { VkImageView image_view_unsigned; VkImageView image_view_signed; diff --git a/include/rex/system/kernel_state.h b/include/rex/system/kernel_state.h index 85994b4..55e30ac 100644 --- a/include/rex/system/kernel_state.h +++ b/include/rex/system/kernel_state.h @@ -290,6 +290,9 @@ class KernelState { void UnregisterNotifyListener(XNotifyListener* listener); void BroadcastNotification(XNotificationID id, uint32_t data); + void SetUserContext(uint32_t user_index, uint32_t context_id, uint32_t value); + std::optional GetUserContext(uint32_t user_index, uint32_t context_id) const; + util::NativeList* dpc_list() { return &dpc_list_; } void CompleteOverlapped(uint32_t overlapped_ptr, X_RESULT result); @@ -346,6 +349,9 @@ class KernelState { std::vector> notify_listeners_; bool has_notified_startup_ = false; + mutable std::mutex user_contexts_mutex_; + std::unordered_map user_contexts_; + // Protected by global_critical_region_. std::unordered_map fiber_map_; diff --git a/include/rex/ui/imgui_drawer.h b/include/rex/ui/imgui_drawer.h index c72b628..ace4dd2 100644 --- a/include/rex/ui/imgui_drawer.h +++ b/include/rex/ui/imgui_drawer.h @@ -43,6 +43,7 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer { ~ImGuiDrawer(); ImGuiIO& GetIO(); + bool HasDialogs() const { return !dialogs_.empty(); } void AddDialog(ImGuiDialog* dialog); void RemoveDialog(ImGuiDialog* dialog); diff --git a/src/filesystem/devices/host_path_entry.cpp b/src/filesystem/devices/host_path_entry.cpp index 56f5a97..0befecd 100644 --- a/src/filesystem/devices/host_path_entry.cpp +++ b/src/filesystem/devices/host_path_entry.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -56,6 +57,10 @@ X_STATUS HostPathEntry::Open(uint32_t desired_access, File** out_file) { REXFS_ERROR("Attempting to open file for write access on read-only device"); return X_STATUS_ACCESS_DENIED; } + if (attributes_ & kFileAttributeDirectory) { + *out_file = new NullFile(desired_access, this); + return X_STATUS_SUCCESS; + } auto file_handle = rex::filesystem::FileHandle::OpenExisting(host_path_, desired_access); if (!file_handle) { // TODO(benvanik): pick correct response. diff --git a/src/filesystem/devices/host_path_file.cpp b/src/filesystem/devices/host_path_file.cpp index 4c57a18..0c6b2ff 100644 --- a/src/filesystem/devices/host_path_file.cpp +++ b/src/filesystem/devices/host_path_file.cpp @@ -12,8 +12,63 @@ #include #include +#include +#include +#include + +#include +#include +#include +#include + namespace rex::filesystem { +namespace { + +bool LooksLikeSkaterPreviewAssetPath(std::string_view path) { + std::string lower(path); + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + static constexpr std::string_view kNeedles[] = { + "import_skater", "team_management", "preset", "skater", + "preview", "fedynamic", "fedata", "fetexture", + "createacharacter", "db.big", "data/fe", "data\\fe", + }; + for (std::string_view needle : kNeedles) { + if (lower.find(needle) != std::string::npos) { + return true; + } + } + return false; +} + +bool LooksLikeTeamProfileBackgroundPath(std::string_view path) { + std::string lower(path); + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return lower.find("team_profile_background_0") != std::string::npos; +} + +bool ConsumeFeAssetLogBudget() { + const int32_t remaining = REXCVAR_GET(filesystem_debug_log_fe_asset_ops_remaining); + if (remaining <= 0) { + return false; + } + REXCVAR_SET(filesystem_debug_log_fe_asset_ops_remaining, remaining - 1); + return true; +} + +bool ConsumeTeamProfileBackgroundLogBudget() { + const int32_t remaining = REXCVAR_GET(filesystem_debug_log_team_profile_background_remaining); + if (remaining <= 0) { + return false; + } + REXCVAR_SET(filesystem_debug_log_team_profile_background_remaining, remaining - 1); + return true; +} + +} // namespace + HostPathFile::HostPathFile(uint32_t file_access, HostPathEntry* entry, std::unique_ptr file_handle) : File(file_access, entry), file_handle_(std::move(file_handle)) {} @@ -31,6 +86,20 @@ X_STATUS HostPathFile::ReadSync(std::span buffer, size_t byte_offset, } if (file_handle_->Read(byte_offset, buffer.data(), buffer.size(), out_bytes_read)) { + if (entry_ && LooksLikeTeamProfileBackgroundPath(entry_->absolute_path()) && + ConsumeTeamProfileBackgroundLogBudget()) { + REXFS_WARN( + "Team profile BG diagnostic: op=read, path='{}', offset={:X}, requested={:X}, read={:X}", + entry_->absolute_path(), byte_offset, buffer.size(), + out_bytes_read ? *out_bytes_read : 0); + } + if (entry_ && LooksLikeSkaterPreviewAssetPath(entry_->absolute_path()) && + ConsumeFeAssetLogBudget()) { + REXFS_WARN( + "FE asset diagnostic: op=read, path='{}', offset={:X}, requested={:X}, read={:X}", + entry_->absolute_path(), byte_offset, buffer.size(), + out_bytes_read ? *out_bytes_read : 0); + } return X_STATUS_SUCCESS; } else { return X_STATUS_END_OF_FILE; diff --git a/src/filesystem/devices/null_file.cpp b/src/filesystem/devices/null_file.cpp index 68a63ce..e0b7d5a 100644 --- a/src/filesystem/devices/null_file.cpp +++ b/src/filesystem/devices/null_file.cpp @@ -14,7 +14,7 @@ namespace rex::filesystem { -NullFile::NullFile(uint32_t file_access, NullEntry* entry) : File(file_access, entry) {} +NullFile::NullFile(uint32_t file_access, Entry* entry) : File(file_access, entry) {} NullFile::~NullFile() = default; @@ -23,6 +23,9 @@ void NullFile::Destroy() { } X_STATUS NullFile::ReadSync(std::span buffer, size_t byte_offset, size_t* out_bytes_read) { + (void)buffer; + (void)byte_offset; + (void)out_bytes_read; if (!(file_access_ & FileAccess::kFileReadData)) { return X_STATUS_ACCESS_DENIED; } @@ -32,6 +35,9 @@ X_STATUS NullFile::ReadSync(std::span buffer, size_t byte_offset, size_ X_STATUS NullFile::WriteSync(std::span buffer, size_t byte_offset, size_t* out_bytes_written) { + (void)buffer; + (void)byte_offset; + (void)out_bytes_written; if (!(file_access_ & (FileAccess::kFileWriteData | FileAccess::kFileAppendData))) { return X_STATUS_ACCESS_DENIED; } @@ -40,6 +46,7 @@ X_STATUS NullFile::WriteSync(std::span buffer, size_t byte_offset } X_STATUS NullFile::SetLength(size_t length) { + (void)length; if (!(file_access_ & FileAccess::kFileWriteData)) { return X_STATUS_ACCESS_DENIED; } diff --git a/src/filesystem/virtual_file_system.cpp b/src/filesystem/virtual_file_system.cpp index 1460174..e4bad13 100644 --- a/src/filesystem/virtual_file_system.cpp +++ b/src/filesystem/virtual_file_system.cpp @@ -9,6 +9,10 @@ * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime */ +#include +#include +#include + #include #include #include @@ -20,8 +24,67 @@ REXCVAR_DEFINE_BOOL(allow_game_relative_writes, false, "Filesystem", "relative to game://. Used for " "generating test data to compare with original hardware."); +REXCVAR_DEFINE_INT32(filesystem_debug_log_fe_asset_ops_remaining, 0, "Filesystem", + "Log FE/import-skater-related file opens and reads for this many operations") + .range(0, 10000) + .lifecycle(rex::cvar::Lifecycle::kHotReload) + .debug_only(); + +REXCVAR_DEFINE_INT32(filesystem_debug_log_team_profile_background_remaining, 200, "Filesystem", + "Log team_profile_background_0.rx2 file opens and reads for this many " + "operations") + .range(0, 10000) + .lifecycle(rex::cvar::Lifecycle::kHotReload) + .debug_only(); + namespace rex::filesystem { +namespace { + +bool LooksLikeSkaterPreviewAssetPath(std::string_view path) { + std::string lower(path); + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + static constexpr std::string_view kNeedles[] = { + "import_skater", "team_management", "preset", "skater", + "preview", "fedynamic", "fedata", "fetexture", + "createacharacter", "db.big", "data/fe", "data\\fe", + }; + for (std::string_view needle : kNeedles) { + if (lower.find(needle) != std::string::npos) { + return true; + } + } + return false; +} + +bool LooksLikeTeamProfileBackgroundPath(std::string_view path) { + std::string lower(path); + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return lower.find("team_profile_background_0") != std::string::npos; +} + +bool ConsumeFeAssetLogBudget() { + const int32_t remaining = REXCVAR_GET(filesystem_debug_log_fe_asset_ops_remaining); + if (remaining <= 0) { + return false; + } + REXCVAR_SET(filesystem_debug_log_fe_asset_ops_remaining, remaining - 1); + return true; +} + +bool ConsumeTeamProfileBackgroundLogBudget() { + const int32_t remaining = REXCVAR_GET(filesystem_debug_log_team_profile_background_remaining); + if (remaining <= 0) { + return false; + } + REXCVAR_SET(filesystem_debug_log_team_profile_background_remaining, remaining - 1); + return true; +} + +} // namespace + VirtualFileSystem::VirtualFileSystem() {} VirtualFileSystem::~VirtualFileSystem() { @@ -225,6 +288,15 @@ X_STATUS VirtualFileSystem::OpenFile(Entry* root_entry, const std::string_view p auto file_name = rex::string::utf8_find_name_from_guest_path(path); entry = parent_entry->GetChild(file_name); + if (!entry && !root_entry) { + // Some virtual overlays are registered for a full path that doesn't exist + // under the real parent device. Resolve the full path too so those + // symlinks can satisfy opens like d:\data\scene\. + entry = ResolvePath(path); + if (entry) { + parent_entry = entry->parent(); + } + } } else { entry = !root_entry ? ResolvePath(path) : root_entry->GetChild(path); } @@ -279,10 +351,7 @@ X_STATUS VirtualFileSystem::OpenFile(Entry* root_entry, const std::string_view p desired_access = FileAccess::kGenericRead | FileAccess::kFileReadData; } - bool created = false; if (!entry) { - // Remember that we are creating this new, instead of replacing. - created = true; *out_action = FileAction::kCreated; } else { // May need to delete, if it exists. @@ -327,6 +396,20 @@ X_STATUS VirtualFileSystem::OpenFile(Entry* root_entry, const std::string_view p // Open. auto result = entry->Open(desired_access, out_file); + if (LooksLikeTeamProfileBackgroundPath(path) && ConsumeTeamProfileBackgroundLogBudget()) { + REXFS_WARN( + "Team profile BG diagnostic: op=open, path='{}', access={:#x}, disposition={}, action={}, " + "status={:#x}, entry='{}'", + path, desired_access, static_cast(creation_disposition), static_cast(*out_action), + result, entry ? entry->absolute_path() : std::string_view("")); + } + if (LooksLikeSkaterPreviewAssetPath(path) && ConsumeFeAssetLogBudget()) { + REXFS_WARN( + "FE asset diagnostic: op=open, path='{}', access={:#x}, disposition={}, action={}, " + "status={:#x}, entry='{}'", + path, desired_access, static_cast(creation_disposition), static_cast(*out_action), + result, entry ? entry->absolute_path() : std::string_view("")); + } if (XFAILED(result)) { *out_action = FileAction::kDoesNotExist; } diff --git a/src/graphics/command_processor.cpp b/src/graphics/command_processor.cpp index d6d2279..74424a0 100644 --- a/src/graphics/command_processor.cpp +++ b/src/graphics/command_processor.cpp @@ -42,6 +42,21 @@ REXCVAR_DEFINE_BOOL(clear_memory_page_state, false, "GPU", "Disable for minor CPU overhead reduction, but may break memory coherency.") .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_BOOL(draw_resolution_scaled_texture_offsets, true, "GPU/Shader", + "Scale texture offsets with draw resolution"); + +REXCVAR_DEFINE_BOOL(scaled_resolve_texture_loads_from_shared_memory, false, "GPU/Texture", + "Materialize scaled render-to-texture resolves into guest memory on the GPU " + "before loading them as regular textures") + .lifecycle(rex::cvar::Lifecycle::kHotReload); + +REXCVAR_DEFINE_INT32(scaled_resolve_texture_loads_from_shared_memory_max_pixels, 1152 * 640, + "GPU/Texture", + "Maximum unscaled pixel count for GPU materialization of scaled " + "render-to-texture resolves into guest memory") + .range(1, 8192 * 8192) + .lifecycle(rex::cvar::Lifecycle::kHotReload); + REXCVAR_DEFINE_BOOL(occlusion_query_enable, true, "GPU", "Enable host occlusion query handling") .lifecycle(rex::cvar::Lifecycle::kHotReload); @@ -50,8 +65,9 @@ REXCVAR_DEFINE_STRING(readback_resolve, "none", "GPU", " none: Disable readback (default)\n" " fast: Read previous frame (delayed, copy every frame)\n" " some: Read previous frame (delayed, copy on cache miss)\n" - " full: Immediate sync readback (accurate but stalls)") - .allowed({"none", "fast", "some", "full"}) + " full: Immediate sync readback (accurate but stalls)\n" + " auto: Backend-specific adaptive policy") + .allowed({"none", "fast", "some", "full", "auto"}) .lifecycle(rex::cvar::Lifecycle::kHotReload); REXCVAR_DEFINE_BOOL(readback_resolve_half_pixel_offset, false, "GPU", @@ -59,6 +75,19 @@ REXCVAR_DEFINE_BOOL(readback_resolve_half_pixel_offset, false, "GPU", "scaled block during resolve readback downscale") .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_BOOL(scaled_resolve_texture_loads_update_guest_memory, true, "GPU", + "When scaled resolve texture loads are materialized into shared memory, also " + "mirror the downscaled data into guest memory for CPU-visible coherency") + .lifecycle(rex::cvar::Lifecycle::kHotReload); + +REXCVAR_DEFINE_INT32(scaled_resolve_small_texture_readback_max_length, 0x2D0000, "GPU/Texture", + "Maximum unscaled byte length of a scaled render-to-texture resolve that " + "will be immediately read back even when readback_resolve is disabled. " + "This is intended for small UI thumbnails that depend on CPU-visible " + "resolved data without forcing full-screen resolve readbacks.") + .range(0, 16 * 1024 * 1024) + .lifecycle(rex::cvar::Lifecycle::kHotReload); + REXCVAR_DEFINE_BOOL(readback_memexport, false, "GPU", "Enable CPU readback of shader memexport writes for guest memory " "coherency (can reduce correctness issues, but may add GPU/CPU sync cost)") @@ -96,6 +125,9 @@ ReadbackResolveMode ParseReadbackResolveMode(std::string_view value) { if (value == "full") { return ReadbackResolveMode::kFull; } + if (value == "auto") { + return ReadbackResolveMode::kAuto; + } return ReadbackResolveMode::kDisabled; } diff --git a/src/graphics/d3d12/command_processor.cpp b/src/graphics/d3d12/command_processor.cpp index 02edcd6..bbdabbd 100644 --- a/src/graphics/d3d12/command_processor.cpp +++ b/src/graphics/d3d12/command_processor.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -52,6 +53,14 @@ namespace rex::graphics::d3d12 { namespace { +bool IsGameplayStateActive(const system::KernelState* kernel_state) { + if (!kernel_state) { + return false; + } + auto gameplay_context = kernel_state->GetUserContext(0, 0x8001); + return gameplay_context && *gameplay_context == 1; +} + #ifdef REXGLUE_ENABLE_PERF_COUNTERS class ScopedDrawStageTimer { public: @@ -3134,7 +3143,9 @@ bool D3D12CommandProcessor::IssueCopy() { return false; } ReadbackResolveMode readback_mode = GetReadbackResolveMode(REXCVAR_GET(d3d12_readback_resolve)); - if (readback_mode == ReadbackResolveMode::kDisabled) { + const bool gameplay_state_active = IsGameplayStateActive(kernel_state_); + 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); @@ -3160,6 +3171,20 @@ bool D3D12CommandProcessor::IssueCopy_ReadbackResolvePath() { } bool is_scaled = texture_cache_->IsDrawResolutionScaled(); + ReadbackResolveMode readback_mode = GetReadbackResolveMode(REXCVAR_GET(d3d12_readback_resolve)); + if (readback_mode == ReadbackResolveMode::kDisabled) { + constexpr uint32_t kImportSkaterPreviewResolveAddress = UINT32_C(0x04911000); + constexpr uint32_t kImportSkaterPreviewResolveLength = UINT32_C(0x2D0000); + const bool force_scaled_resolve_cpu_copy = + !IsGameplayStateActive(kernel_state_) && is_scaled && + written_address == kImportSkaterPreviewResolveAddress && + written_length == kImportSkaterPreviewResolveLength; + if (!force_scaled_resolve_cpu_copy) { + return true; + } + readback_mode = ReadbackResolveMode::kFull; + } + uint64_t resolve_key = MakeReadbackResolveKey(written_address, written_length); ReadbackBuffer& rb = readback_buffers_[resolve_key]; rb.last_used_frame = frame_current_; @@ -3327,7 +3352,6 @@ bool D3D12CommandProcessor::IssueCopy_ReadbackResolvePath() { written_address, written_length); } - ReadbackResolveMode readback_mode = GetReadbackResolveMode(REXCVAR_GET(d3d12_readback_resolve)); bool use_delayed_sync = readback_mode == ReadbackResolveMode::kFast || readback_mode == ReadbackResolveMode::kSome; uint32_t read_index = write_index; diff --git a/src/graphics/d3d12/texture_cache.cpp b/src/graphics/d3d12/texture_cache.cpp index 2be0e8e..c5783a6 100644 --- a/src/graphics/d3d12/texture_cache.cpp +++ b/src/graphics/d3d12/texture_cache.cpp @@ -1333,6 +1333,9 @@ bool D3D12TextureCache::MakeScaledResolveRangeCurrent(uint32_t start_unscaled, void D3D12TextureCache::TransitionCurrentScaledResolveRange(D3D12_RESOURCE_STATES new_state) { assert_true(IsDrawResolutionScaled()); ScaledResolveVirtualBuffer& buffer = GetCurrentScaledResolveBuffer(); + if (buffer.ConsumeUAVBarrierPending()) { + command_processor_.PushUAVBarrier(buffer.resource()); + } command_processor_.PushTransitionBarrier(buffer.resource(), buffer.SetResourceState(new_state), new_state); } diff --git a/src/graphics/pipeline/shader/dxbc_translator_fetch.cpp b/src/graphics/pipeline/shader/dxbc_translator_fetch.cpp index 31de175..551ed85 100644 --- a/src/graphics/pipeline/shader/dxbc_translator_fetch.cpp +++ b/src/graphics/pipeline/shader/dxbc_translator_fetch.cpp @@ -24,9 +24,6 @@ #include #include -REXCVAR_DEFINE_BOOL(draw_resolution_scaled_texture_offsets, true, "GPU/Shader", - "Scale texture offsets with draw resolution"); - namespace rex::graphics { using namespace ucode; diff --git a/src/graphics/pipeline/shader/spirv_translator_fetch.cpp b/src/graphics/pipeline/shader/spirv_translator_fetch.cpp index 9ce3bb4..a4fe0ae 100644 --- a/src/graphics/pipeline/shader/spirv_translator_fetch.cpp +++ b/src/graphics/pipeline/shader/spirv_translator_fetch.cpp @@ -19,6 +19,8 @@ #include #include +#include +#include #include #include @@ -975,7 +977,10 @@ void SpirvShaderTranslator::ProcessTextureFetchInstruction( } spv::Id texture_resolution_scaled = spv::NoResult; uint32_t revert_resolution_scale_axes = - uint32_t(draw_resolution_scale_x_ > 1) | (uint32_t(draw_resolution_scale_y_ > 1) << 1); + REXCVAR_GET(draw_resolution_scaled_texture_offsets) + ? uint32_t(draw_resolution_scale_x_ > 1) | + (uint32_t(draw_resolution_scale_y_ > 1) << 1) + : 0; spv::Id const_texture_resolution_scale[] = {spv::NoResult, spv::NoResult}; spv::Id const_texture_resolution_scale_reciprocal[] = {spv::NoResult, spv::NoResult}; if (revert_resolution_scale_axes) { diff --git a/src/graphics/pipeline/texture/cache.cpp b/src/graphics/pipeline/texture/cache.cpp index 734cbdc..c6020c3 100644 --- a/src/graphics/pipeline/texture/cache.cpp +++ b/src/graphics/pipeline/texture/cache.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -129,6 +130,8 @@ REXCVAR_DEFINE_BOOL(pre_mask_resolve_l2_block, true, "GPU", namespace rex::graphics { +constexpr uint64_t kDebugTeamProfileBackgroundHash = UINT64_C(0x2C31D77D35F4EC6F); + const TextureCache::LoadShaderInfo TextureCache::load_shader_info_[kLoadShaderCount] = { // k8bpb {3, 4, 1, 4}, @@ -464,6 +467,7 @@ bool TextureCache::CommitPreparedTextureLoad(const PendingTextureLoad& pending_l // not up to date anymore. texture.MakeUpToDateAndWatch(global_critical_region_.Acquire()); texture.LogAction("Loaded"); + DebugLogTeamProfileBackgroundTextureCandidate("loaded", UINT32_MAX, texture); return true; } @@ -490,6 +494,7 @@ void TextureCache::RequestTextures(uint32_t used_texture_mask) { uint32_t textures_remaining = used_texture_mask & ~texture_bindings_in_sync_; if (!textures_remaining) { + DebugLogScaledTextureBindings(used_texture_mask); return; } @@ -633,6 +638,10 @@ void TextureCache::RequestTextures(uint32_t used_texture_mask) { if (batched_shared_memory_request_succeeded) { for (size_t i = 0; i < pending_texture_load_count; ++i) { + if (pending_texture_loads[i].texture != nullptr) { + DebugLogTeamProfileBackgroundTextureCandidate("commit-load", UINT32_MAX, + *pending_texture_loads[i].texture); + } CommitPreparedTextureLoad(pending_texture_loads[i]); } } else { @@ -653,6 +662,390 @@ void TextureCache::RequestTextures(uint32_t used_texture_mask) { #endif UpdateTextureBindingsImpl(bindings_changed); } + DebugLogScaledTextureBindings(used_texture_mask); +} + +void TextureCache::DebugRecordResolveReadback(uint32_t start, uint32_t length, bool scaled) { + if (!length) { + return; + } + + DebugResolveReadbackRange& range = + debug_recent_resolve_readbacks_[debug_recent_resolve_readback_next_]; + range.start = start; + range.length = length; + range.serial = ++debug_resolve_readback_serial_; + range.scaled = scaled; + debug_recent_resolve_readback_next_ = + (debug_recent_resolve_readback_next_ + 1) % debug_recent_resolve_readbacks_.size(); + debug_recent_resolve_readback_count_ = + std::min(debug_recent_resolve_readback_count_ + 1, + uint32_t(debug_recent_resolve_readbacks_.size())); + + int32_t remaining = REXCVAR_GET(vulkan_debug_log_frame_summaries_remaining); + int32_t interval = std::max(1, REXCVAR_GET(vulkan_debug_frame_summary_interval_frames)); + bool log_this_sample = remaining == 36000 || remaining == 3600 || (remaining % interval) == 0; + if (remaining > 0 && log_this_sample) { + REXGPU_WARN("Vulkan debug resolve readback full copy: serial={}, scaled={}, range={:08X}+{:X}", + range.serial, scaled, start, length); + } +} + +void TextureCache::DebugClearRecentResolveReadbacks() { + for (DebugResolveReadbackRange& range : debug_recent_resolve_readbacks_) { + range = {}; + } + debug_recent_resolve_readback_next_ = 0; + debug_recent_resolve_readback_count_ = 0; +} + +void TextureCache::DebugRecordTeamProfileBackgroundResolveRange(uint32_t start, uint32_t length, + bool scaled, + uint64_t scaled_start, + uint64_t scaled_length) { + if (!length) { + return; + } + + DebugTeamProfileResolveRange& range = + debug_recent_team_profile_resolves_[debug_recent_team_profile_resolve_next_]; + range.start = start; + range.length = length; + range.serial = ++debug_team_profile_resolve_serial_; + range.scaled = scaled; + range.scaled_start = scaled_start; + range.scaled_length = scaled_length; + debug_recent_team_profile_resolve_next_ = + (debug_recent_team_profile_resolve_next_ + 1) % + debug_recent_team_profile_resolves_.size(); + debug_recent_team_profile_resolve_count_ = + std::min(debug_recent_team_profile_resolve_count_ + 1, + uint32_t(debug_recent_team_profile_resolves_.size())); + if (!debug_team_profile_resolve_texture_logs_remaining_) { + debug_team_profile_resolve_texture_logs_remaining_ = 32; + } + + REXGPU_WARN( + "Team profile BG resolve range recorded: serial={}, scaled={}, range={:08X}+{:X}, " + "scaled_range={:X}+{:X}", + range.serial, scaled, start, length, scaled_start, scaled_length); +} + +bool TextureCache::DebugIsTeamProfileBackgroundCandidateKey(const TextureKey& key) { + return key.dimension == xenos::DataDimension::k2DOrStacked && key.GetWidth() == 128 && + key.GetHeight() == 256 && key.GetDepthOrArraySize() == 1 && + key.format == xenos::TextureFormat::k_DXT4_5; +} + +bool TextureCache::DebugIsTeamProfileBackgroundTexture(const Texture& texture) const { + const TextureKey& key = texture.key(); + if (!DebugIsTeamProfileBackgroundCandidateKey(key) || key.scaled_resolve) { + 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) { + return false; + } + uint64_t fnv1a = UINT64_C(14695981039346656037); + for (uint32_t i = 0; i < sample_length; ++i) { + fnv1a ^= data[i]; + fnv1a *= UINT64_C(1099511628211); + } + return fnv1a == kDebugTeamProfileBackgroundHash; +} + +bool TextureCache::DebugFindTeamProfileBackgroundBinding(uint32_t used_texture_mask, + uint32_t& fetch_index_out, + bool& signed_view_out, + uint32_t* texture_base_out, + uint32_t* texture_length_out) const { + uint32_t textures_remaining = used_texture_mask; + uint32_t index = 0; + while (rex::bit_scan_forward(textures_remaining, &index)) { + textures_remaining &= ~(UINT32_C(1) << index); + const TextureBinding* binding = GetValidTextureBinding(index); + if (binding == nullptr) { + continue; + } + if (binding->texture != nullptr && DebugIsTeamProfileBackgroundTexture(*binding->texture)) { + fetch_index_out = index; + signed_view_out = false; + if (texture_base_out != nullptr) { + *texture_base_out = binding->texture->key().base_page << 12; + } + if (texture_length_out != nullptr) { + *texture_length_out = binding->texture->GetGuestBaseSize(); + } + return true; + } + if (binding->texture_signed != nullptr && + DebugIsTeamProfileBackgroundTexture(*binding->texture_signed)) { + fetch_index_out = index; + signed_view_out = true; + if (texture_base_out != nullptr) { + *texture_base_out = binding->texture_signed->key().base_page << 12; + } + if (texture_length_out != nullptr) { + *texture_length_out = binding->texture_signed->GetGuestBaseSize(); + } + return true; + } + } + return false; +} + +void TextureCache::DebugLogTeamProfileBackgroundTextureCandidate(const char* action, + uint32_t fetch_index, + const Texture& texture) { + const TextureKey& key = texture.key(); + if (!DebugIsTeamProfileBackgroundCandidateKey(key)) { + return; + } + const bool is_bind_action = std::string_view(action).starts_with("bind"); + const int32_t remaining = is_bind_action + ? REXCVAR_GET( + vulkan_debug_log_team_profile_background_bindings_remaining) + : REXCVAR_GET( + vulkan_debug_log_team_profile_background_candidates_remaining); + if (remaining <= 0) { + return; + } + if (is_bind_action) { + REXCVAR_SET(vulkan_debug_log_team_profile_background_bindings_remaining, remaining - 1); + } else { + REXCVAR_SET(vulkan_debug_log_team_profile_background_candidates_remaining, remaining - 1); + } + + uint32_t sample_length = 0; + uint32_t nonzero_count = 0; + uint64_t fnv1a = UINT64_C(14695981039346656037); + uint8_t head[8] = {}; + if (!is_bind_action) { + const uint8_t* data = shared_memory().DebugTranslatePhysical(key.base_page << 12); + sample_length = std::min(texture.GetGuestBaseSize(), UINT32_C(0x8000)); + if (data != nullptr && sample_length != 0) { + for (uint32_t i = 0; i < sample_length; ++i) { + uint8_t value = data[i]; + nonzero_count += value != 0; + fnv1a ^= value; + fnv1a *= UINT64_C(1099511628211); + } + std::memcpy(head, data, std::min(sizeof(head), sample_length)); + } + } + + REXGPU_WARN( + "Team profile BG texture candidate: action={}, fetch={}, scaled={}, {} {}x{}x{} {}, " + "base={:08X}+{:X}, mips={:08X}+{:X}, pitch={}, mip_max={}, packed_mips={}, " + "sample_len={:X}, nonzero={}, hash={:016X}, head={:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}", + action, fetch_index, bool(key.scaled_resolve), key.tiled ? "tiled" : "linear", + key.GetWidth(), key.GetHeight(), key.GetDepthOrArraySize(), + FormatInfo::Get(key.format)->name, key.base_page << 12, texture.GetGuestBaseSize(), + key.mip_page << 12, texture.GetGuestMipsSize(), key.pitch << 5, key.mip_max_level, + bool(key.packed_mips), sample_length, nonzero_count, fnv1a, head[0], head[1], head[2], + head[3], head[4], head[5], head[6], head[7]); +} + +bool TextureCache::DebugTextureOverlapsRecentResolveReadback( + const Texture& texture, bool mips, uint32_t& readback_serial_out, + bool& readback_scaled_out) const { + const TextureKey& key = texture.key(); + uint32_t texture_start = (mips ? key.mip_page : key.base_page) << 12; + uint32_t texture_length = mips ? texture.GetGuestMipsSize() : texture.GetGuestBaseSize(); + if (!texture_length) { + return false; + } + uint64_t texture_end = uint64_t(texture_start) + texture_length; + for (uint32_t i = 0; i < debug_recent_resolve_readback_count_; ++i) { + const DebugResolveReadbackRange& readback = debug_recent_resolve_readbacks_[i]; + if (!readback.length) { + continue; + } + uint64_t readback_end = uint64_t(readback.start) + readback.length; + if (uint64_t(texture_start) < readback_end && uint64_t(readback.start) < texture_end) { + readback_serial_out = readback.serial; + readback_scaled_out = readback.scaled; + return true; + } + } + return false; +} + +bool TextureCache::DebugRangeOverlapsRecentScaledResolveReadback(uint32_t start, + uint32_t length) const { + if (!length) { + return false; + } + uint64_t end = uint64_t(start) + length; + for (uint32_t i = 0; i < debug_recent_resolve_readback_count_; ++i) { + const DebugResolveReadbackRange& readback = debug_recent_resolve_readbacks_[i]; + if (!readback.length || !readback.scaled) { + continue; + } + uint64_t readback_end = uint64_t(readback.start) + readback.length; + if (uint64_t(start) < readback_end && uint64_t(readback.start) < end) { + return true; + } + } + return false; +} + +bool TextureCache::DebugTextureOverlapsRecentTeamProfileBackgroundResolve( + const Texture& texture, bool mips, uint32_t& resolve_serial_out, bool& resolve_scaled_out, + uint64_t& resolve_scaled_start_out, uint64_t& resolve_scaled_length_out) const { + const TextureKey& key = texture.key(); + uint32_t texture_start = (mips ? key.mip_page : key.base_page) << 12; + uint32_t texture_length = mips ? texture.GetGuestMipsSize() : texture.GetGuestBaseSize(); + if (!texture_length) { + return false; + } + uint64_t texture_end = uint64_t(texture_start) + texture_length; + bool found = false; + for (uint32_t i = 0; i < debug_recent_team_profile_resolve_count_; ++i) { + const DebugTeamProfileResolveRange& resolve = debug_recent_team_profile_resolves_[i]; + if (!resolve.length) { + continue; + } + uint64_t resolve_end = uint64_t(resolve.start) + resolve.length; + if (uint64_t(texture_start) < resolve_end && uint64_t(resolve.start) < texture_end) { + if (found && resolve.serial <= resolve_serial_out) { + continue; + } + resolve_serial_out = resolve.serial; + resolve_scaled_out = resolve.scaled; + resolve_scaled_start_out = resolve.scaled_start; + resolve_scaled_length_out = resolve.scaled_length; + found = true; + } + } + return found; +} + +void TextureCache::DebugLogScaledTextureBindings(uint32_t used_texture_mask) { + if (used_texture_mask == 0) { + return; + } + + if (REXCVAR_GET(vulkan_debug_log_team_profile_background_candidates_remaining) > 0 || + REXCVAR_GET(vulkan_debug_log_team_profile_background_bindings_remaining) > 0) { + uint32_t textures_remaining = used_texture_mask; + uint32_t index = 0; + while (rex::bit_scan_forward(textures_remaining, &index)) { + textures_remaining &= ~(UINT32_C(1) << index); + const TextureBinding* binding = GetValidTextureBinding(index); + if (binding == nullptr) { + continue; + } + if (binding->texture) { + DebugLogTeamProfileBackgroundTextureCandidate("bind", index, *binding->texture); + } + if (binding->texture_signed) { + DebugLogTeamProfileBackgroundTextureCandidate("bind-signed", index, + *binding->texture_signed); + } + } + } + + int32_t remaining = REXCVAR_GET(vulkan_debug_log_frame_summaries_remaining); + bool log_team_profile_overlaps = debug_team_profile_resolve_texture_logs_remaining_ != 0; + if (remaining <= 0 && !log_team_profile_overlaps) { + return; + } + + int32_t interval = std::max(1, REXCVAR_GET(vulkan_debug_frame_summary_interval_frames)); + bool log_this_sample = remaining == 36000 || remaining == 3600 || (remaining % interval) == 0; + if (remaining <= 0) { + log_this_sample = false; + } + if (!log_this_sample && !log_team_profile_overlaps) { + return; + } + + if (remaining != debug_scaled_texture_bindings_remaining_seen_) { + debug_scaled_texture_bindings_remaining_seen_ = remaining; + debug_scaled_texture_binding_logs_this_sample_ = 0; + } + constexpr uint32_t kMaxScaledTextureBindingLogsPerSample = 64; + if (debug_scaled_texture_binding_logs_this_sample_ >= kMaxScaledTextureBindingLogsPerSample) { + return; + } + + auto log_texture = [&](uint32_t fetch_index, const char* view_name, const Texture* texture) { + if (texture == nullptr || + debug_scaled_texture_binding_logs_this_sample_ >= kMaxScaledTextureBindingLogsPerSample || + (!log_this_sample && debug_team_profile_resolve_texture_logs_remaining_ == 0)) { + return; + } + const TextureKey& key = texture->key(); + uint32_t base_readback_serial = 0; + uint32_t mips_readback_serial = 0; + bool base_readback_scaled = false; + bool mips_readback_scaled = false; + bool overlaps_base_readback = DebugTextureOverlapsRecentResolveReadback( + *texture, false, base_readback_serial, base_readback_scaled); + bool overlaps_mips_readback = DebugTextureOverlapsRecentResolveReadback( + *texture, true, mips_readback_serial, mips_readback_scaled); + uint32_t base_team_profile_serial = 0; + uint32_t mips_team_profile_serial = 0; + bool base_team_profile_scaled = false; + bool mips_team_profile_scaled = false; + uint64_t base_team_profile_scaled_start = 0; + uint64_t base_team_profile_scaled_length = 0; + uint64_t mips_team_profile_scaled_start = 0; + uint64_t mips_team_profile_scaled_length = 0; + bool overlaps_base_team_profile = DebugTextureOverlapsRecentTeamProfileBackgroundResolve( + *texture, false, base_team_profile_serial, base_team_profile_scaled, + base_team_profile_scaled_start, base_team_profile_scaled_length); + bool overlaps_mips_team_profile = DebugTextureOverlapsRecentTeamProfileBackgroundResolve( + *texture, true, mips_team_profile_serial, mips_team_profile_scaled, + mips_team_profile_scaled_start, mips_team_profile_scaled_length); + if (!key.scaled_resolve && !overlaps_base_readback && !overlaps_mips_readback && + !overlaps_base_team_profile && !overlaps_mips_team_profile) { + return; + } + if (!log_this_sample && !(overlaps_base_team_profile || overlaps_mips_team_profile)) { + return; + } + ++debug_scaled_texture_binding_logs_this_sample_; + if (overlaps_base_team_profile || overlaps_mips_team_profile) { + --debug_team_profile_resolve_texture_logs_remaining_; + } + REXGPU_WARN( + "Vulkan debug texture binding: remaining={}, fetch={}, view={}, scaled={}, {} {}x{}x{} " + "{}, base={:08X}+{:X}, mips={:08X}+{:X}, pitch={}, host={}x{}, readback_base=#{} {}, " + "readback_mips=#{} {}, team_profile_base=#{} {} {:X}+{:X}, " + "team_profile_mips=#{} {} {:X}+{:X}", + remaining, fetch_index, view_name, bool(key.scaled_resolve), + key.tiled ? "tiled" : "linear", key.GetWidth(), key.GetHeight(), + key.GetDepthOrArraySize(), FormatInfo::Get(key.format)->name, + key.base_page << 12, texture->GetGuestBaseSize(), key.mip_page << 12, + texture->GetGuestMipsSize(), key.pitch << 5, + key.GetWidth() * (key.scaled_resolve ? draw_resolution_scale_x_ : 1), + key.GetHeight() * (key.scaled_resolve ? draw_resolution_scale_y_ : 1), + base_readback_serial, overlaps_base_readback ? (base_readback_scaled ? "scaled" : "1x") + : "none", + mips_readback_serial, overlaps_mips_readback ? (mips_readback_scaled ? "scaled" : "1x") + : "none", + base_team_profile_serial, + overlaps_base_team_profile ? (base_team_profile_scaled ? "scaled" : "1x") : "none", + base_team_profile_scaled_start, base_team_profile_scaled_length, + mips_team_profile_serial, + overlaps_mips_team_profile ? (mips_team_profile_scaled ? "scaled" : "1x") : "none", + mips_team_profile_scaled_start, mips_team_profile_scaled_length); + }; + + uint32_t textures_remaining = used_texture_mask; + uint32_t index = 0; + while (rex::bit_scan_forward(textures_remaining, &index)) { + textures_remaining &= ~(UINT32_C(1) << index); + const TextureBinding* binding = GetValidTextureBinding(index); + if (binding == nullptr) { + continue; + } + log_texture(index, "unsigned", binding->texture); + log_texture(index, "signed", binding->texture_signed); + } } const char* TextureCache::TextureKey::GetLogDimensionName(xenos::DataDimension dimension) { @@ -795,7 +1188,12 @@ void TextureCache::Texture::MarkAsUsed() { } void TextureCache::Texture::WatchCallback( - [[maybe_unused]] const std::unique_lock& global_lock, bool is_mip) { + [[maybe_unused]] const std::unique_lock& global_lock, bool is_mip, + bool invalidated_by_gpu) { + texture_cache().DebugLogTeamProfileBackgroundTextureCandidate( + invalidated_by_gpu ? (is_mip ? "invalidate-gpu-mips" : "invalidate-gpu-base") + : (is_mip ? "invalidate-cpu-mips" : "invalidate-cpu-base"), + UINT32_MAX, *this); if (is_mip) { assert_not_zero(GetGuestMipsSize()); mips_outdated_ = true; @@ -813,7 +1211,7 @@ void TextureCache::WatchCallback(const std::unique_lock& g void* context, void* data, uint64_t argument, bool invalidated_by_gpu) { Texture& texture = *static_cast(context); - texture.WatchCallback(global_lock, argument != 0); + texture.WatchCallback(global_lock, argument != 0, invalidated_by_gpu); texture.texture_cache().texture_became_outdated_.store(true, std::memory_order_release); } @@ -923,6 +1321,8 @@ TextureCache::Texture* TextureCache::FindOrCreateTexture(TextureKey key) { // previously 0, now not 0, to save memory - common case in streaming. auto found_texture_it = textures_.find(key); if (found_texture_it != textures_.end()) { + DebugLogTeamProfileBackgroundTextureCandidate("reuse", UINT32_MAX, + *found_texture_it->second); return found_texture_it->second.get(); } @@ -939,6 +1339,7 @@ TextureCache::Texture* TextureCache::FindOrCreateTexture(TextureKey key) { } COUNT_profile_set("gpu/texture_cache/textures", textures_.size()); texture->LogAction("Created"); + DebugLogTeamProfileBackgroundTextureCandidate("create", UINT32_MAX, *texture); return texture; } @@ -958,6 +1359,7 @@ bool TextureCache::LoadTextureData(Texture& texture) { return false; } + DebugLogTeamProfileBackgroundTextureCandidate("load", UINT32_MAX, texture); return CommitPreparedTextureLoad(pending_load); } @@ -1125,6 +1527,37 @@ bool TextureCache::IsRangeScaledResolved(uint32_t start_unscaled, uint32_t lengt return false; } +bool TextureCache::IsRangeFullyScaledResolved(uint32_t start_unscaled, uint32_t length_unscaled) { + if (!IsDrawResolutionScaled()) { + return false; + } + + start_unscaled = std::min(start_unscaled, SharedMemory::kBufferSize); + length_unscaled = std::min(length_unscaled, SharedMemory::kBufferSize - start_unscaled); + if (!length_unscaled) { + return false; + } + + uint32_t page_first = start_unscaled >> 12; + uint32_t page_last = (start_unscaled + length_unscaled - 1) >> 12; + uint32_t block_first = page_first >> 5; + uint32_t block_last = page_last >> 5; + auto global_lock = global_critical_region_.Acquire(); + for (uint32_t block_index = block_first; block_index <= block_last; ++block_index) { + uint32_t check_bits = UINT32_MAX; + if (block_index == block_first) { + check_bits &= ~((UINT32_C(1) << (page_first & 31)) - 1); + } + if (block_index == block_last && (page_last & 31) != 31) { + check_bits &= (UINT32_C(1) << ((page_last & 31) + 1)) - 1; + } + if ((scaled_resolve_pages_[block_index] & check_bits) != check_bits) { + return false; + } + } + return true; +} + void TextureCache::ScaledResolveGlobalWatchCallbackThunk( const std::unique_lock& global_lock, void* context, uint32_t address_first, uint32_t address_last, bool invalidated_by_gpu) { diff --git a/src/graphics/vulkan/command_processor.cpp b/src/graphics/vulkan/command_processor.cpp index 195c9ef..a92f0ab 100644 --- a/src/graphics/vulkan/command_processor.cpp +++ b/src/graphics/vulkan/command_processor.cpp @@ -11,13 +11,17 @@ #include #include +#include #include #include +#include +#include #include #include #include #include #include +#include #include #include @@ -42,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -62,6 +67,80 @@ REXCVAR_DEFINE_BOOL(vulkan_async_skip_incomplete_frames, true, "GPU/Vulkan", "used placeholder pipelines to avoid visible flashing") .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_INT32(vulkan_async_placeholder_max_consecutive_skips, 120, "GPU/Vulkan", + "Maximum consecutive frames to skip because they used async placeholder " + "pipelines before presenting incomplete output anyway (0 for unlimited)") + .range(0, 3600) + .lifecycle(rex::cvar::Lifecycle::kHotReload); + +REXCVAR_DEFINE_INT32(vulkan_debug_log_frame_summaries_remaining, 0, "GPU/Vulkan", + "Track Vulkan per-frame draw summaries for this many frames") + .range(0, 36000) + .lifecycle(rex::cvar::Lifecycle::kHotReload) + .debug_only(); + +REXCVAR_DEFINE_INT32(vulkan_debug_frame_summary_interval_frames, 30, "GPU/Vulkan", + "Frame interval for Vulkan debug frame summary logging") + .range(1, 600) + .lifecycle(rex::cvar::Lifecycle::kHotReload) + .debug_only(); + +REXCVAR_DEFINE_INT32(vulkan_debug_log_resolve_decisions_remaining, 0, "GPU/Vulkan", + "Log Vulkan render-target resolve/readback decisions for this many copy " + "commands") + .range(0, 10000) + .lifecycle(rex::cvar::Lifecycle::kHotReload) + .debug_only(); + +REXCVAR_DEFINE_INT32(vulkan_debug_log_team_profile_background_candidates_remaining, 0, + "GPU/Vulkan", + "Log texture lifecycle events matching team_profile_background_0.rx2") + .range(0, 10000) + .lifecycle(rex::cvar::Lifecycle::kHotReload) + .debug_only(); + +REXCVAR_DEFINE_INT32(vulkan_debug_log_team_profile_background_bindings_remaining, 0, + "GPU/Vulkan", + "Log texture binding events matching team_profile_background_0.rx2") + .range(0, 10000) + .lifecycle(rex::cvar::Lifecycle::kHotReload) + .debug_only(); + +REXCVAR_DEFINE_INT32(vulkan_debug_log_team_profile_background_draws_remaining, 0, + "GPU/Vulkan", + "Log draws sampling the exact team_profile_background_0.rx2 texture") + .range(0, 10000) + .lifecycle(rex::cvar::Lifecycle::kHotReload) + .debug_only(); + +REXCVAR_DEFINE_BOOL(vulkan_sync_after_resolve, false, "GPU/Vulkan", + "Synchronously wait for Vulkan render-target resolves without CPU readback") + .lifecycle(rex::cvar::Lifecycle::kHotReload) + .debug_only(); + +REXCVAR_DEFINE_BOOL(vulkan_barrier_after_scaled_resolve, true, "GPU/Vulkan", + "Submit a GPU memory barrier after scaled Vulkan render-target resolves so " + "follow-up texture loads can read the scaled resolve buffer without CPU " + "readback") + .lifecycle(rex::cvar::Lifecycle::kHotReload); + +REXCVAR_DEFINE_INT32(vulkan_readback_resolve_max_length, 0, "GPU/Vulkan", + "Maximum resolve byte length eligible for Vulkan CPU readback " + "(0 disables the length filter)") + .range(0, 0x7FFFFFFF) + .lifecycle(rex::cvar::Lifecycle::kHotReload); + +REXCVAR_DEFINE_INT32(vulkan_readback_resolve_min_address, 0, "GPU/Vulkan", + "Minimum guest address eligible for Vulkan CPU resolve readback") + .range(0, 0x7FFFFFFF) + .lifecycle(rex::cvar::Lifecycle::kHotReload); + +REXCVAR_DEFINE_INT32(vulkan_readback_resolve_max_address, 0, "GPU/Vulkan", + "Maximum exclusive guest address eligible for Vulkan CPU resolve readback " + "(0 disables the address filter)") + .range(0, 0x7FFFFFFF) + .lifecycle(rex::cvar::Lifecycle::kHotReload); + 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); @@ -79,6 +158,58 @@ namespace rex::graphics::vulkan { namespace { +const char* ReadbackResolveModeName(ReadbackResolveMode mode) { + switch (mode) { + case ReadbackResolveMode::kDisabled: + return "none"; + case ReadbackResolveMode::kFast: + return "fast"; + case ReadbackResolveMode::kSome: + return "some"; + case ReadbackResolveMode::kFull: + return "full"; + case ReadbackResolveMode::kAuto: + return "auto"; + } + return "unknown"; +} + +bool IsGameplayStateActive(const system::KernelState* kernel_state) { + if (!kernel_state) { + return false; + } + auto gameplay_context = kernel_state->GetUserContext(0, 0x8001); + return gameplay_context && *gameplay_context == 1; +} + +bool IsInGameContextActive(const system::KernelState* kernel_state) { + if (!kernel_state) { + return false; + } + auto title_context = kernel_state->GetUserContext(0, 0x0); + return title_context && *title_context != 0; +} + +ReadbackResolveMode ResolveAutoReadbackMode(ReadbackResolveMode mode, + const system::KernelState* kernel_state) { + if (mode != ReadbackResolveMode::kAuto) { + return mode; + } + + return (!IsGameplayStateActive(kernel_state) && !IsInGameContextActive(kernel_state)) + ? ReadbackResolveMode::kFull + : ReadbackResolveMode::kDisabled; +} + +bool ConsumeResolveDecisionLogBudget() { + const int32_t remaining = REXCVAR_GET(vulkan_debug_log_resolve_decisions_remaining); + if (remaining <= 0) { + return false; + } + REXCVAR_SET(vulkan_debug_log_resolve_decisions_remaining, remaining - 1); + return true; +} + // glslang default built-in resource limits. constexpr TBuiltInResource kGlslangDefaultTBuiltInResource = { /* .maxLights = */ 32, @@ -188,6 +319,60 @@ constexpr TBuiltInResource kGlslangDefaultTBuiltInResource = { }, }; +void DebugDumpResolveReadback(uint32_t address, uint32_t length, bool scaled, const void* data) { + if (!data || !length || REXCVAR_GET(vulkan_debug_log_frame_summaries_remaining) <= 0) { + return; + } + + constexpr uint32_t kMaxDumps = 96; + static std::mutex dump_mutex; + static std::unordered_set dumped_ranges; + static uint32_t dump_count = 0; + + const uint64_t key = (uint64_t(address) << 32) | length; + std::lock_guard lock(dump_mutex); + if (dump_count >= kMaxDumps || !dumped_ranges.insert(key).second) { + return; + } + + std::error_code ec; + const std::filesystem::path dump_dir = std::filesystem::path("logs") / "texture_dumps" / "raw"; + std::filesystem::create_directories(dump_dir, ec); + if (ec) { + REXGPU_WARN("Failed to create Vulkan resolve readback dump directory: {}", ec.message()); + return; + } + + char filename[160]; + std::snprintf(filename, sizeof(filename), "resolve_%03u_addr_%08X_len_%X_%s.bin", dump_count, + address, length, scaled ? "scaled" : "unscaled"); + const std::filesystem::path dump_path = dump_dir / filename; + + std::ofstream dump_file(dump_path, std::ios::binary | std::ios::trunc); + if (!dump_file) { + REXGPU_WARN("Failed to open Vulkan resolve readback dump '{}'", dump_path.string()); + return; + } + dump_file.write(reinterpret_cast(data), length); + if (!dump_file) { + REXGPU_WARN("Failed to write Vulkan resolve readback dump '{}'", dump_path.string()); + return; + } + + const std::filesystem::path manifest_path = + std::filesystem::path("logs") / "texture_dumps" / "manifest.csv"; + std::ofstream manifest_file(manifest_path, std::ios::app); + if (manifest_file) { + manifest_file << dump_count << ",0x" << std::hex << std::uppercase << address << ",0x" + << length << "," << (scaled ? "scaled" : "unscaled") << "," << filename + << "\n"; + } + + REXGPU_INFO("Dumped Vulkan resolve readback texture {}: address={:08X}, length={:X}, scaled={}", + dump_path.string(), address, length, scaled); + ++dump_count; +} + const char* GetSwapFxaaComputeSource(bool extreme_quality) { return extreme_quality ? R"(#version 450 layout(local_size_x = 16, local_size_y = 8, local_size_z = 1) in; @@ -2313,16 +2498,75 @@ void VulkanCommandProcessor::IssueSwap(uint32_t frontbuffer_ptr, uint32_t frontb bool skip_present_due_async_placeholder = REXCVAR_GET(async_shader_compilation) && REXCVAR_GET(vulkan_async_skip_incomplete_frames) && frame_used_async_placeholder_pipeline_; - if (skip_present_due_async_placeholder) { - static bool skipped_incomplete_frame_logged = false; - if (!skipped_incomplete_frame_logged) { - skipped_incomplete_frame_logged = true; - REXGPU_WARN( - "Skipping Vulkan frame presentation due to async placeholder draw " - "usage in this frame"); + auto log_debug_frame_summary = [&]() { + int32_t remaining = REXCVAR_GET(vulkan_debug_log_frame_summaries_remaining); + if (remaining <= 0) { + return; } - EndSubmission(true); - return; + const int32_t interval = + std::max(1, REXCVAR_GET(vulkan_debug_frame_summary_interval_frames)); + const bool should_log = remaining == 36000 || remaining == 3600 || + (remaining % interval) == 0 || + skip_present_due_async_placeholder || + debug_frame_placeholder_draws_ != 0; + if (should_log) { + REXGPU_WARN( + "Vulkan debug frame summary: frame={}, remaining={}, frontbuffer={:08X} {}x{}, " + "draws={}, copy_resolves={}, raster={}, color={}, depth_stencil={}, textured={}, " + "memexport={}, no_effect={}, placeholder={}, resolve_ranges={}, " + "scaled_resolve_ranges={}, resolve_bytes={}, " + "resolve0={:08X}+{:X}{}, resolve1={:08X}+{:X}{}, resolve2={:08X}+{:X}{}, " + "resolve3={:08X}+{:X}{}, resolve4={:08X}+{:X}{}, resolve5={:08X}+{:X}{}, " + "skip_present={}", + frame_current_, remaining, frontbuffer_ptr, frontbuffer_width, frontbuffer_height, + debug_frame_draws_, debug_frame_copy_resolves_, debug_frame_raster_draws_, + debug_frame_color_draws_, debug_frame_depth_stencil_draws_, debug_frame_texture_draws_, + debug_frame_memexport_draws_, debug_frame_no_effect_draws_, + debug_frame_placeholder_draws_, debug_frame_resolve_ranges_, + debug_frame_scaled_resolve_ranges_, debug_frame_resolve_bytes_, + debug_frame_resolve_addresses_[0], debug_frame_resolve_lengths_[0], + debug_frame_resolve_scaled_[0] ? "s" : "", + debug_frame_resolve_addresses_[1], debug_frame_resolve_lengths_[1], + debug_frame_resolve_scaled_[1] ? "s" : "", + debug_frame_resolve_addresses_[2], debug_frame_resolve_lengths_[2], + debug_frame_resolve_scaled_[2] ? "s" : "", + debug_frame_resolve_addresses_[3], debug_frame_resolve_lengths_[3], + debug_frame_resolve_scaled_[3] ? "s" : "", + debug_frame_resolve_addresses_[4], debug_frame_resolve_lengths_[4], + debug_frame_resolve_scaled_[4] ? "s" : "", + debug_frame_resolve_addresses_[5], debug_frame_resolve_lengths_[5], + debug_frame_resolve_scaled_[5] ? "s" : "", skip_present_due_async_placeholder); + } + REXCVAR_SET(vulkan_debug_log_frame_summaries_remaining, remaining - 1); + }; + log_debug_frame_summary(); + if (skip_present_due_async_placeholder) { + const uint32_t max_consecutive_skips = uint32_t(std::max( + 0, REXCVAR_GET(vulkan_async_placeholder_max_consecutive_skips))); + if (!max_consecutive_skips || + async_placeholder_consecutive_frame_skips_ < max_consecutive_skips) { + ++async_placeholder_consecutive_frame_skips_; + static bool skipped_incomplete_frame_logged = false; + if (!skipped_incomplete_frame_logged) { + skipped_incomplete_frame_logged = true; + REXGPU_WARN( + "Skipping Vulkan frame presentation due to async placeholder draw " + "usage in this frame"); + } + EndSubmission(true); + return; + } + + if (!async_placeholder_skip_fallback_logged_) { + async_placeholder_skip_fallback_logged_ = true; + REXGPU_WARN( + "Presenting Vulkan frames with async placeholder draws after {} consecutive " + "skips to avoid a persistent black screen", + async_placeholder_consecutive_frame_skips_); + } + } else { + async_placeholder_consecutive_frame_skips_ = 0; + async_placeholder_skip_fallback_logged_ = false; } SwapPostEffect swap_post_effect = GetActualSwapPostEffect(); @@ -3619,6 +3863,10 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t const RegisterFile& regs = *register_file_; (void)index_buffer_info; + const bool debug_log_frame = REXCVAR_GET(vulkan_debug_log_frame_summaries_remaining) > 0; + if (debug_log_frame) { + ++debug_frame_draws_; + } auto draw_fail = [&](const char* stage) { auto vgt_draw_initiator = regs.Get(); REXGPU_ERROR( @@ -3636,6 +3884,9 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t xenos::EdramMode edram_mode = regs.Get().edram_mode; if (edram_mode == xenos::EdramMode::kCopy) { // Special copy handling. + if (debug_log_frame) { + ++debug_frame_copy_resolves_; + } rex::perf::ScopedCounterTimer resolve_timer(rex::perf::CounterId::kRtResolveUs); const uint32_t primitive_count = draw_util::EstimatePrimitiveCount(prim_type, index_count); PROFILE_DRAW_BUCKET(rex::perf::DrawBucket::kCopyResolve, index_count, primitive_count); @@ -3688,6 +3939,9 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t if (surface_pitch_is_zero && is_rasterization_done) { // Doesn't actually draw. // Unlikely that zero would even really be legal though. + if (debug_log_frame) { + ++debug_frame_no_effect_draws_; + } return true; } VulkanShader* pixel_shader = nullptr; @@ -3708,6 +3962,9 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t // cache. if (!memexport_used_vertex) { // This draw has no effect. + if (debug_log_frame) { + ++debug_frame_no_effect_draws_; + } return true; } } @@ -3758,6 +4015,9 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t } if (!primitive_processing_result.host_draw_vertex_count) { // Nothing to draw. + if (debug_log_frame) { + ++debug_frame_no_effect_draws_; + } return true; } if (primitive_processing_result.host_primitive_type == xenos::PrimitiveType::kTriangleFan) { @@ -3876,6 +4136,9 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t if (REXCVAR_GET(vulkan_skip_inert_no_pixel_draws) && pixel_shader == nullptr && normalized_color_mask == 0 && !normalized_depth_control.z_enable && !normalized_depth_control.stencil_enable && !active_occlusion_query_.valid) { + if (debug_log_frame) { + ++debug_frame_no_effect_draws_; + } return true; } @@ -3885,10 +4148,34 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t uint32_t used_texture_mask = vertex_shader->GetUsedTextureMaskAfterTranslation() | (pixel_shader != nullptr ? pixel_shader->GetUsedTextureMaskAfterTranslation() : 0); + uint32_t debug_team_profile_bg_fetch_index = UINT32_MAX; + bool debug_team_profile_bg_signed_view = false; + uint32_t debug_team_profile_bg_texture_base = 0; + uint32_t debug_team_profile_bg_texture_length = 0; + if (debug_log_frame) { + if (is_rasterization_done) { + ++debug_frame_raster_draws_; + } + if (normalized_color_mask) { + ++debug_frame_color_draws_; + } + if (normalized_depth_control.z_enable || normalized_depth_control.stencil_enable) { + ++debug_frame_depth_stencil_draws_; + } + if (used_texture_mask) { + ++debug_frame_texture_draws_; + } + if (!memexport_ranges_.empty()) { + ++debug_frame_memexport_draws_; + } + } { 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); const VulkanPipelineCache::PipelineLayoutProvider* pipeline_layout_provider; // Set up the render targets - this may perform dispatches and draws. @@ -3922,6 +4209,9 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t &pipeline_is_placeholder); if (REXCVAR_GET(async_shader_compilation) && pipeline_is_placeholder) { frame_used_async_placeholder_pipeline_ = true; + if (debug_log_frame) { + ++debug_frame_placeholder_draws_; + } return true; } if (pipeline == VK_NULL_HANDLE || pipeline_layout_provider == nullptr) { @@ -3998,6 +4288,35 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t UpdateDynamicState(viewport_info, primitive_polygonal, normalized_depth_control); } + if (debug_team_profile_bg_draw) { + int32_t remaining = REXCVAR_GET(vulkan_debug_log_team_profile_background_draws_remaining); + if (remaining > 0) { + REXCVAR_SET(vulkan_debug_log_team_profile_background_draws_remaining, remaining - 1); + render_target_cache_->DebugLogLastUpdateRenderTargetsForTeamProfileBackgroundDraw( + frame_current_); + const auto rb_surface_info = regs.Get(); + const auto pa_sc_window_scissor_tl = regs.Get(); + const auto pa_sc_window_scissor_br = regs.Get(); + REXGPU_WARN( + "Team profile BG Vulkan draw trace: frame={}, fetch={}, signed_view={}, " + "prim={}, index_count={}, host_vertices={}, used_mask={:08X}, vs={:016X}, " + "ps={:016X}, color_mask={:X}, depth={}, stencil={}, edram_mode={}, " + "surface_pitch={}, viewport={}x{}+{},{} z={}..{}, scissor={}..{},{}..{}, " + "texture_base={:08X}+{:X}", + frame_current_, debug_team_profile_bg_fetch_index, debug_team_profile_bg_signed_view, + uint32_t(prim_type), index_count, primitive_processing_result.host_draw_vertex_count, + used_texture_mask, vertex_shader->ucode_data_hash(), + pixel_shader != nullptr ? pixel_shader->ucode_data_hash() : 0, normalized_color_mask, + bool(normalized_depth_control.z_enable), bool(normalized_depth_control.stencil_enable), + uint32_t(edram_mode), rb_surface_info.surface_pitch, viewport_info.xy_extent[0], + viewport_info.xy_extent[1], viewport_info.xy_offset[0], viewport_info.xy_offset[1], + viewport_info.z_min, viewport_info.z_max, pa_sc_window_scissor_tl.tl_x, + pa_sc_window_scissor_tl.tl_y, pa_sc_window_scissor_br.br_x, + pa_sc_window_scissor_br.br_y, debug_team_profile_bg_texture_base, + debug_team_profile_bg_texture_length); + } + } + auto vgt_draw_initiator = regs.Get(); // Whether to load the guest 32-bit (usually big-endian) vertex index @@ -4527,16 +4846,294 @@ bool VulkanCommandProcessor::IssueCopy() { return false; } - ReadbackResolveMode readback_mode = GetReadbackResolveMode(REXCVAR_GET(vulkan_readback_resolve)); - if (readback_mode == ReadbackResolveMode::kDisabled) { - uint32_t written_address, written_length; - return render_target_cache_->Resolve(*memory_, *shared_memory_, *texture_cache_, - written_address, written_length); + ReadbackResolveMode readback_mode = + ResolveAutoReadbackMode(GetReadbackResolveMode(REXCVAR_GET(vulkan_readback_resolve)), + kernel_state_); + const bool gameplay_state_active = IsGameplayStateActive(kernel_state_); + if (readback_mode == ReadbackResolveMode::kDisabled && + (!texture_cache_->IsDrawResolutionScaled() || gameplay_state_active)) { + uint32_t written_address = 0; + uint32_t written_length = 0; + bool resolved = render_target_cache_->Resolve(*memory_, *shared_memory_, *texture_cache_, + written_address, written_length); + const bool scaled = texture_cache_->IsDrawResolutionScaled(); + if (ConsumeResolveDecisionLogBudget()) { + REXGPU_WARN( + "Vulkan resolve diagnostic: frame={}, mode={}, path=resolve-only, result={}, " + "address={:08X}, length={:X}, scaled={}", + frame_current_, ReadbackResolveModeName(readback_mode), resolved, written_address, + written_length, scaled); + } + if (resolved) { + DebugRecordResolve(written_address, written_length, scaled); + } + return resolved; } return IssueCopy_ReadbackResolvePath(); } +bool VulkanCommandProcessor::DownscaleScaledResolveToSharedMemory(uint32_t start_unscaled, + uint32_t length_unscaled, + uint32_t pixel_size_log2) { + if (!texture_cache_ || !shared_memory_ || !resolve_downscale_pipeline_ || + !resolve_downscale_pipeline_layout_ || !length_unscaled || pixel_size_log2 > 3) { + return false; + } + + uint32_t pixel_size = uint32_t(1) << pixel_size_log2; + uint32_t tile_size_1x = 32 * 32 * pixel_size; + uint32_t tile_count = length_unscaled / tile_size_1x; + if (!tile_count || tile_count * tile_size_1x != length_unscaled) { + return false; + } + + if (!texture_cache_->CommitScaledResolveRange(start_unscaled, length_unscaled, 0)) { + return false; + } + + uint64_t scaled_start = 0; + uint64_t scaled_length = 0; + if (!texture_cache_->GetScaledResolveRange(start_unscaled, length_unscaled, 0, scaled_start, + scaled_length) || + !scaled_length || scaled_start > uint64_t(UINT32_MAX)) { + return false; + } + + VkBuffer scaled_resolve_buffer = texture_cache_->scaled_resolve_buffer(); + if (scaled_resolve_buffer == VK_NULL_HANDLE) { + return false; + } + + const bool update_guest_memory = false; + const uint32_t downscale_buffer_size = AlignReadbackBufferSize(length_unscaled); + const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice(); + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); + const VkDevice device = vulkan_device->device(); + if (downscale_buffer_size > resolve_downscale_buffer_size_) { + VkBuffer new_buffer = VK_NULL_HANDLE; + VkDeviceMemory new_memory = VK_NULL_HANDLE; + if (!ui::vulkan::util::CreateDedicatedAllocationBuffer( + vulkan_device, downscale_buffer_size, + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, + ui::vulkan::util::MemoryPurpose::kDeviceLocal, new_buffer, new_memory)) { + REXGPU_ERROR("Failed to create a {} MB Vulkan resolve downscale buffer", + downscale_buffer_size >> 20); + return false; + } + if (resolve_downscale_buffer_ != VK_NULL_HANDLE || + resolve_downscale_buffer_memory_ != VK_NULL_HANDLE) { + if (!AwaitAllQueueOperationsCompletion()) { + dfn.vkDestroyBuffer(device, new_buffer, nullptr); + dfn.vkFreeMemory(device, new_memory, nullptr); + return false; + } + if (resolve_downscale_buffer_ != VK_NULL_HANDLE) { + dfn.vkDestroyBuffer(device, resolve_downscale_buffer_, nullptr); + } + if (resolve_downscale_buffer_memory_ != VK_NULL_HANDLE) { + dfn.vkFreeMemory(device, resolve_downscale_buffer_memory_, nullptr); + } + } + resolve_downscale_buffer_ = new_buffer; + resolve_downscale_buffer_memory_ = new_memory; + resolve_downscale_buffer_size_ = downscale_buffer_size; + } + if (resolve_downscale_buffer_ == VK_NULL_HANDLE) { + return false; + } + + ReadbackBuffer* readback = nullptr; + uint32_t readback_write_index = 0; + if (update_guest_memory) { + auto ensure_readback_slot = [&](ReadbackBuffer& readback_buffer, uint32_t index, + uint32_t size) -> bool { + if (readback_buffer.buffers[index] != VK_NULL_HANDLE && + size <= readback_buffer.sizes[index] && readback_buffer.mapped_data[index] != nullptr) { + return true; + } + + VkBuffer new_buffer = VK_NULL_HANDLE; + VkDeviceMemory new_memory = VK_NULL_HANDLE; + if (!ui::vulkan::util::CreateDedicatedAllocationBuffer( + vulkan_device, size, VK_BUFFER_USAGE_TRANSFER_DST_BIT, + ui::vulkan::util::MemoryPurpose::kReadback, new_buffer, new_memory)) { + REXGPU_ERROR("Failed to create a {} MB Vulkan materialized resolve readback buffer", + size >> 20); + return false; + } + + void* new_mapping = nullptr; + if (dfn.vkMapMemory(device, new_memory, 0, VK_WHOLE_SIZE, 0, &new_mapping) != VK_SUCCESS) { + REXGPU_ERROR("Failed to map a Vulkan materialized resolve readback buffer"); + dfn.vkDestroyBuffer(device, new_buffer, nullptr); + dfn.vkFreeMemory(device, new_memory, nullptr); + return false; + } + + if (readback_buffer.buffers[index] != VK_NULL_HANDLE || + readback_buffer.memories[index] != VK_NULL_HANDLE) { + if (!AwaitAllQueueOperationsCompletion()) { + dfn.vkUnmapMemory(device, new_memory); + dfn.vkDestroyBuffer(device, new_buffer, nullptr); + dfn.vkFreeMemory(device, new_memory, nullptr); + return false; + } + if (readback_buffer.mapped_data[index] != nullptr && + readback_buffer.memories[index] != VK_NULL_HANDLE) { + dfn.vkUnmapMemory(device, readback_buffer.memories[index]); + } + if (readback_buffer.buffers[index] != VK_NULL_HANDLE) { + dfn.vkDestroyBuffer(device, readback_buffer.buffers[index], nullptr); + } + if (readback_buffer.memories[index] != VK_NULL_HANDLE) { + dfn.vkFreeMemory(device, readback_buffer.memories[index], nullptr); + } + } + + readback_buffer.buffers[index] = new_buffer; + readback_buffer.memories[index] = new_memory; + readback_buffer.mapped_data[index] = new_mapping; + readback_buffer.sizes[index] = size; + return true; + }; + + uint64_t readback_key = + MakeReadbackResolveKey(start_unscaled, length_unscaled) ^ UINT64_C(0x8000000000000000); + readback = &readback_buffers_[readback_key]; + readback->last_used_frame = frame_current_; + readback_write_index = readback->current_index; + if (!ensure_readback_slot(*readback, readback_write_index, downscale_buffer_size)) { + readback = nullptr; + } + } + + VkDescriptorSet descriptor_set = AllocateSingleTransientDescriptor( + SingleTransientDescriptorLayout::kStorageBufferPairCompute); + if (descriptor_set == VK_NULL_HANDLE) { + return false; + } + + VkDescriptorBufferInfo buffer_infos[2] = {}; + buffer_infos[0].buffer = scaled_resolve_buffer; + buffer_infos[0].offset = 0; + buffer_infos[0].range = VK_WHOLE_SIZE; + buffer_infos[1].buffer = resolve_downscale_buffer_; + buffer_infos[1].offset = 0; + buffer_infos[1].range = length_unscaled; + + VkWriteDescriptorSet descriptor_writes[2] = {}; + for (uint32_t i = 0; i < 2; ++i) { + descriptor_writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptor_writes[i].dstSet = descriptor_set; + descriptor_writes[i].dstBinding = i; + descriptor_writes[i].descriptorCount = 1; + descriptor_writes[i].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptor_writes[i].pBufferInfo = &buffer_infos[i]; + } + dfn.vkUpdateDescriptorSets(device, 2, descriptor_writes, 0, nullptr); + + texture_cache_->UseScaledResolveBufferForRead(); + SubmitBarriers(true); + + uint32_t resolve_downscale_timestamp_start = + BeginGpuTimestampedRegion(rex::perf::DrawBucket::kResolveDownscale); + + VkBufferMemoryBarrier pre_barrier = {}; + pre_barrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + pre_barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + pre_barrier.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + pre_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + pre_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + pre_barrier.buffer = resolve_downscale_buffer_; + pre_barrier.offset = 0; + pre_barrier.size = length_unscaled; + deferred_command_buffer_.CmdVkPipelineBarrier( + VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 1, &pre_barrier, 0, nullptr); + + BindExternalComputePipeline(resolve_downscale_pipeline_); + + ResolveDownscaleConstants constants; + constants.scale_x = texture_cache_->draw_resolution_scale_x(); + constants.scale_y = texture_cache_->draw_resolution_scale_y(); + constants.pixel_size_log2 = pixel_size_log2; + constants.tile_count = tile_count; + constants.source_offset_bytes = uint32_t(scaled_start); + constants.half_pixel_offset = + (REXCVAR_GET(readback_resolve_half_pixel_offset) && + (constants.scale_x > 1 || constants.scale_y > 1)) + ? 1u + : 0u; + deferred_command_buffer_.CmdVkPushConstants(resolve_downscale_pipeline_layout_, + VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(constants), + &constants); + deferred_command_buffer_.CmdVkBindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, + resolve_downscale_pipeline_layout_, 0, 1, + &descriptor_set, 0, nullptr); + deferred_command_buffer_.CmdVkDispatch(tile_count, 1, 1); + + VkBufferMemoryBarrier downscale_barrier = {}; + downscale_barrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + downscale_barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT; + downscale_barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + downscale_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + downscale_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + downscale_barrier.buffer = resolve_downscale_buffer_; + downscale_barrier.offset = 0; + downscale_barrier.size = length_unscaled; + deferred_command_buffer_.CmdVkPipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, + &downscale_barrier, 0, nullptr); + + shared_memory_->Use(VulkanSharedMemory::Usage::kTransferDestination, + std::make_pair(start_unscaled, length_unscaled)); + SubmitBarriers(true); + + VkBufferCopy shared_memory_copy = {}; + shared_memory_copy.srcOffset = 0; + shared_memory_copy.dstOffset = start_unscaled; + shared_memory_copy.size = length_unscaled; + deferred_command_buffer_.CmdVkCopyBuffer(resolve_downscale_buffer_, shared_memory_->buffer(), 1, + &shared_memory_copy); + + if (readback != nullptr && readback->buffers[readback_write_index] != VK_NULL_HANDLE) { + VkBufferCopy readback_copy = {}; + readback_copy.srcOffset = 0; + readback_copy.dstOffset = 0; + readback_copy.size = length_unscaled; + deferred_command_buffer_.CmdVkCopyBuffer(resolve_downscale_buffer_, + readback->buffers[readback_write_index], 1, + &readback_copy); + PushBufferMemoryBarrier(readback->buffers[readback_write_index], 0, length_unscaled, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + } + + EndGpuTimestampedDraw(resolve_downscale_timestamp_start); + if (readback != nullptr && readback->buffers[readback_write_index] != VK_NULL_HANDLE && + readback->mapped_data[readback_write_index] != nullptr) { + if (!AwaitAllQueueOperationsCompletion()) { + return false; + } + VkMappedMemoryRange readback_memory_range = {}; + readback_memory_range.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; + readback_memory_range.memory = readback->memories[readback_write_index]; + readback_memory_range.offset = 0; + readback_memory_range.size = VK_WHOLE_SIZE; + dfn.vkInvalidateMappedMemoryRanges(device, 1, &readback_memory_range); + + if (uint8_t* destination = memory_->TranslatePhysical(start_unscaled)) { + std::memcpy(destination, readback->mapped_data[readback_write_index], length_unscaled); + } + readback->current_index = 1 - readback->current_index; + } else if (!AwaitAllQueueOperationsCompletion()) { + return false; + } + shared_memory_->RangeWrittenByGpu(start_unscaled, length_unscaled); + return true; +} + bool VulkanCommandProcessor::IssueCopy_ReadbackResolvePath() { const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice(); const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); @@ -4545,22 +5142,93 @@ bool VulkanCommandProcessor::IssueCopy_ReadbackResolvePath() { uint32_t written_address, written_length; if (!render_target_cache_->Resolve(*memory_, *shared_memory_, *texture_cache_, written_address, written_length)) { + if (ConsumeResolveDecisionLogBudget()) { + REXGPU_WARN("Vulkan resolve diagnostic: frame={}, mode=readback, path=readback, result=false", + frame_current_); + } return false; } + bool is_scaled = texture_cache_->IsDrawResolutionScaled(); + DebugRecordResolve(written_address, written_length, is_scaled); + + auto log_readback_decision = [&](const char* decision, ReadbackResolveMode mode, + bool cache_miss = false, bool should_copy = false, + uint32_t extra = 0) { + if (!ConsumeResolveDecisionLogBudget()) { + return; + } + REXGPU_WARN( + "Vulkan resolve diagnostic: frame={}, mode={}, path=readback, decision={}, " + "address={:08X}, length={:X}, scaled={}, cache_miss={}, should_copy={}, extra={:X}", + frame_current_, ReadbackResolveModeName(mode), decision, written_address, written_length, + is_scaled, cache_miss, should_copy, extra); + }; if (!written_length) { + log_readback_decision("empty-range", ReadbackResolveMode::kDisabled); return true; } if (!memory_->TranslatePhysical(written_address)) { + log_readback_decision("unmapped-destination", ReadbackResolveMode::kDisabled); return true; } - ReadbackResolveMode readback_mode = GetReadbackResolveMode(REXCVAR_GET(vulkan_readback_resolve)); + ReadbackResolveMode readback_mode = + ResolveAutoReadbackMode(GetReadbackResolveMode(REXCVAR_GET(vulkan_readback_resolve)), + kernel_state_); + bool force_scaled_resolve_sync_only = false; + bool force_scaled_resolve_cpu_copy = false; if (readback_mode == ReadbackResolveMode::kDisabled) { + constexpr uint32_t kImportSkaterPreviewResolveAddress = UINT32_C(0x04911000); + constexpr uint32_t kImportSkaterPreviewResolveLength = UINT32_C(0x2D0000); + force_scaled_resolve_cpu_copy = + !IsGameplayStateActive(kernel_state_) && is_scaled && + written_address == kImportSkaterPreviewResolveAddress && + written_length == kImportSkaterPreviewResolveLength; + if (!force_scaled_resolve_cpu_copy) { + log_readback_decision("disabled-after-resolve", readback_mode); + return true; + } + readback_mode = ReadbackResolveMode::kFull; + force_scaled_resolve_sync_only = true; + log_readback_decision("targeted-scaled-resolve-full", readback_mode, false, true, + kImportSkaterPreviewResolveLength); + } + + const int32_t max_readback_length = REXCVAR_GET(vulkan_readback_resolve_max_length); + if (max_readback_length > 0 && written_length > uint32_t(max_readback_length)) { + log_readback_decision("length-filter", readback_mode, false, false, + uint32_t(max_readback_length)); + if (REXCVAR_GET(vulkan_debug_log_frame_summaries_remaining) > 0) { + REXGPU_WARN( + "Vulkan resolve readback skipped by length filter: address={:08X}, length={:X}, " + "max={:X}", + written_address, written_length, uint32_t(max_readback_length)); + } return true; } + const int32_t max_readback_address = REXCVAR_GET(vulkan_readback_resolve_max_address); + if (max_readback_address > 0) { + const uint32_t min_readback_address = + uint32_t(std::max(0, REXCVAR_GET(vulkan_readback_resolve_min_address))); + const uint64_t readback_end = uint64_t(written_address) + written_length; + if (written_address < min_readback_address || + readback_end > uint32_t(max_readback_address)) { + log_readback_decision("address-filter", readback_mode, false, false, + uint32_t(max_readback_address)); + if (REXCVAR_GET(vulkan_debug_log_frame_summaries_remaining) > 0) { + REXGPU_WARN( + "Vulkan resolve readback skipped by address filter: address={:08X}, length={:X}, " + "window={:08X}-{:08X}", + written_address, written_length, min_readback_address, + uint32_t(max_readback_address)); + } + return true; + } + } + auto ensure_readback_slot = [&](ReadbackBuffer& readback, uint32_t index, uint32_t size) -> bool { if (readback.buffers[index] != VK_NULL_HANDLE && size <= readback.sizes[index] && readback.mapped_data[index] != nullptr) { @@ -4609,7 +5277,6 @@ bool VulkanCommandProcessor::IssueCopy_ReadbackResolvePath() { return true; }; - bool is_scaled = texture_cache_->IsDrawResolutionScaled(); uint64_t resolve_key = MakeReadbackResolveKey(written_address, written_length); ReadbackBuffer& readback = readback_buffers_[resolve_key]; readback.last_used_frame = frame_current_; @@ -4810,7 +5477,11 @@ bool VulkanCommandProcessor::IssueCopy_ReadbackResolvePath() { } } - bool should_copy = (readback_mode == ReadbackResolveMode::kSome) ? is_cache_miss : true; + bool should_copy = + (!force_scaled_resolve_sync_only || force_scaled_resolve_cpu_copy) && + ((readback_mode == ReadbackResolveMode::kSome) ? is_cache_miss : true); + log_readback_decision(should_copy ? "copy-guest-memory" : "cache-hit-skip", readback_mode, + is_cache_miss, should_copy, uint32_t(resolve_key)); if (should_copy && readback.buffers[read_index] != VK_NULL_HANDLE && written_length <= readback.sizes[read_index] && readback.mapped_data[read_index] != nullptr) { VkMappedMemoryRange readback_memory_range = {}; @@ -4823,6 +5494,11 @@ bool VulkanCommandProcessor::IssueCopy_ReadbackResolvePath() { uint8_t* destination = memory_->TranslatePhysical(written_address); if (destination) { std::memcpy(destination, readback.mapped_data[read_index], written_length); + if (readback_mode == ReadbackResolveMode::kFull) { + texture_cache_->DebugRecordResolveReadback(written_address, written_length, is_scaled); + DebugDumpResolveReadback(written_address, written_length, is_scaled, + readback.mapped_data[read_index]); + } } } @@ -5594,6 +6270,23 @@ bool VulkanCommandProcessor::CanEndSubmissionImmediately() const { return !submission_open_ || (!scratch_buffer_used_ && !pipeline_cache_->IsCreatingPipelines()); } +void VulkanCommandProcessor::DebugRecordResolve(uint32_t address, uint32_t length, bool scaled) { + if (REXCVAR_GET(vulkan_debug_log_frame_summaries_remaining) <= 0 || !length) { + return; + } + const uint32_t sample_index = debug_frame_resolve_ranges_; + if (sample_index < kDebugFrameResolveRangeSamples) { + debug_frame_resolve_addresses_[sample_index] = address; + debug_frame_resolve_lengths_[sample_index] = length; + debug_frame_resolve_scaled_[sample_index] = scaled; + } + ++debug_frame_resolve_ranges_; + if (scaled) { + ++debug_frame_scaled_resolve_ranges_; + } + debug_frame_resolve_bytes_ += length; +} + bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) { #if XE_GPU_FINE_GRAINED_DRAW_SCOPES SCOPE_profile_cpu_f("gpu"); @@ -5680,6 +6373,21 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) { if (is_opening_frame) { frame_open_ = true; frame_used_async_placeholder_pipeline_ = false; + debug_frame_draws_ = 0; + debug_frame_copy_resolves_ = 0; + debug_frame_raster_draws_ = 0; + debug_frame_color_draws_ = 0; + debug_frame_depth_stencil_draws_ = 0; + debug_frame_texture_draws_ = 0; + debug_frame_placeholder_draws_ = 0; + debug_frame_no_effect_draws_ = 0; + debug_frame_memexport_draws_ = 0; + debug_frame_resolve_ranges_ = 0; + debug_frame_scaled_resolve_ranges_ = 0; + debug_frame_resolve_bytes_ = 0; + debug_frame_resolve_addresses_.fill(0); + debug_frame_resolve_lengths_.fill(0); + debug_frame_resolve_scaled_.fill(false); // Reset bindings that depend on transient data. std::memset(current_float_constant_map_vertex_, 0, sizeof(current_float_constant_map_vertex_)); diff --git a/src/graphics/vulkan/render_target_cache.cpp b/src/graphics/vulkan/render_target_cache.cpp index 9103252..b8630db 100644 --- a/src/graphics/vulkan/render_target_cache.cpp +++ b/src/graphics/vulkan/render_target_cache.cpp @@ -1336,10 +1336,40 @@ void VulkanRenderTargetCache::RestoreEdramSnapshot(const void* snapshot) { } } +void VulkanRenderTargetCache::DebugLogLastUpdateRenderTargetsForTeamProfileBackgroundDraw( + uint64_t frame) const { + REXGPU_WARN( + "Team profile BG Vulkan RT trace: frame={}, path={}, render_pass_key={:08X}, " + "pitch_tiles_32bpp={}, framebuffer={}x{}", + frame, uint32_t(GetPath()), last_update_render_pass_key_.key, + last_update_framebuffer_pitch_tiles_at_32bpp_, + last_update_framebuffer_ ? last_update_framebuffer_->host_extent.width : 0, + last_update_framebuffer_ ? last_update_framebuffer_->host_extent.height : 0); + for (uint32_t i = 0; i < 1 + xenos::kMaxColorRenderTargets; ++i) { + const RenderTarget* rt = + reinterpret_cast(last_update_framebuffer_attachments_[i]); + if (rt == nullptr) { + continue; + } + const RenderTargetKey key = rt->key(); + const VulkanRenderTarget* vulkan_rt = static_cast(rt); + REXGPU_WARN( + "Team profile BG Vulkan RT attachment: slot={}, kind={}, base_tiles={}, pitch_tiles={}, " + "pitch_tiles_32bpp={}, width={}, format={}, msaa={}, image={:016X}, stage={:X}, " + "access={:X}, layout={}", + i, key.is_depth ? "depth" : "color", key.base_tiles, key.GetPitchTiles(), + key.pitch_tiles_at_32bpp, key.GetWidth(), key.GetFormatName(), uint32_t(key.msaa_samples), + uint64_t(uintptr_t(vulkan_rt->image())), uint32_t(vulkan_rt->current_stage_mask()), + uint32_t(vulkan_rt->current_access_mask()), uint32_t(vulkan_rt->current_layout())); + } +} + bool VulkanRenderTargetCache::Resolve(const memory::Memory& memory, VulkanSharedMemory& shared_memory, VulkanTextureCache& texture_cache, - uint32_t& written_address_out, uint32_t& written_length_out) { + uint32_t& written_address_out, + uint32_t& written_length_out, + bool debug_log_team_profile_background_trace) { written_address_out = 0; written_length_out = 0; @@ -1422,6 +1452,21 @@ bool VulkanRenderTargetCache::Resolve(const memory::Memory& memory, } } + if (debug_log_team_profile_background_trace) { + REXGPU_WARN( + "Team profile BG Vulkan resolve trace: scaled={}, copy_shader={}, dest_base={:08X}, " + "dest_extent={:08X}+{:X}, dest_range_unscaled={:X}, buffer_offset={:X}, " + "buffer_range={:X}, use_range={:X}+{:X}, groups={}x{}, dest_bpe_log2={}", + draw_resolution_scaled, uint32_t(copy_shader), resolve_info.copy_dest_base, + resolve_info.copy_dest_extent_start, resolve_info.copy_dest_extent_length, + copy_dest_range_unscaled, copy_dest_base, copy_dest_range_length, + copy_dest_use_start, copy_dest_use_length, copy_group_count_x, copy_group_count_y, + copy_shader_info.dest_bpe_log2); + texture_cache.DebugRecordTeamProfileBackgroundResolveRange( + resolve_info.copy_dest_base, copy_dest_range_unscaled, draw_resolution_scaled, + copy_dest_base, copy_dest_range_length); + } + // Make sure there is memory to write to. bool copy_dest_committed; if (draw_resolution_scaled) { @@ -1465,7 +1510,7 @@ bool VulkanRenderTargetCache::Resolve(const memory::Memory& memory, // Submit the resolve. if (draw_resolution_scaled) { - texture_cache.UseScaledResolveBufferForWrite(copy_dest_use_start, copy_dest_use_length); + texture_cache.UseScaledResolveBufferForWrite(copy_dest_base, copy_dest_range_length); } else { shared_memory.Use(VulkanSharedMemory::Usage::kComputeWrite, std::pair(uint32_t(copy_dest_use_start), diff --git a/src/graphics/vulkan/shared_memory.cpp b/src/graphics/vulkan/shared_memory.cpp index b52ae15..e0c3603 100644 --- a/src/graphics/vulkan/shared_memory.cpp +++ b/src/graphics/vulkan/shared_memory.cpp @@ -411,7 +411,7 @@ void VulkanSharedMemory::GetUsageMasks(Usage usage, VkPipelineStageFlags& stage_ switch (usage) { case Usage::kComputeWrite: stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; - access_mask = VK_ACCESS_SHADER_READ_BIT; + access_mask = VK_ACCESS_SHADER_WRITE_BIT; return; case Usage::kTransferDestination: stage_mask = VK_PIPELINE_STAGE_TRANSFER_BIT; diff --git a/src/graphics/vulkan/texture_cache.cpp b/src/graphics/vulkan/texture_cache.cpp index 371feab..13785b6 100644 --- a/src/graphics/vulkan/texture_cache.cpp +++ b/src/graphics/vulkan/texture_cache.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -40,8 +41,27 @@ 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", + "Log scaled 3D-as-2D texture wrapper usage") + .range(0, 10000) + .lifecycle(rex::cvar::Lifecycle::kHotReload) + .debug_only(); + namespace rex::graphics::vulkan { +namespace { + +bool Consume3DAs2DLogBudget() { + const int32_t remaining = REXCVAR_GET(vulkan_debug_log_3d_as_2d_remaining); + if (remaining <= 0) { + return false; + } + REXCVAR_SET(vulkan_debug_log_3d_as_2d_remaining, remaining - 1); + return true; +} + +} // namespace + // Generated with `xb buildshaders`. namespace shaders { #include "../shaders/vulkan_spirv/texture_load_128bpb_cs.h" @@ -649,6 +669,15 @@ VkImageView VulkanTextureCache::GetActiveBindingOrNullImageView(uint32_t fetch_c if (texture) { image_view = static_cast(texture)->GetOrCreate3DAs2DImageView( use_signed, binding->host_swizzle); + if (Consume3DAs2DLogBudget()) { + const TextureKey& key = texture->key(); + REXGPU_WARN( + "Vulkan 3D-as-2D binding: fetch={}, signed={}, scaled={}, {}x{}x{}, format={}, " + "view={:016X}", + fetch_constant_index, use_signed, bool(key.scaled_resolve), key.GetWidth(), + key.GetHeight(), key.GetDepthOrArraySize(), FormatInfo::Get(key.format)->name, + uint64_t(uintptr_t(image_view))); + } } } else { const VulkanTextureBinding& vulkan_binding = vulkan_texture_bindings_[fetch_constant_index]; @@ -1214,10 +1243,70 @@ bool VulkanTextureCache::EnsureScaledResolveMemoryCommitted(uint32_t start_unsca return EnsureScaledResolveBufferAllocated(start_scaled, length_scaled); } +bool VulkanTextureCache::MaterializeScaledResolveToSharedMemory(uint32_t start_unscaled, + uint32_t length_unscaled, + uint32_t pixel_size_log2) { + return command_processor_.DownscaleScaledResolveToSharedMemory(start_unscaled, length_unscaled, + pixel_size_log2); +} + +const char* VulkanTextureCache::DebugVulkanTextureUsageName(VulkanTexture::Usage usage) { + switch (usage) { + case VulkanTexture::Usage::kUndefined: + return "undefined"; + case VulkanTexture::Usage::kTransferDestination: + return "transfer-dst"; + case VulkanTexture::Usage::kGuestShaderSampled: + return "guest-sampled"; + case VulkanTexture::Usage::kSwapSampled: + return "swap-sampled"; + } + return "unknown"; +} + +void VulkanTextureCache::DebugLogTeamProfileBackgroundGpuEvent( + const char* action, uint32_t fetch_index, const Texture& texture, bool signed_view, + uint32_t host_swizzle, VkImageView image_view) const { + if (!DebugIsTeamProfileBackgroundTexture(texture)) { + return; + } + const bool is_binding_event = std::string_view(action).starts_with("binding") || + std::string_view(action).starts_with("view"); + const int32_t remaining = is_binding_event + ? REXCVAR_GET( + vulkan_debug_log_team_profile_background_bindings_remaining) + : REXCVAR_GET( + vulkan_debug_log_team_profile_background_candidates_remaining); + if (remaining <= 0) { + return; + } + if (is_binding_event) { + REXCVAR_SET(vulkan_debug_log_team_profile_background_bindings_remaining, remaining - 1); + } else { + REXCVAR_SET(vulkan_debug_log_team_profile_background_candidates_remaining, remaining - 1); + } + + const VulkanTexture& vulkan_texture = static_cast(texture); + const TextureKey& key = texture.key(); + const HostFormatPair& host_format_pair = GetHostFormatPair(key); + const VkFormat unsigned_format = host_format_pair.format_unsigned.format; + const VkFormat signed_format = host_format_pair.format_signed.format; + REXGPU_WARN( + "Team profile BG Vulkan texture trace: action={}, fetch={}, signed_view={}, scaled={}, " + "base={:08X}+{:X}, mips={:08X}+{:X}, image={:016X}, view={:016X}, usage={}, " + "host_swizzle={:03X}, host_formats=({}, {}), host_swizzle_pair={:03X}", + action, fetch_index, signed_view, bool(key.scaled_resolve), key.base_page << 12, + texture.GetGuestBaseSize(), key.mip_page << 12, texture.GetGuestMipsSize(), + uint64_t(uintptr_t(vulkan_texture.image())), uint64_t(uintptr_t(image_view)), + DebugVulkanTextureUsageName(vulkan_texture.usage()), host_swizzle, uint32_t(unsigned_format), + uint32_t(signed_format), host_format_pair.swizzle); +} + bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, bool load_base, bool load_mips) { VulkanTexture& vulkan_texture = static_cast(texture); TextureKey texture_key = vulkan_texture.key(); + DebugLogTeamProfileBackgroundGpuEvent("load-enter", UINT32_MAX, texture); // Get the pipeline. const HostFormatPair& host_format_pair = GetHostFormatPair(texture_key); @@ -1415,6 +1504,10 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, VkDescriptorSet descriptor_set_source_mips = VK_NULL_HANDLE; VkDescriptorBufferInfo write_descriptor_set_source_base_buffer_info; VkDescriptorBufferInfo write_descriptor_set_source_mips_buffer_info; + uint64_t debug_source_base_start = 0; + 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) { descriptor_set_source_base = command_processor_.AllocateSingleTransientDescriptor( VulkanCommandProcessor::SingleTransientDescriptorLayout ::kStorageBufferCompute); @@ -1440,6 +1533,8 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, } write_descriptor_set_source_base_buffer_info.buffer = vulkan_shared_memory.buffer(); } + debug_source_base_start = source_base_start; + debug_source_base_range = source_base_range; write_descriptor_set_source_base_buffer_info.offset = source_base_start; write_descriptor_set_source_base_buffer_info.range = source_base_range; VkWriteDescriptorSet& write_descriptor_set_source_base = @@ -1480,6 +1575,8 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, } write_descriptor_set_source_mips_buffer_info.buffer = vulkan_shared_memory.buffer(); } + debug_source_mips_start = source_mips_start; + debug_source_mips_range = source_mips_range; write_descriptor_set_source_mips_buffer_info.offset = source_mips_start; write_descriptor_set_source_mips_buffer_info.range = source_mips_range; VkWriteDescriptorSet& write_descriptor_set_source_mips = @@ -1499,6 +1596,55 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, dfn.vkUpdateDescriptorSets(device, write_descriptor_set_count, write_descriptor_sets.data(), 0, nullptr); } + uint32_t debug_team_profile_base_serial = 0; + uint32_t debug_team_profile_mips_serial = 0; + bool debug_team_profile_base_scaled = false; + bool debug_team_profile_mips_scaled = false; + uint64_t debug_team_profile_base_scaled_start = 0; + uint64_t debug_team_profile_base_scaled_length = 0; + uint64_t debug_team_profile_mips_scaled_start = 0; + uint64_t debug_team_profile_mips_scaled_length = 0; + bool debug_team_profile_base_overlap = + DebugTextureOverlapsRecentTeamProfileBackgroundResolve( + texture, false, debug_team_profile_base_serial, debug_team_profile_base_scaled, + debug_team_profile_base_scaled_start, debug_team_profile_base_scaled_length); + bool debug_team_profile_mips_overlap = + DebugTextureOverlapsRecentTeamProfileBackgroundResolve( + texture, true, debug_team_profile_mips_serial, debug_team_profile_mips_scaled, + debug_team_profile_mips_scaled_start, debug_team_profile_mips_scaled_length); + if (DebugIsTeamProfileBackgroundTexture(texture)) { + REXGPU_WARN( + "Team profile BG Vulkan upload setup: scaled={}, load_base={}, load_mips={}, " + "load_shader={}, host_buffer_size={:X}, source_buffer={}, source_base={:X}+{:X}, " + "source_mips={:X}+{:X}, texture_scale={}x{}, levels={}..{}, packed_level={}", + bool(texture_key.scaled_resolve), load_base, load_mips, uint32_t(load_shader), + uint64_t(host_buffer_size), texture_key.scaled_resolve ? "scaled-resolve" : "shared-memory", + debug_source_base_start, debug_source_base_range, debug_source_mips_start, + debug_source_mips_range, texture_resolution_scale_x, texture_resolution_scale_y, + level_first, level_last, level_packed); + } + if (debug_team_profile_base_overlap || debug_team_profile_mips_overlap) { + REXGPU_WARN( + "Team profile BG resolved texture upload: texture_scaled={}, load_base={}, load_mips={}, " + "{} {}x{}x{} {}, base={:08X}+{:X}, mips={:08X}+{:X}, source_buffer={}, " + "source_base={:X}+{:X}, source_mips={:X}+{:X}, texture_scale={}x{}, " + "overlap_base=#{} {} {:X}+{:X}, overlap_mips=#{} {} {:X}+{:X}", + bool(texture_key.scaled_resolve), load_base, load_mips, + texture_key.tiled ? "tiled" : "linear", texture_key.GetWidth(), texture_key.GetHeight(), + texture_key.GetDepthOrArraySize(), FormatInfo::Get(texture_key.format)->name, + texture_key.base_page << 12, vulkan_texture.GetGuestBaseSize(), + texture_key.mip_page << 12, vulkan_texture.GetGuestMipsSize(), + texture_key.scaled_resolve ? "scaled-resolve" : "shared-memory", debug_source_base_start, + debug_source_base_range, debug_source_mips_start, debug_source_mips_range, + texture_resolution_scale_x, texture_resolution_scale_y, debug_team_profile_base_serial, + debug_team_profile_base_overlap ? (debug_team_profile_base_scaled ? "scaled" : "1x") + : "none", + debug_team_profile_base_scaled_start, debug_team_profile_base_scaled_length, + debug_team_profile_mips_serial, + debug_team_profile_mips_overlap ? (debug_team_profile_mips_scaled ? "scaled" : "1x") + : "none", + debug_team_profile_mips_scaled_start, debug_team_profile_mips_scaled_length); + } if (texture_key.scaled_resolve) { UseScaledResolveBufferForRead(); } else { @@ -1728,6 +1874,13 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, vulkan_texture.MarkAsUsed(); VulkanTexture::Usage texture_old_usage = vulkan_texture.SetUsage(VulkanTexture::Usage::kTransferDestination); + if (DebugIsTeamProfileBackgroundTexture(texture)) { + REXGPU_WARN( + "Team profile BG Vulkan upload copy begin: old_usage={}, new_usage=transfer-dst, " + "scratch_buffer={:016X}, scratch_size={:X}, image={:016X}", + DebugVulkanTextureUsageName(texture_old_usage), uint64_t(uintptr_t(scratch_buffer)), + uint64_t(host_buffer_size), uint64_t(uintptr_t(vulkan_texture.image()))); + } if (texture_old_usage != VulkanTexture::Usage::kTransferDestination) { VkPipelineStageFlags texture_src_stage_mask, texture_dst_stage_mask; VkAccessFlags texture_src_access_mask, texture_dst_access_mask; @@ -1782,7 +1935,17 @@ bool VulkanTextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, copy_region.imageExtent.height = std::max((height * texture_resolution_scale_y) >> level, UINT32_C(1)); copy_region.imageExtent.depth = std::max(depth >> level, UINT32_C(1)); + if (DebugIsTeamProfileBackgroundTexture(texture)) { + REXGPU_WARN( + "Team profile BG Vulkan upload copy region: level={}, buffer_offset={:X}, " + "row_length={}, image_height={}, extent={}x{}x{}, layers={}", + level, uint64_t(copy_region.bufferOffset), copy_region.bufferRowLength, + copy_region.bufferImageHeight, copy_region.imageExtent.width, + copy_region.imageExtent.height, copy_region.imageExtent.depth, + copy_region.imageSubresource.layerCount); + } } + DebugLogTeamProfileBackgroundGpuEvent("load-exit", UINT32_MAX, texture); return true; } @@ -1813,20 +1976,33 @@ void VulkanTextureCache::UpdateTextureBindingsImpl(uint32_t fetch_constant_mask) host_format_pair.format_unsigned.format != VK_FORMAT_UNDEFINED) { vulkan_binding.image_view_unsigned = static_cast(binding->texture)->GetView(false, binding->host_swizzle); + DebugLogTeamProfileBackgroundGpuEvent("binding-unsigned", binding_index, + *binding->texture, false, binding->host_swizzle, + vulkan_binding.image_view_unsigned); } if (binding->texture_signed && uses_signed && host_format_pair.format_signed.format != VK_FORMAT_UNDEFINED) { vulkan_binding.image_view_signed = static_cast(binding->texture_signed) ->GetView(true, binding->host_swizzle); + DebugLogTeamProfileBackgroundGpuEvent("binding-signed", binding_index, + *binding->texture_signed, true, + binding->host_swizzle, + vulkan_binding.image_view_signed); } } else { VulkanTexture* texture = static_cast(binding->texture); if (texture) { if (uses_unsigned && host_format_pair.format_unsigned.format != VK_FORMAT_UNDEFINED) { vulkan_binding.image_view_unsigned = texture->GetView(false, binding->host_swizzle); + DebugLogTeamProfileBackgroundGpuEvent("binding-unsigned", binding_index, *texture, + false, binding->host_swizzle, + vulkan_binding.image_view_unsigned); } if (uses_signed && host_format_pair.format_signed.format != VK_FORMAT_UNDEFINED) { vulkan_binding.image_view_signed = texture->GetView(true, binding->host_swizzle); + DebugLogTeamProfileBackgroundGpuEvent("binding-signed", binding_index, *texture, true, + binding->host_swizzle, + vulkan_binding.image_view_signed); } } } @@ -1900,6 +2076,8 @@ VkImageView VulkanTextureCache::VulkanTexture::GetView(bool is_signed, uint32_t // Try to find an existing view. auto it = views_.find(view_key); if (it != views_.end()) { + vulkan_texture_cache.DebugLogTeamProfileBackgroundGpuEvent("view-reuse", UINT32_MAX, *this, + is_signed, host_swizzle, it->second); return it->second; } @@ -1942,6 +2120,8 @@ VkImageView VulkanTextureCache::VulkanTexture::GetView(bool is_signed, uint32_t return VK_NULL_HANDLE; } views_.emplace(view_key, view); + vulkan_texture_cache.DebugLogTeamProfileBackgroundGpuEvent("view-create", UINT32_MAX, *this, + is_signed, host_swizzle, view); return view; } @@ -1977,6 +2157,17 @@ VkImageView VulkanTextureCache::VulkanTexture::GetOrCreate3DAs2DImageView(bool i image_create_info.format = wrapper_format; image_create_info.extent.width = key().GetWidth(); image_create_info.extent.height = key().GetHeight(); + if (key().scaled_resolve) { + image_create_info.extent.width *= vulkan_texture_cache.draw_resolution_scale_x(); + image_create_info.extent.height *= vulkan_texture_cache.draw_resolution_scale_y(); + } + if (Consume3DAs2DLogBudget()) { + REXGPU_WARN( + "Vulkan 3D-as-2D create: scaled={}, source={}x{}x{}, wrapper={}x{}x1, format={}", + bool(key().scaled_resolve), key().GetWidth(), key().GetHeight(), + key().GetDepthOrArraySize(), image_create_info.extent.width, + image_create_info.extent.height, uint32_t(wrapper_format)); + } image_create_info.extent.depth = 1; image_create_info.mipLevels = 1; image_create_info.arrayLayers = 1; @@ -2011,6 +2202,11 @@ VkImageView VulkanTextureCache::VulkanTexture::GetOrCreate3DAs2DImageView(bool i texture_3d_as_2d_.reset(); return VK_NULL_HANDLE; } + if (Consume3DAs2DLogBudget()) { + REXGPU_WARN("Vulkan 3D-as-2D load complete: scaled={}, wrapper_image={:016X}", + bool(key().scaled_resolve), + uint64_t(uintptr_t(texture_3d_as_2d_->image()))); + } if (!vulkan_texture_cache.command_processor_.PushImageMemoryBarrier( texture_3d_as_2d_->image(), ui::vulkan::util::InitializeSubresourceRange(), @@ -2056,6 +2252,10 @@ VkImageView VulkanTextureCache::VulkanTexture::GetOrCreate3DAs2DImageView(bool i REXGPU_ERROR("VulkanTextureCache: Failed to create 3D-as-2D wrapper image view"); return VK_NULL_HANDLE; } + if (Consume3DAs2DLogBudget()) { + REXGPU_WARN("Vulkan 3D-as-2D view create: signed={}, scaled={}, view={:016X}", is_signed, + bool(key().scaled_resolve), uint64_t(uintptr_t(cached_view))); + } return cached_view; } @@ -2270,14 +2470,15 @@ void VulkanTextureCache::UseScaledResolveBufferForRead() { VkAccessFlags src_access_mask, dst_access_mask; GetScaledResolveUsageMasks(src_stage_mask, src_access_mask, scaled_resolve_last_usage_write_); GetScaledResolveUsageMasks(dst_stage_mask, dst_access_mask, false); - VkDeviceSize offset; - VkDeviceSize size; - if (!scaled_resolve_last_usage_write_) { - offset = VkDeviceSize(scaled_resolve_last_written_range_.first); - size = VkDeviceSize(scaled_resolve_last_written_range_.second); - } else { - offset = 0; - size = VK_WHOLE_SIZE; + VkDeviceSize offset = VkDeviceSize(scaled_resolve_last_written_range_.first); + VkDeviceSize size = VkDeviceSize(scaled_resolve_last_written_range_.second); + if (scaled_resolve_last_usage_write_) { + // The range tracked here is the union of scaled resolve writes since the + // last read. Make exactly those writes visible to later texture uploads. + if (!size) { + offset = 0; + size = VK_WHOLE_SIZE; + } scaled_resolve_last_usage_write_ = false; } command_processor_.PushBufferMemoryBarrier( @@ -2330,7 +2531,20 @@ void VulkanTextureCache::UseScaledResolveBufferForWrite(uint64_t written_start_s scaled_resolve_last_usage_write_ = true; } - scaled_resolve_last_written_range_ = std::make_pair(written_start_scaled, written_length_scaled); + if (!written_length_scaled) { + return; + } + if (!scaled_resolve_last_written_range_.second) { + scaled_resolve_last_written_range_ = std::make_pair(written_start_scaled, written_length_scaled); + return; + } + uint64_t written_end = written_start_scaled + written_length_scaled; + uint64_t combined_start = std::min(scaled_resolve_last_written_range_.first, written_start_scaled); + uint64_t combined_end = + std::max(scaled_resolve_last_written_range_.first + scaled_resolve_last_written_range_.second, + written_end); + scaled_resolve_last_written_range_ = + std::make_pair(combined_start, combined_end - combined_start); } bool VulkanTextureCache::Initialize() { diff --git a/src/kernel/xam/apps/xgi_app.cpp b/src/kernel/xam/apps/xgi_app.cpp index 7fd57ec..a4d2011 100644 --- a/src/kernel/xam/apps/xgi_app.cpp +++ b/src/kernel/xam/apps/xgi_app.cpp @@ -40,8 +40,15 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, uint32_t user_index = memory::load_and_swap(buffer + 0); uint32_t context_id = memory::load_and_swap(buffer + 16); uint32_t context_value = memory::load_and_swap(buffer + 20); - REXKRNL_DEBUG("XGIUserSetContextEx({:08X}, {:08X}, {:08X})", user_index, context_id, - context_value); + auto old_value = kernel_state_->GetUserContext(user_index, context_id); + kernel_state_->SetUserContext(user_index, context_id, context_value); + if (!old_value || *old_value != context_value) { + REXKRNL_INFO("XGIUserSetContextEx(user={}, context={:#x}, value={:#x})", user_index, + context_id, context_value); + } else { + REXKRNL_DEBUG("XGIUserSetContextEx(user={}, context={:#x}, value={:#x})", user_index, + context_id, context_value); + } return X_E_SUCCESS; } case 0x000B0007: { @@ -353,13 +360,18 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, uint32_t context_ptr = memory::load_and_swap(buffer + 16); auto context = context_ptr ? memory_->TranslateVirtual(context_ptr) : nullptr; uint32_t context_id = context ? memory::load_and_swap(context + 0) : 0; - REXKRNL_DEBUG("XGIUserGetContext({:08X}, {:08X}, {:08X}))", user_index, context_ptr, - context_id); - uint32_t value = 0; - if (context) { - memory::store_and_swap(context + 4, value); + auto stored_value = kernel_state_->GetUserContext(user_index, context_id); + if (stored_value) { + REXKRNL_DEBUG("XGIUserGetContext({:08X}, {:08X}, {:08X}) -> {:#x}", user_index, + context_ptr, context_id, *stored_value); + } else { + REXKRNL_DEBUG("XGIUserGetContext({:08X}, {:08X}, {:08X}) -> unset", user_index, + context_ptr, context_id); } - return X_E_FAIL; + if (context) { + memory::store_and_swap(context + 4, stored_value.value_or(0)); + } + return stored_value ? X_E_SUCCESS : X_E_FAIL; } case 0x000B0060: { assert_true(!buffer_length || buffer_length == 32); diff --git a/src/kernel/xam/xam_ui.cpp b/src/kernel/xam/xam_ui.cpp index edb3b3d..1ff5944 100644 --- a/src/kernel/xam/xam_ui.cpp +++ b/src/kernel/xam/xam_ui.cpp @@ -363,17 +363,16 @@ class KeyboardInputDialog : public XamDialog { bool cancelled() const { return cancelled_; } void OnDraw(ImGuiIO& io) override { - bool first_draw = false; if (!has_opened_) { ImGui::OpenPopup(title_.c_str()); has_opened_ = true; - first_draw = true; + focus_request_frames_ = 10; } if (ImGui::BeginPopupModal(title_.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { if (description_.size()) { ImGui::TextWrapped("%s", description_.c_str()); } - if (first_draw) { + if (focus_request_frames_) { ImGui::SetKeyboardFocusHere(); } if (ImGui::InputText("##body", text_buffer_.data(), text_buffer_.size(), @@ -383,6 +382,13 @@ class KeyboardInputDialog : public XamDialog { ImGui::CloseCurrentPopup(); Close(); } + if (focus_request_frames_) { + if (ImGui::IsItemActive()) { + focus_request_frames_ = 0; + } else { + --focus_request_frames_; + } + } if (ImGui::Button("OK")) { text_ = std::string(text_buffer_.data(), text_buffer_.size()); cancelled_ = false; @@ -405,6 +411,7 @@ class KeyboardInputDialog : public XamDialog { private: bool has_opened_ = false; + uint32_t focus_request_frames_ = 0; std::string title_; std::string description_; std::string default_text_; diff --git a/src/system/kernel_state.cpp b/src/system/kernel_state.cpp index 465e4b8..e127d42 100644 --- a/src/system/kernel_state.cpp +++ b/src/system/kernel_state.cpp @@ -995,6 +995,23 @@ void KernelState::BroadcastNotification(XNotificationID id, uint32_t data) { } } +void KernelState::SetUserContext(uint32_t user_index, uint32_t context_id, uint32_t value) { + const uint64_t key = (uint64_t(user_index) << 32) | context_id; + std::lock_guard lock(user_contexts_mutex_); + user_contexts_[key] = value; +} + +std::optional KernelState::GetUserContext(uint32_t user_index, + uint32_t context_id) const { + const uint64_t key = (uint64_t(user_index) << 32) | context_id; + std::lock_guard lock(user_contexts_mutex_); + auto it = user_contexts_.find(key); + if (it == user_contexts_.end()) { + return std::nullopt; + } + return it->second; +} + void KernelState::CompleteOverlapped(uint32_t overlapped_ptr, X_RESULT result) { CompleteOverlappedEx(overlapped_ptr, result, result, 0); } diff --git a/src/ui/rex_app.cpp b/src/ui/rex_app.cpp index 57fcec7..8ddf37f 100644 --- a/src/ui/rex_app.cpp +++ b/src/ui/rex_app.cpp @@ -40,14 +40,23 @@ #include #include +#if REX_PLATFORM_LINUX +#include +#include +#endif + #include #include #include #include +#include +#include #include +#include #include #include +#include #include namespace rex { @@ -61,6 +70,139 @@ constexpr bool kBlockShaderStorageStartup = false; #endif +#if REX_PLATFORM_LINUX +std::string Trim(std::string value) { + const auto first = value.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) { + return ""; + } + const auto last = value.find_last_not_of(" \t\r\n"); + return value.substr(first, last - first + 1); +} + +std::string UnquoteOsReleaseValue(std::string value) { + value = Trim(std::move(value)); + if (value.size() >= 2 && value.front() == '"' && value.back() == '"') { + std::string unquoted; + unquoted.reserve(value.size() - 2); + bool escaped = false; + for (size_t i = 1; i + 1 < value.size(); ++i) { + const char ch = value[i]; + if (escaped) { + unquoted.push_back(ch); + escaped = false; + } else if (ch == '\\') { + escaped = true; + } else { + unquoted.push_back(ch); + } + } + return unquoted; + } + return value; +} + +std::optional ReadOsReleaseValue(std::string_view key) { + std::ifstream file("/etc/os-release"); + std::string line; + while (std::getline(file, line)) { + const auto equals = line.find('='); + if (equals == std::string::npos) { + continue; + } + if (std::string_view(line.data(), equals) == key) { + return UnquoteOsReleaseValue(line.substr(equals + 1)); + } + } + return std::nullopt; +} + +std::string TruncateForLog(std::string value) { + constexpr size_t kMaxLength = 512; + if (value.size() <= kMaxLength) { + return value; + } + value.resize(kMaxLength); + value += "..."; + return value; +} + +void LogEnvIfSet(const char* name) { + const char* value = std::getenv(name); + if (value && *value) { + REXLOG_INFO(" {}={}", name, TruncateForLog(value)); + } +} + +void LogLinuxRuntimeDiagnostics() { + REXLOG_INFO("Linux runtime diagnostics:"); + + const auto pretty_name = ReadOsReleaseValue("PRETTY_NAME"); + const auto id = ReadOsReleaseValue("ID"); + const auto version_id = ReadOsReleaseValue("VERSION_ID"); + if (pretty_name) { + REXLOG_INFO(" OS: {}", *pretty_name); + } + if (id || version_id) { + REXLOG_INFO(" OS ID: {} {}", id.value_or("unknown"), version_id.value_or("")); + } + + utsname uts = {}; + if (uname(&uts) == 0) { + REXLOG_INFO(" Kernel: {} {} {}", uts.sysname, uts.release, uts.machine); + } + REXLOG_INFO(" glibc: {}", gnu_get_libc_version()); + +#if defined(__clang__) + REXLOG_INFO(" Compiler: clang {}", __clang_version__); +#elif defined(__GNUC__) + REXLOG_INFO(" Compiler: GCC {}.{}.{}", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__); +#endif + + REXLOG_INFO("Linux session environment:"); + LogEnvIfSet("XDG_SESSION_TYPE"); + LogEnvIfSet("XDG_CURRENT_DESKTOP"); + LogEnvIfSet("DESKTOP_SESSION"); + LogEnvIfSet("DISPLAY"); + LogEnvIfSet("WAYLAND_DISPLAY"); + LogEnvIfSet("GDK_BACKEND"); + LogEnvIfSet("SDL_VIDEODRIVER"); + LogEnvIfSet("LD_LIBRARY_PATH"); + + REXLOG_INFO("Steam runtime environment:"); + LogEnvIfSet("SteamAppId"); + LogEnvIfSet("SteamGameId"); + LogEnvIfSet("STEAM_COMPAT_APP_ID"); + LogEnvIfSet("STEAM_COMPAT_CLIENT_INSTALL_PATH"); + LogEnvIfSet("STEAM_COMPAT_DATA_PATH"); + LogEnvIfSet("STEAM_RUNTIME"); + LogEnvIfSet("STEAM_RUNTIME_LIBRARY_PATH"); + LogEnvIfSet("STEAM_RUNTIME_HEAVY"); + LogEnvIfSet("PRESSURE_VESSEL_RUNTIME"); + LogEnvIfSet("PRESSURE_VESSEL_APP_ID"); + LogEnvIfSet("container"); + + REXLOG_INFO("Steam Deck / gamescope environment:"); + LogEnvIfSet("SteamDeck"); + LogEnvIfSet("STEAMOS"); + LogEnvIfSet("GAMESCOPE_WAYLAND_DISPLAY"); + LogEnvIfSet("GAMESCOPE_EXTERNAL_OVERLAY"); + LogEnvIfSet("ENABLE_GAMESCOPE_WSI"); + LogEnvIfSet("MESA_VK_WSI_PRESENT_MODE"); + LogEnvIfSet("RADV_PERFTEST"); + LogEnvIfSet("VK_ICD_FILENAMES"); + LogEnvIfSet("VK_DRIVER_FILES"); +} + +void StartForcedExitWatchdog(const char* reason) { + std::thread([reason]() { + std::this_thread::sleep_for(std::chrono::seconds(10)); + REXLOG_WARN("{} watchdog exiting process after shutdown timeout", reason); + std::_Exit(EXIT_SUCCESS); + }).detach(); +} +#endif + } // namespace REXCVAR_DEFINE_BOOL(advanced_settings_overlay_enabled, true, "UI/Advanced", @@ -239,6 +381,9 @@ bool ReXApp::SetupEnvironment() { REXLOG_INFO(" Update data: {}", update_data_root_.string()); } REXLOG_INFO(" Cache root: {}", cache_root_.string()); +#if REX_PLATFORM_LINUX + LogLinuxRuntimeDiagnostics(); +#endif return true; } @@ -300,8 +445,9 @@ bool ReXApp::ConstructRuntime(const PathConfig& paths) { auto* input_sys = static_cast(runtime_->input_system()); if (input_sys) { input_sys->SetActiveCallback([this]() { - if (!debug_overlay_ && !console_overlay_ && !settings_overlay_) + if (!imgui_drawer_->HasDialogs()) { return true; + } const auto& io = imgui_drawer_->GetIO(); return !io.WantCaptureMouse && !io.WantCaptureKeyboard; }); @@ -496,6 +642,9 @@ void ReXApp::OnKeyDown(ui::KeyEvent& e) { void ReXApp::OnClosing(ui::UIEvent& e) { (void)e; REXLOG_INFO("Window closing, shutting down..."); +#if REX_PLATFORM_LINUX + StartForcedExitWatchdog("Linux window close"); +#endif shutting_down_.store(true, std::memory_order_release); #if REX_PLATFORM_MAC if (main_thread_ && main_thread_->is_running()) {