improve Vulkan texture and presentation handling

This commit is contained in:
mchughalex 2026-06-06 15:13:40 +01:00
parent 3184592404
commit 5b6b87bcea
30 changed files with 1998 additions and 62 deletions

View file

@ -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;

View file

@ -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);

View file

@ -45,6 +45,7 @@ enum class ReadbackResolveMode {
kFast,
kSome,
kFull,
kAuto,
};
struct SwapState {

View file

@ -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<ID3D12Resource> resource_;

View file

@ -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);

View file

@ -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<std::recursive_mutex>& global_lock);
void WatchCallback(const std::unique_lock<std::recursive_mutex>& global_lock, bool is_mip);
void WatchCallback(const std::unique_lock<std::recursive_mutex>& 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<DebugResolveReadbackRange, 32> 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<DebugTeamProfileResolveRange, 64> 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

View file

@ -80,6 +80,10 @@ class SharedMemory {
bool RequestRanges(const std::pair<uint32_t, uint32_t>* ranges, size_t count);
bool RequestRange(uint32_t start, uint32_t length);
const uint8_t* DebugTranslatePhysical(uint32_t address) const {
return memory_.TranslatePhysical<const uint8_t*>(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

View file

@ -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<uint32_t, kDebugFrameResolveRangeSamples> debug_frame_resolve_addresses_ = {};
std::array<uint32_t, kDebugFrameResolveRangeSamples> debug_frame_resolve_lengths_ = {};
std::array<bool, kDebugFrameResolveRangeSamples> 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;

View file

@ -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();

View file

@ -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;

View file

@ -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<uint32_t> 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<object_ref<XNotifyListener>> notify_listeners_;
bool has_notified_startup_ = false;
mutable std::mutex user_contexts_mutex_;
std::unordered_map<uint64_t, uint32_t> user_contexts_;
// Protected by global_critical_region_.
std::unordered_map<uint32_t, FiberInfo> fiber_map_;

View file

@ -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);

View file

@ -11,6 +11,7 @@
#include <rex/filesystem/devices/host_path_entry.h>
#include <rex/filesystem/devices/host_path_file.h>
#include <rex/filesystem/devices/null_file.h>
#include <rex/filesystem.h>
#include <rex/filesystem/device.h>
@ -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.

View file

@ -12,8 +12,63 @@
#include <rex/filesystem/devices/host_path_entry.h>
#include <rex/filesystem/devices/host_path_file.h>
#include <algorithm>
#include <string>
#include <string_view>
#include <rex/cvar.h>
#include <rex/filesystem/entry.h>
#include <rex/filesystem/flags.h>
#include <rex/logging.h>
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<char>(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<char>(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<rex::filesystem::FileHandle> file_handle)
: File(file_access, entry), file_handle_(std::move(file_handle)) {}
@ -31,6 +86,20 @@ X_STATUS HostPathFile::ReadSync(std::span<uint8_t> 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;

View file

@ -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<uint8_t> 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<uint8_t> buffer, size_t byte_offset, size_
X_STATUS NullFile::WriteSync(std::span<const uint8_t> 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<const uint8_t> buffer, size_t byte_offset
}
X_STATUS NullFile::SetLength(size_t length) {
(void)length;
if (!(file_access_ & FileAccess::kFileWriteData)) {
return X_STATUS_ACCESS_DENIED;
}

View file

@ -9,6 +9,10 @@
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
*/
#include <algorithm>
#include <string>
#include <string_view>
#include <rex/filesystem/vfs.h>
#include <rex/logging.h>
#include <rex/string.h>
@ -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<char>(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<char>(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<int>(creation_disposition), static_cast<int>(*out_action),
result, entry ? entry->absolute_path() : std::string_view("<null>"));
}
if (LooksLikeSkaterPreviewAssetPath(path) && ConsumeFeAssetLogBudget()) {
REXFS_WARN(
"FE asset diagnostic: op=open, path='{}', access={:#x}, disposition={}, action={}, "
"status={:#x}, entry='{}'",
path, desired_access, static_cast<int>(creation_disposition), static_cast<int>(*out_action),
result, entry ? entry->absolute_path() : std::string_view("<null>"));
}
if (XFAILED(result)) {
*out_action = FileAction::kDoesNotExist;
}

View file

@ -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;
}

View file

@ -30,6 +30,7 @@
#include <rex/kernel/xboxkrnl/video.h>
#include <rex/logging.h>
#include <rex/memory/utils.h>
#include <rex/system/kernel_state.h>
#include <rex/ui/d3d12/d3d12_presenter.h>
#include <rex/ui/d3d12/d3d12_util.h>
@ -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;

View file

@ -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);
}

View file

@ -24,9 +24,6 @@
#include <rex/math.h>
#include <rex/string.h>
REXCVAR_DEFINE_BOOL(draw_resolution_scaled_texture_offsets, true, "GPU/Shader",
"Scale texture offsets with draw resolution");
namespace rex::graphics {
using namespace ucode;

View file

@ -19,6 +19,8 @@
#include <fmt/format.h>
#include <rex/assert.h>
#include <rex/cvar.h>
#include <rex/graphics/flags.h>
#include <rex/graphics/pipeline/shader/spirv_translator.h>
#include <rex/math.h>
@ -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) {

View file

@ -12,6 +12,7 @@
#include <algorithm>
#include <array>
#include <cstdint>
#include <string_view>
#include <utility>
#include <rex/assert.h>
@ -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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<size_t>(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<std::recursive_mutex>& global_lock, bool is_mip) {
[[maybe_unused]] const std::unique_lock<std::recursive_mutex>& 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<std::recursive_mutex>& g
void* context, void* data, uint64_t argument,
bool invalidated_by_gpu) {
Texture& texture = *static_cast<Texture*>(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<std::recursive_mutex>& global_lock, void* context,
uint32_t address_first, uint32_t address_last, bool invalidated_by_gpu) {

View file

@ -11,13 +11,17 @@
#include <algorithm>
#include <array>
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <mutex>
#include <string>
#include <string_view>
#include <tuple>
#include <unordered_set>
#include <utility>
#include <vector>
@ -42,6 +46,7 @@
#include <rex/graphics/vulkan/shared_memory.h>
#include <rex/graphics/xenos.h>
#include <rex/kernel/xboxkrnl/video.h>
#include <rex/system/kernel_state.h>
#include <rex/types.h>
#include <rex/memory/utils.h>
#include <rex/ui/flags.h>
@ -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<uint64_t> 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<const char*>(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,7 +2498,54 @@ 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_;
auto log_debug_frame_summary = [&]() {
int32_t remaining = REXCVAR_GET(vulkan_debug_log_frame_summaries_remaining);
if (remaining <= 0) {
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;
@ -2325,6 +2557,18 @@ void VulkanCommandProcessor::IssueSwap(uint32_t frontbuffer_ptr, uint32_t frontb
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();
// Obtain the actual swap source texture size (resolution-scaled if it's a
@ -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<reg::VGT_DRAW_INITIATOR>();
REXGPU_ERROR(
@ -3636,6 +3884,9 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
xenos::EdramMode edram_mode = regs.Get<reg::RB_MODECONTROL>().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<reg::RB_SURFACE_INFO>();
const auto pa_sc_window_scissor_tl = regs.Get<reg::PA_SC_WINDOW_SCISSOR_TL>();
const auto pa_sc_window_scissor_br = regs.Get<reg::PA_SC_WINDOW_SCISSOR_BR>();
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<reg::VGT_DRAW_INITIATOR>();
// 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_,
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,21 +5142,92 @@ 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] &&
@ -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_));

View file

@ -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<const RenderTarget*>(last_update_framebuffer_attachments_[i]);
if (rt == nullptr) {
continue;
}
const RenderTargetKey key = rt->key();
const VulkanRenderTarget* vulkan_rt = static_cast<const VulkanRenderTarget*>(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, uint32_t>(uint32_t(copy_dest_use_start),

View file

@ -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;

View file

@ -13,6 +13,7 @@
#include <array>
#include <cstddef>
#include <cstring>
#include <string_view>
#include <utility>
#include <rex/assert.h>
@ -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<VulkanTexture*>(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<const VulkanTexture&>(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<VulkanTexture&>(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<VulkanTexture*>(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<VulkanTexture*>(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<VulkanTexture*>(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 {
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;
}
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() {

View file

@ -40,8 +40,15 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
uint32_t user_index = memory::load_and_swap<uint32_t>(buffer + 0);
uint32_t context_id = memory::load_and_swap<uint32_t>(buffer + 16);
uint32_t context_value = memory::load_and_swap<uint32_t>(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<uint32_t>(buffer + 16);
auto context = context_ptr ? memory_->TranslateVirtual(context_ptr) : nullptr;
uint32_t context_id = context ? memory::load_and_swap<uint32_t>(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<uint32_t>(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<uint32_t>(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);

View file

@ -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_;

View file

@ -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<uint32_t> 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);
}

View file

@ -40,14 +40,23 @@
#include <rex/ui/keybinds.h>
#include <rex/version.h>
#if REX_PLATFORM_LINUX
#include <gnu/libc-version.h>
#include <sys/utsname.h>
#endif
#include <fmt/format.h>
#include <imgui.h>
#include <toml++/toml.hpp>
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <optional>
#include <string_view>
#include <thread>
#include <utility>
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<std::string> 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<rex::input::InputSystem*>(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()) {