improve Vulkan and graphics performance
This commit is contained in:
parent
6e34b9a745
commit
4ae4da7552
36 changed files with 3531 additions and 317 deletions
|
|
@ -12,6 +12,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
|
@ -306,6 +307,19 @@ class CommandProcessor {
|
|||
const char* legacy_readback_memexport_cvar_name_ = nullptr;
|
||||
|
||||
private:
|
||||
// CPU-side frame attribution for the GPU emulation thread (gpu_cpu_profile
|
||||
// cvar): manages the rex::perf::cpu_profile enable state and logs the
|
||||
// per-bucket breakdown every kCpuProfileLogFrameInterval frames. Called once
|
||||
// per guest frame from the XE_SWAP packet handler.
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
void CpuProfileOnFrameEnd();
|
||||
|
||||
static constexpr uint32_t kCpuProfileLogFrameInterval = 120;
|
||||
uint32_t cpu_profile_frames_ = 0;
|
||||
uint64_t cpu_profile_window_start_ticks_ = 0;
|
||||
std::chrono::steady_clock::time_point cpu_profile_window_start_time_{};
|
||||
#endif
|
||||
|
||||
reg::DC_LUT_30_COLOR gamma_ramp_256_entry_table_[256] = {};
|
||||
reg::DC_LUT_PWL_DATA gamma_ramp_pwl_rgb_[128][3] = {};
|
||||
uint32_t gamma_ramp_rw_component_ = 0;
|
||||
|
|
|
|||
|
|
@ -387,9 +387,10 @@ class D3D12CommandProcessor : public CommandProcessor {
|
|||
void EndGpuTimestampFrame();
|
||||
void ResolveGpuTimestampFrame(ID3D12GraphicsCommandList* command_list);
|
||||
void ProcessGpuTimestampResults();
|
||||
public:
|
||||
// Also used by the texture and render target caches for profiling regions.
|
||||
uint32_t BeginGpuTimestampedDraw(rex::perf::DrawBucket bucket);
|
||||
void EndGpuTimestampedDraw(uint32_t start_query_index);
|
||||
public:
|
||||
uint32_t BeginGpuTimestampedCounter(ID3D12GraphicsCommandList* command_list,
|
||||
rex::perf::CounterId counter_id);
|
||||
void EndGpuTimestampedCounter(ID3D12GraphicsCommandList* command_list,
|
||||
|
|
@ -693,6 +694,7 @@ class D3D12CommandProcessor : public CommandProcessor {
|
|||
uint64_t* gpu_timestamp_readback_mapping_ = nullptr;
|
||||
uint64_t gpu_timestamp_frequency_ = 0;
|
||||
bool gpu_timestamp_resources_available_ = false;
|
||||
bool gpu_timestamp_draws_enabled_ = false;
|
||||
struct GpuTimestampFrame {
|
||||
uint64_t frame = 0;
|
||||
uint64_t submission = 0;
|
||||
|
|
@ -707,6 +709,12 @@ class D3D12CommandProcessor : public CommandProcessor {
|
|||
std::vector<uint32_t> counter_start_queries;
|
||||
};
|
||||
std::array<GpuTimestampFrame, kQueueFrames> gpu_timestamp_frames_;
|
||||
// Averaged full-frame GPU time attribution, logged periodically when bucket
|
||||
// timestamps are enabled.
|
||||
static constexpr uint32_t kGpuProfileLogFrameInterval = 120;
|
||||
uint64_t gpu_profile_bucket_us_[size_t(rex::perf::DrawBucket::kCount)] = {};
|
||||
uint64_t gpu_profile_span_us_ = 0;
|
||||
uint32_t gpu_profile_frames_ = 0;
|
||||
struct VertexBufferState {
|
||||
uint32_t address = UINT32_MAX;
|
||||
uint32_t size = UINT32_MAX;
|
||||
|
|
|
|||
|
|
@ -541,6 +541,16 @@ class RenderTargetCache {
|
|||
RenderTarget*& color_render_target_out,
|
||||
std::vector<Transfer>& color_transfers_out);
|
||||
|
||||
// Returns whether every EDRAM tile range for which the given render target
|
||||
// holds the up-to-date contents - as the current owner, or as the latest
|
||||
// host depth copy if the host depth encoding differs from the guest - lies
|
||||
// fully within the given range relative to the render target's base (using
|
||||
// the same wrapping rules as ChangeOwnership). When this holds, destroying
|
||||
// the host render target's contents outside that range (for example with a
|
||||
// whole-image clear) loses no data that could still be read.
|
||||
bool IsOwnershipConfinedToRange(RenderTargetKey key, uint32_t start_tiles_base_relative,
|
||||
uint32_t length_tiles) const;
|
||||
|
||||
// For restoring EDRAM contents from frame traces, obtains or creates a render
|
||||
// target at base 0 with of 1280 (only 1 sample and color because copying
|
||||
// between MSAA render targets and buffers is not possible in Direct3D 12, and
|
||||
|
|
|
|||
|
|
@ -115,6 +115,13 @@ class TextureCache {
|
|||
}
|
||||
|
||||
virtual void RequestTextures(uint32_t used_texture_mask);
|
||||
|
||||
// Monotonic generation of the active texture binding views - incremented
|
||||
// whenever any binding's textures change (UpdateTextureBindingsImpl calls).
|
||||
// Backends can compare this between draws to skip rebuilding descriptor
|
||||
// contents when the bound views are unchanged.
|
||||
uint64_t texture_bindings_generation() const { return texture_bindings_generation_; }
|
||||
|
||||
void DebugRecordResolveReadback(uint32_t start, uint32_t length, bool scaled);
|
||||
void DebugClearRecentResolveReadbacks();
|
||||
void DebugRecordTeamProfileBackgroundResolveRange(uint32_t start, uint32_t length, bool scaled,
|
||||
|
|
@ -669,6 +676,8 @@ class TextureCache {
|
|||
// Bit vector with bits reset on fetch constant writes to avoid parsing fetch
|
||||
// constants again and again.
|
||||
uint32_t texture_bindings_in_sync_ = 0;
|
||||
// See texture_bindings_generation().
|
||||
uint64_t texture_bindings_generation_ = 1;
|
||||
|
||||
int32_t debug_scaled_texture_bindings_remaining_seen_ = 0;
|
||||
uint32_t debug_scaled_texture_binding_logs_this_sample_ = 0;
|
||||
|
|
|
|||
|
|
@ -614,6 +614,12 @@ class VulkanCommandProcessor : public CommandProcessor {
|
|||
// Descriptor set layouts used by different shaders.
|
||||
VkDescriptorSetLayout descriptor_set_layout_empty_ = VK_NULL_HANDLE;
|
||||
VkDescriptorSetLayout descriptor_set_layout_constants_ = VK_NULL_HANDLE;
|
||||
// Whether the guest constant buffer descriptors are pushed via
|
||||
// VK_KHR_push_descriptor instead of being written to transient descriptor
|
||||
// sets (vulkan_push_constants_descriptors and driver support, applied at
|
||||
// setup; descriptor_set_layout_constants_ is created with the push
|
||||
// descriptor flag and must not be used with vkAllocateDescriptorSets then).
|
||||
bool constants_push_descriptors_used_ = false;
|
||||
std::array<VkDescriptorSetLayout, size_t(SingleTransientDescriptorLayout::kCount)>
|
||||
descriptor_set_layouts_single_transient_{};
|
||||
VkDescriptorSetLayout descriptor_set_layout_shared_memory_and_edram_ = VK_NULL_HANDLE;
|
||||
|
|
@ -646,6 +652,21 @@ class VulkanCommandProcessor : public CommandProcessor {
|
|||
std::unordered_map<TextureDescriptorSetLayoutKey, std::vector<VkDescriptorSet>,
|
||||
TextureDescriptorSetLayoutKey::Hasher>
|
||||
texture_transient_descriptor_sets_free_;
|
||||
// Frame-local reuse of transient texture/sampler descriptor sets: XXH3 of
|
||||
// the binding contents (stage, counts, image views, samplers) -> a set
|
||||
// already written with those contents this frame. Transient sets are
|
||||
// recycled only after their frame completes on the GPU, so within a frame
|
||||
// their contents are immutable and the same set can simply be rebound
|
||||
// instead of allocating and writing a new one for every draw. Cleared at
|
||||
// frame open.
|
||||
std::unordered_map<uint64_t, VkDescriptorSet, rex::IdentityHasher<uint64_t>>
|
||||
texture_transient_descriptor_set_reuse_cache_;
|
||||
// Content hashes of the texture descriptor sets currently recorded in
|
||||
// current_graphics_descriptor_sets_, indexed [0] = vertex, [1] = pixel. The
|
||||
// valid flag is false when the recorded set's contents are unknown (frame
|
||||
// open, write failure, or reuse disabled).
|
||||
bool current_texture_descriptor_set_hash_valid_[2] = {};
|
||||
uint64_t current_texture_descriptor_set_hashes_[2] = {};
|
||||
|
||||
std::unique_ptr<VulkanSharedMemory> shared_memory_;
|
||||
|
||||
|
|
@ -784,8 +805,12 @@ class VulkanCommandProcessor : public CommandProcessor {
|
|||
bool valid = false;
|
||||
} active_occlusion_query_;
|
||||
// MoltenVK maps timestamp queries to Metal counter sample buffers with a
|
||||
// small per-pool limit, so keep the per-frame query budget modest.
|
||||
// small per-pool limit, so keep the per-frame query budget modest there.
|
||||
#if REX_PLATFORM_MAC
|
||||
static constexpr uint32_t kMaxGpuTimestampQueriesPerFrame = 1024;
|
||||
#else
|
||||
static constexpr uint32_t kMaxGpuTimestampQueriesPerFrame = 4096;
|
||||
#endif
|
||||
VkQueryPool gpu_timestamp_query_pool_ = VK_NULL_HANDLE;
|
||||
VkBuffer gpu_timestamp_readback_buffer_ = VK_NULL_HANDLE;
|
||||
VkDeviceMemory gpu_timestamp_readback_memory_ = VK_NULL_HANDLE;
|
||||
|
|
@ -793,9 +818,13 @@ class VulkanCommandProcessor : public CommandProcessor {
|
|||
VkDeviceSize gpu_timestamp_readback_memory_size_ = 0;
|
||||
uint64_t* gpu_timestamp_readback_mapping_ = nullptr;
|
||||
bool gpu_timestamp_resources_available_ = false;
|
||||
bool gpu_timestamp_draws_enabled_ = false;
|
||||
struct GpuTimestampFrame {
|
||||
uint64_t frame = 0;
|
||||
uint64_t submission = 0;
|
||||
// Submission index when the frame began - for counting the queue
|
||||
// submissions the frame spans in the profile log.
|
||||
uint64_t first_submission = 0;
|
||||
uint32_t query_base = 0;
|
||||
uint32_t query_count = 0;
|
||||
uint32_t frame_start_query = UINT32_MAX;
|
||||
|
|
@ -809,6 +838,13 @@ class VulkanCommandProcessor : public CommandProcessor {
|
|||
std::vector<uint32_t> bucket_end_queries;
|
||||
};
|
||||
std::array<GpuTimestampFrame, kMaxFramesInFlight> gpu_timestamp_frames_;
|
||||
// Averaged full-frame GPU time attribution, logged periodically when bucket
|
||||
// timestamps are enabled.
|
||||
static constexpr uint32_t kGpuProfileLogFrameInterval = 120;
|
||||
uint64_t gpu_profile_bucket_us_[size_t(rex::perf::DrawBucket::kCount)] = {};
|
||||
uint64_t gpu_profile_span_us_ = 0;
|
||||
uint64_t gpu_profile_submissions_ = 0;
|
||||
uint32_t gpu_profile_frames_ = 0;
|
||||
struct VertexBufferState {
|
||||
uint32_t address = UINT32_MAX;
|
||||
uint32_t size = UINT32_MAX;
|
||||
|
|
@ -856,6 +892,20 @@ class VulkanCommandProcessor : public CommandProcessor {
|
|||
std::vector<std::pair<VulkanTextureCache::SamplerParameters, VkSampler>> current_samplers_vertex_;
|
||||
std::vector<std::pair<VulkanTextureCache::SamplerParameters, VkSampler>> current_samplers_pixel_;
|
||||
|
||||
// State the per-draw binding fast path compares against
|
||||
// (vulkan_reuse_unchanged_draw_bindings): when the previous draw in the same
|
||||
// submission used the same shaders, identical sampler parameters and the
|
||||
// texture cache's binding views are unchanged, the sampler cache walks and
|
||||
// the texture descriptor gather/hash in UpdateBindings are skipped.
|
||||
bool draw_binding_state_valid_ = false;
|
||||
uint64_t draw_binding_submission_ = 0;
|
||||
const VulkanShader* draw_binding_vertex_shader_ = nullptr;
|
||||
const VulkanShader* draw_binding_pixel_shader_ = nullptr;
|
||||
uint64_t draw_binding_texture_generation_ = 0;
|
||||
// Set by IssueDraw for the draw currently being issued, consumed by
|
||||
// UpdateBindings.
|
||||
bool texture_bindings_unchanged_this_draw_ = false;
|
||||
|
||||
// Cache render pass currently started in the command buffer with the
|
||||
// framebuffer. For dynamic rendering, current_render_pass_ is VK_NULL_HANDLE
|
||||
// but in_render_pass_ is true.
|
||||
|
|
|
|||
|
|
@ -164,6 +164,28 @@ class DeferredCommandBuffer {
|
|||
sizeof(VkImageSubresourceRange) * range_count);
|
||||
}
|
||||
|
||||
VkImageSubresourceRange* CmdClearDepthStencilImageEmplace(
|
||||
VkImage image, VkImageLayout image_layout, const VkClearDepthStencilValue* depth_stencil,
|
||||
uint32_t range_count) {
|
||||
const size_t header_size =
|
||||
rex::align(sizeof(ArgsVkClearDepthStencilImage), alignof(VkImageSubresourceRange));
|
||||
uint8_t* args_ptr = reinterpret_cast<uint8_t*>(
|
||||
WriteCommand(Command::kVkClearDepthStencilImage,
|
||||
header_size + sizeof(VkImageSubresourceRange) * range_count));
|
||||
auto& args = *reinterpret_cast<ArgsVkClearDepthStencilImage*>(args_ptr);
|
||||
args.image = image;
|
||||
args.image_layout = image_layout;
|
||||
args.depth_stencil = *depth_stencil;
|
||||
args.range_count = range_count;
|
||||
return reinterpret_cast<VkImageSubresourceRange*>(args_ptr + header_size);
|
||||
}
|
||||
void CmdVkClearDepthStencilImage(VkImage image, VkImageLayout image_layout,
|
||||
const VkClearDepthStencilValue* depth_stencil,
|
||||
uint32_t range_count, const VkImageSubresourceRange* ranges) {
|
||||
std::memcpy(CmdClearDepthStencilImageEmplace(image, image_layout, depth_stencil, range_count),
|
||||
ranges, sizeof(VkImageSubresourceRange) * range_count);
|
||||
}
|
||||
|
||||
VkBufferCopy* CmdCopyBufferEmplace(VkBuffer src_buffer, VkBuffer dst_buffer,
|
||||
uint32_t region_count) {
|
||||
const size_t header_size = rex::align(sizeof(ArgsVkCopyBuffer), alignof(VkBufferCopy));
|
||||
|
|
@ -294,6 +316,29 @@ class DeferredCommandBuffer {
|
|||
std::memcpy(args_ptr + sizeof(ArgsVkPushConstants), values, size);
|
||||
}
|
||||
|
||||
// Pushes uniform buffer descriptors (bindings 0 through buffer_count - 1 of
|
||||
// the set, one buffer each) via VK_KHR_push_descriptor - the extension must
|
||||
// be available and the set layout must have been created with the push
|
||||
// descriptor flag.
|
||||
void CmdVkPushUniformBufferDescriptorSet(VkPipelineBindPoint pipeline_bind_point,
|
||||
VkPipelineLayout layout, uint32_t set,
|
||||
uint32_t buffer_count,
|
||||
const VkDescriptorBufferInfo* buffer_infos) {
|
||||
size_t arguments_size = rex::align(sizeof(ArgsVkPushUniformBufferDescriptorSet),
|
||||
alignof(VkDescriptorBufferInfo));
|
||||
size_t buffer_infos_offset = arguments_size;
|
||||
arguments_size += sizeof(VkDescriptorBufferInfo) * buffer_count;
|
||||
uint8_t* args_ptr = reinterpret_cast<uint8_t*>(
|
||||
WriteCommand(Command::kVkPushUniformBufferDescriptorSet, arguments_size));
|
||||
auto& args = *reinterpret_cast<ArgsVkPushUniformBufferDescriptorSet*>(args_ptr);
|
||||
args.pipeline_bind_point = pipeline_bind_point;
|
||||
args.layout = layout;
|
||||
args.set = set;
|
||||
args.buffer_count = buffer_count;
|
||||
std::memcpy(args_ptr + buffer_infos_offset, buffer_infos,
|
||||
sizeof(VkDescriptorBufferInfo) * buffer_count);
|
||||
}
|
||||
|
||||
void CmdVkSetBlendConstants(const float* blend_constants) {
|
||||
auto& args = *reinterpret_cast<ArgsVkSetBlendConstants*>(
|
||||
WriteCommand(Command::kVkSetBlendConstants, sizeof(ArgsVkSetBlendConstants)));
|
||||
|
|
@ -370,6 +415,7 @@ class DeferredCommandBuffer {
|
|||
kVkBindVertexBuffers,
|
||||
kVkClearAttachments,
|
||||
kVkClearColorImage,
|
||||
kVkClearDepthStencilImage,
|
||||
kVkCopyBuffer,
|
||||
kVkCopyBufferToImage,
|
||||
kVkCopyQueryPoolResults,
|
||||
|
|
@ -382,6 +428,7 @@ class DeferredCommandBuffer {
|
|||
kVkEndRendering,
|
||||
kVkPipelineBarrier,
|
||||
kVkPushConstants,
|
||||
kVkPushUniformBufferDescriptorSet,
|
||||
kVkResetQueryPool,
|
||||
kVkSetBlendConstants,
|
||||
kVkSetDepthBias,
|
||||
|
|
@ -477,6 +524,15 @@ class DeferredCommandBuffer {
|
|||
static_assert(alignof(VkImageSubresourceRange) <= alignof(uintmax_t));
|
||||
};
|
||||
|
||||
struct ArgsVkClearDepthStencilImage {
|
||||
VkImage image;
|
||||
VkImageLayout image_layout;
|
||||
VkClearDepthStencilValue depth_stencil;
|
||||
uint32_t range_count;
|
||||
// Followed by aligned VkImageSubresourceRange[].
|
||||
static_assert(alignof(VkImageSubresourceRange) <= alignof(uintmax_t));
|
||||
};
|
||||
|
||||
struct ArgsVkCopyBuffer {
|
||||
VkBuffer src_buffer;
|
||||
VkBuffer dst_buffer;
|
||||
|
|
@ -564,6 +620,15 @@ class DeferredCommandBuffer {
|
|||
// Followed by `size` bytes of values.
|
||||
};
|
||||
|
||||
struct ArgsVkPushUniformBufferDescriptorSet {
|
||||
VkPipelineBindPoint pipeline_bind_point;
|
||||
VkPipelineLayout layout;
|
||||
uint32_t set;
|
||||
uint32_t buffer_count;
|
||||
// Followed by aligned VkDescriptorBufferInfo[buffer_count].
|
||||
static_assert(alignof(VkDescriptorBufferInfo) <= alignof(uintmax_t));
|
||||
};
|
||||
|
||||
struct ArgsVkSetBlendConstants {
|
||||
float blend_constants[4];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -127,6 +127,13 @@ class VulkanRenderTargetCache final : public RenderTargetCache {
|
|||
uint32_t* color_attachment_count_out,
|
||||
VkRenderingAttachmentInfo* depth_attachment,
|
||||
VkRenderingAttachmentInfo* stencil_attachment) const;
|
||||
// To be called by the command processor right after entering the render pass
|
||||
// for guest draws - replays the deferred resolve clears pending on the bound
|
||||
// render targets with vkCmdClearAttachments inside that render pass.
|
||||
// render_area_extent is the render area of the entered render pass which the
|
||||
// clear rectangles must be contained in (entries that don't fit are kept
|
||||
// deferred).
|
||||
void FlushDeferredResolveClearsIntoEnteredRenderPass(const VkExtent2D& render_area_extent);
|
||||
|
||||
// Using R16G16[B16A16]_SNORM, which are -1...1, not the needed -32...32.
|
||||
// Persistent data doesn't depend on this, so can be overriden by per-game
|
||||
|
|
@ -372,6 +379,18 @@ class VulkanRenderTargetCache final : public RenderTargetCache {
|
|||
uint32_t temporary_sort_index() const { return temporary_sort_index_; }
|
||||
void SetTemporarySortIndex(uint32_t index) { temporary_sort_index_ = index; }
|
||||
|
||||
// Resolve clears deferred until the next render pass binding this render
|
||||
// target, or a standalone flush if its contents are needed earlier.
|
||||
struct DeferredClear {
|
||||
Transfer::Rectangle rectangle;
|
||||
uint64_t clear_value;
|
||||
// Absolute EDRAM tile range (may wrap across the addressing period)
|
||||
// conservatively covering the rectangle, for read intersection checks.
|
||||
uint32_t tiles_start;
|
||||
uint32_t tiles_length;
|
||||
};
|
||||
std::vector<DeferredClear>& deferred_clears() { return deferred_clears_; }
|
||||
|
||||
private:
|
||||
VulkanRenderTargetCache& render_target_cache_;
|
||||
|
||||
|
|
@ -396,6 +415,8 @@ class VulkanRenderTargetCache final : public RenderTargetCache {
|
|||
|
||||
// Temporary storage for indices in operations like transfers and dumps.
|
||||
uint32_t temporary_sort_index_ = 0;
|
||||
|
||||
std::vector<DeferredClear> deferred_clears_;
|
||||
};
|
||||
|
||||
struct FramebufferKey {
|
||||
|
|
@ -769,6 +790,13 @@ class VulkanRenderTargetCache final : public RenderTargetCache {
|
|||
uint32_t source_base_tiles;
|
||||
uint32_t source_pitch_tiles;
|
||||
uint32_t dispatch_first_tile;
|
||||
// Resolve rectangle height in pixels (the width is in
|
||||
// resolve.dest_relative.coordinate_info, but the height is normally only
|
||||
// implied by the dispatch size).
|
||||
uint32_t height_pixels;
|
||||
// How to pack the sampled host render target texel into the raw 32bpp
|
||||
// guest dword: 0 = unorm8x4 (k_8_8_8_8 family), 1 = unorm 2_10_10_10.
|
||||
uint32_t source_format_case;
|
||||
};
|
||||
|
||||
struct DirectResolvePipelineKey {
|
||||
|
|
@ -813,9 +841,43 @@ class VulkanRenderTargetCache final : public RenderTargetCache {
|
|||
|
||||
VkPipeline GetDumpPipeline(DumpPipelineKey key);
|
||||
VkPipeline GetDirectResolvePipeline(DirectResolvePipelineKey key);
|
||||
// Compute pipeline reading a 32bpp color or unorm24 depth / stencil host
|
||||
// render target (single-sampled, native 2x MSAA or 4x MSAA) and writing the
|
||||
// raw resolve destination directly, skipping the EDRAM buffer round trip.
|
||||
// VK_NULL_HANDLE if unsupported or creation failed.
|
||||
VkPipeline GetDirectResolveFast32Pipeline(bool is_depth, xenos::MsaaSamples source_msaa_samples);
|
||||
// When this returns true, the destination has been fully written - the
|
||||
// caller must skip both the render target dump and the EDRAM buffer copy.
|
||||
bool TryResolveCopyDirectly(const draw_util::ResolveInfo& resolve_info,
|
||||
draw_util::ResolveCopyShaderIndex copy_shader,
|
||||
bool draw_resolution_scaled);
|
||||
bool draw_resolution_scaled, VulkanSharedMemory& shared_memory,
|
||||
const draw_util::ResolveCopyShaderConstants& copy_shader_constants);
|
||||
|
||||
// Deferred resolve clears: instead of an own render pass with a rectangle
|
||||
// attachment clear per resolve, clears are accumulated on the render target
|
||||
// and replayed with vkCmdClearAttachments inside the next render pass that
|
||||
// binds it for drawing (via FlushDeferredResolveClearsIntoEnteredRenderPass)
|
||||
// or in a standalone ownership transfer render pass if something needs the
|
||||
// render target's contents before then. This removes the per-resolve render
|
||||
// pass and layout transition overhead, which is a large fixed cost on some
|
||||
// hosts.
|
||||
void DeferResolveClear(VulkanRenderTarget& render_target,
|
||||
const Transfer::Rectangle& clear_rectangle, uint64_t clear_value);
|
||||
// Emits the pending deferred clears of the render target in a standalone
|
||||
// ownership transfer render pass.
|
||||
void FlushDeferredResolveClears(VulkanRenderTarget& render_target);
|
||||
// Flushes the pending deferred clears of any render target whose deferred
|
||||
// tile ranges intersect the given absolute EDRAM tile range that's about to
|
||||
// be read (such as by a resolve dump).
|
||||
void FlushDeferredResolveClearsBeforeEdramRead(uint32_t base_tiles, uint32_t length_tiles);
|
||||
// Converts the guest resolve clear value the same way for all the clear
|
||||
// paths. through_transfer_view selects between the image's own format (used
|
||||
// when clearing through the guest render pass attachment) and the ownership
|
||||
// transfer view format (uint for 16- and 32-bit-per-component color).
|
||||
VkClearDepthStencilValue GetResolveClearDepthStencilValue(RenderTargetKey key,
|
||||
uint64_t clear_value) const;
|
||||
VkClearColorValue GetResolveClearColorValue(RenderTargetKey key, uint64_t clear_value,
|
||||
bool through_transfer_view) const;
|
||||
|
||||
// Writes contents of host render targets within rectangles from
|
||||
// ResolveInfo::GetCopyEdramTileSpan to edram_buffer_.
|
||||
|
|
@ -867,6 +929,21 @@ class VulkanRenderTargetCache final : public RenderTargetCache {
|
|||
VkPipelineLayout direct_resolve_pipeline_layout_depth_ = VK_NULL_HANDLE;
|
||||
std::unordered_map<DirectResolvePipelineKey, VkPipeline, DirectResolvePipelineKey::Hasher>
|
||||
direct_resolve_pipelines_;
|
||||
// Lazily created (compiled from GLSL at runtime); the attempt flags prevent
|
||||
// retrying the compilation every resolve after a failure. Indexed
|
||||
// [is_depth][uint32_t(source MsaaSamples)].
|
||||
VkPipeline direct_resolve_fast32_pipelines_[2][3] = {};
|
||||
bool direct_resolve_fast32_pipelines_attempted_[2][3] = {};
|
||||
|
||||
// Render targets that currently have deferred resolve clears pending.
|
||||
std::vector<VulkanRenderTarget*> deferred_clear_render_targets_;
|
||||
// Temporary storage for FlushDeferredResolveClearsBeforeEdramRead.
|
||||
std::vector<VulkanRenderTarget*> flush_deferred_clear_temp_;
|
||||
// Deferred resolve clear statistics, logged periodically for diagnostics.
|
||||
uint64_t deferred_clear_stat_deferred_ = 0;
|
||||
uint64_t deferred_clear_stat_pass_entry_flushed_ = 0;
|
||||
uint64_t deferred_clear_stat_standalone_flushed_ = 0;
|
||||
uint64_t deferred_clear_stat_immediate_ = 0;
|
||||
|
||||
// Temporary storage for Resolve.
|
||||
std::vector<Transfer> clear_transfers_[2];
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@
|
|||
*/
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
|
@ -21,6 +23,7 @@
|
|||
#include <rex/graphics/vulkan/shared_memory.h>
|
||||
#include <rex/hash.h>
|
||||
#include <rex/ui/vulkan/mem_alloc.h>
|
||||
#include <rex/ui/vulkan/single_layout_descriptor_set_pool.h>
|
||||
|
||||
namespace rex::graphics::vulkan {
|
||||
|
||||
|
|
@ -198,6 +201,8 @@ class VulkanTextureCache final : public TextureCache {
|
|||
enum class Usage {
|
||||
kUndefined,
|
||||
kTransferDestination,
|
||||
// Direct load shader writes through a storage image view.
|
||||
kStorageWrite,
|
||||
kGuestShaderSampled,
|
||||
kSwapSampled,
|
||||
};
|
||||
|
|
@ -220,6 +225,14 @@ class VulkanTextureCache final : public TextureCache {
|
|||
VkImageView GetView(bool is_signed, uint32_t host_swizzle, bool is_array = true);
|
||||
VkImageView GetOrCreate3DAs2DImageView(bool is_signed, uint32_t host_swizzle);
|
||||
|
||||
// Descriptor set with an R32_UINT storage image view of mip 0 layer 0, for
|
||||
// direct load shader writes. VK_NULL_HANDLE if unavailable (the image was
|
||||
// not created with storage usage, or creation failed). Created lazily,
|
||||
// released in the destructor.
|
||||
VkDescriptorSet GetStorageDescriptorSet();
|
||||
bool image_storage_capable() const { return image_storage_capable_; }
|
||||
void SetImageStorageCapable(bool capable) { image_storage_capable_ = capable; }
|
||||
|
||||
private:
|
||||
union ViewKey {
|
||||
uint32_t key;
|
||||
|
|
@ -277,6 +290,13 @@ class VulkanTextureCache final : public TextureCache {
|
|||
std::unique_ptr<VulkanTexture> texture_3d_as_2d_;
|
||||
VkImageView image_view_3d_as_2d_unsigned_ = VK_NULL_HANDLE;
|
||||
VkImageView image_view_3d_as_2d_signed_ = VK_NULL_HANDLE;
|
||||
|
||||
// Whether the image was created with storage usage and an R32_UINT view
|
||||
// format for direct load shader writes.
|
||||
bool image_storage_capable_ = false;
|
||||
VkImageView storage_view_ = VK_NULL_HANDLE;
|
||||
size_t storage_descriptor_set_index_ = SIZE_MAX;
|
||||
bool storage_binding_attempted_ = false;
|
||||
};
|
||||
|
||||
static const char* DebugVulkanTextureUsageName(VulkanTexture::Usage usage);
|
||||
|
|
@ -360,6 +380,48 @@ class VulkanTextureCache final : public TextureCache {
|
|||
std::array<VkPipeline, kLoadShaderCount> load_pipelines_{};
|
||||
std::array<VkPipeline, kLoadShaderCount> load_pipelines_scaled_{};
|
||||
|
||||
// Texel-buffer-source variants of hot load pipelines: the source is read
|
||||
// through a uniform texel buffer (the texture path with its caching)
|
||||
// instead of raw storage buffer loads, which is several times faster for
|
||||
// the scattered tiled access pattern of guest texture loading on some
|
||||
// hosts. Lazily compiled from GLSL ports of the corresponding shipped
|
||||
// shaders; the attempt flags prevent retrying after a failure.
|
||||
VkDescriptorSetLayout load_descriptor_set_layout_source_texel_ = VK_NULL_HANDLE;
|
||||
VkPipelineLayout load_pipeline_layout_texel_ = VK_NULL_HANDLE;
|
||||
std::array<VkPipeline, kLoadShaderCount> load_pipelines_scaled_texel_{};
|
||||
std::array<bool, kLoadShaderCount> load_pipelines_scaled_texel_attempted_{};
|
||||
VkPipeline GetLoadPipelineScaledTexelBuffer(LoadShaderIndex load_shader);
|
||||
|
||||
// Storage-image-destination variants of hot load pipelines: the load shader
|
||||
// writes the texture directly through an R32_UINT storage image view,
|
||||
// skipping the intermediate copy buffer and the buffer-to-image copy (which
|
||||
// is several times slower than device bandwidth on some hosts). Set 0 is
|
||||
// the storage image destination, set 1 the storage buffer source.
|
||||
VkDescriptorSetLayout load_descriptor_set_layout_dest_storage_ = VK_NULL_HANDLE;
|
||||
VkPipelineLayout load_pipeline_layout_storage_dest_ = VK_NULL_HANDLE;
|
||||
std::array<VkPipeline, kLoadShaderCount> load_pipelines_scaled_storage_dest_{};
|
||||
std::array<bool, kLoadShaderCount> load_pipelines_scaled_storage_dest_attempted_{};
|
||||
VkPipeline GetLoadPipelineScaledStorageDest(LoadShaderIndex load_shader);
|
||||
std::unique_ptr<ui::vulkan::SingleLayoutDescriptorSetPool>
|
||||
load_dest_storage_descriptor_set_pool_;
|
||||
std::unique_ptr<ui::vulkan::SingleLayoutDescriptorSetPool>
|
||||
load_source_texel_descriptor_set_pool_;
|
||||
struct LoadSourceTexelBinding {
|
||||
VkBufferView buffer_view;
|
||||
size_t descriptor_set_index;
|
||||
};
|
||||
// Persistent cached buffer views and descriptor sets for texel-buffer load
|
||||
// sources, keyed by (buffer, view offset, view range) - the underlying
|
||||
// buffers (shared memory, scaled resolve) live for the cache's lifetime.
|
||||
std::map<std::tuple<uint64_t, uint64_t, uint64_t>, LoadSourceTexelBinding>
|
||||
load_source_texel_bindings_;
|
||||
// Returns the descriptor set (or VK_NULL_HANDLE on failure) for reading
|
||||
// [offset, offset + range) of the buffer as an RGBA32_UINT texel buffer.
|
||||
// The offset must be aligned to minTexelBufferOffsetAlignment and the range
|
||||
// to the texel size by the caller.
|
||||
VkDescriptorSet GetLoadSourceTexelBufferDescriptorSet(VkBuffer buffer, VkDeviceSize offset,
|
||||
VkDeviceSize range);
|
||||
|
||||
// If both images can be placed in the same allocation, it's one allocation,
|
||||
// otherwise it's two separate.
|
||||
std::array<VkDeviceMemory, 2> null_images_memory_{};
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
*/
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
|
|
@ -21,6 +22,14 @@
|
|||
#include <tracy/Tracy.hpp>
|
||||
#endif
|
||||
|
||||
#if defined(_M_X64) || defined(_M_IX86)
|
||||
#include <intrin.h>
|
||||
#define REX_PERF_CPU_PROFILE_RDTSC 1
|
||||
#elif defined(__x86_64__) || defined(__i386__)
|
||||
#include <x86intrin.h>
|
||||
#define REX_PERF_CPU_PROFILE_RDTSC 1
|
||||
#endif
|
||||
|
||||
namespace rex::perf {
|
||||
|
||||
enum class DrawBucket : uint8_t {
|
||||
|
|
@ -34,8 +43,66 @@ enum class DrawBucket : uint8_t {
|
|||
kCopyResolveFast32,
|
||||
kCopyResolveFull32,
|
||||
kResolveDownscale,
|
||||
// Guest render pass entry: ending the previous render pass, beginning the
|
||||
// new one and replaying deferred resolve clears. Not aggregated into the
|
||||
// draw/res/dump FPS overlay lines - shows up in "other".
|
||||
kRenderPassEntry,
|
||||
// Render target ownership transfers and resolve clears performed in
|
||||
// PerformTransfersAndResolveClears.
|
||||
kRtTransfer,
|
||||
// Texture cache uploads (guest texture data load compute dispatches).
|
||||
kTextureUpload,
|
||||
// Shared memory (guest memory mirror buffer) uploads.
|
||||
kSharedMemoryUpload,
|
||||
// From the end of a resolve operation until the next instrumented point -
|
||||
// the post-resolve barriers and any idle gap before the next draws.
|
||||
kResolveGap,
|
||||
// The buffer-to-image copy phase of a texture upload (kTextureUpload then
|
||||
// covers only the load compute dispatches and their barriers).
|
||||
kTextureUploadCopy,
|
||||
kCount,
|
||||
};
|
||||
|
||||
// Short display name of a draw bucket (for logs and capture CSVs).
|
||||
inline const char* DrawBucketName(DrawBucket bucket) {
|
||||
switch (bucket) {
|
||||
case DrawBucket::kMainColorDepth:
|
||||
return "Main";
|
||||
case DrawBucket::kDepthOnly:
|
||||
return "Depth";
|
||||
case DrawBucket::kCopyResolve:
|
||||
return "Copy";
|
||||
case DrawBucket::kMemexport:
|
||||
return "MemExp";
|
||||
case DrawBucket::kNoPixelShader:
|
||||
return "NoPS";
|
||||
case DrawBucket::kCopyDump:
|
||||
return "CopyDump";
|
||||
case DrawBucket::kCopyResolveShader:
|
||||
return "CopyResolve";
|
||||
case DrawBucket::kCopyResolveFast32:
|
||||
return "CopyResolveFast32";
|
||||
case DrawBucket::kCopyResolveFull32:
|
||||
return "CopyResolveFull32";
|
||||
case DrawBucket::kResolveDownscale:
|
||||
return "ResolveDownscale";
|
||||
case DrawBucket::kRenderPassEntry:
|
||||
return "PassEntry";
|
||||
case DrawBucket::kRtTransfer:
|
||||
return "RtTransfer";
|
||||
case DrawBucket::kTextureUpload:
|
||||
return "TexUpload";
|
||||
case DrawBucket::kSharedMemoryUpload:
|
||||
return "SMUpload";
|
||||
case DrawBucket::kResolveGap:
|
||||
return "ResolveGap";
|
||||
case DrawBucket::kTextureUploadCopy:
|
||||
return "TexUpCopy";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
struct DrawFingerprint {
|
||||
DrawBucket bucket = DrawBucket::kMainColorDepth;
|
||||
uint64_t vertex_shader_hash = 0;
|
||||
|
|
@ -91,6 +158,13 @@ enum class CounterId : uint16_t {
|
|||
kDrawStageVertexBuffersUs,
|
||||
kDrawStageBarriersUs,
|
||||
kDrawStageSubmitUs,
|
||||
kDrawStageSystemConstantsUs,
|
||||
kDrawSamplerFastPathDraws,
|
||||
kDrawTextureFastPathDraws,
|
||||
kDrawStageAnalyzeUs,
|
||||
kDrawStageTranslateUs,
|
||||
kDrawStageSamplersUs,
|
||||
kDrawStagePreDrawUs,
|
||||
kGpuMainUs,
|
||||
kGpuDepthUs,
|
||||
kGpuCopyUs,
|
||||
|
|
@ -113,6 +187,8 @@ enum class CounterId : uint16_t {
|
|||
kGpuResolveQueryUs,
|
||||
kCpuPrimaryBufferUs,
|
||||
kCpuIndirectBufferUs,
|
||||
kCpuGuestWaitUs,
|
||||
kCpuSwapUs,
|
||||
kCpuD3D12BeginSubmissionUs,
|
||||
kCpuD3D12BeginSubmissionFenceUs,
|
||||
kCpuD3D12BeginSubmissionFrameOpenUs,
|
||||
|
|
@ -135,6 +211,10 @@ enum class CounterId : uint16_t {
|
|||
kCpuD3D12PaintUiUs,
|
||||
kCpuD3D12PresentWaitUs,
|
||||
kCpuD3D12PresentUs,
|
||||
// Time the GPU emulation thread spent blocked waiting for submission
|
||||
// fences (both backends) - high values mean GPU-bound, near zero means the
|
||||
// thread itself is the bottleneck.
|
||||
kGpuThreadFenceWaitUs,
|
||||
kTextureRequestUs,
|
||||
kTextureRequestCalls,
|
||||
kTextureFetchesRequested,
|
||||
|
|
@ -243,6 +323,50 @@ enum class CounterId : uint16_t {
|
|||
kCount // sentinel -- must be last
|
||||
};
|
||||
|
||||
// Lightweight CPU-side scope attribution for perf-counter builds. When enabled
|
||||
// at runtime (the gpu_cpu_profile cvar), every ScopedCounterTimer scope
|
||||
// accumulates raw timestamp ticks and a hit count per CounterId; the command
|
||||
// processor converts ticks to microseconds against a wall-clock calibration
|
||||
// window and logs the breakdown periodically at frame end. Constraint: each
|
||||
// CounterId may only be written from one thread (slots are not atomic).
|
||||
namespace cpu_profile {
|
||||
|
||||
inline uint64_t Ticks() {
|
||||
#ifdef REX_PERF_CPU_PROFILE_RDTSC
|
||||
return __rdtsc();
|
||||
#else
|
||||
return uint64_t(std::chrono::steady_clock::now().time_since_epoch().count());
|
||||
#endif
|
||||
}
|
||||
|
||||
struct Slot {
|
||||
uint64_t ticks;
|
||||
uint64_t count;
|
||||
};
|
||||
|
||||
inline std::atomic<bool> enabled{false};
|
||||
inline Slot slots[size_t(CounterId::kCount)]{};
|
||||
|
||||
inline void Reset() {
|
||||
for (Slot& slot : slots) {
|
||||
slot.ticks = 0;
|
||||
slot.count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Count an event (no time attribution) when the CPU profile is armed.
|
||||
inline void CountEvent(CounterId id) {
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
if (enabled.load(std::memory_order_relaxed)) {
|
||||
++slots[size_t(id)].count;
|
||||
}
|
||||
#else
|
||||
(void)id;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace cpu_profile
|
||||
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
|
||||
// Returns human-readable name for a counter (e.g. "frame_time_us")
|
||||
|
|
@ -294,6 +418,9 @@ class ScopedCounterTimer {
|
|||
if (active_) {
|
||||
start_ = Clock::now();
|
||||
}
|
||||
if (cpu_profile::enabled.load(std::memory_order_relaxed)) {
|
||||
cpu_profile_start_ticks_ = cpu_profile::Ticks();
|
||||
}
|
||||
}
|
||||
|
||||
~ScopedCounterTimer() {
|
||||
|
|
@ -301,6 +428,12 @@ class ScopedCounterTimer {
|
|||
}
|
||||
|
||||
void Stop() {
|
||||
if (cpu_profile_start_ticks_) {
|
||||
cpu_profile::Slot& slot = cpu_profile::slots[size_t(counter_id_)];
|
||||
slot.ticks += cpu_profile::Ticks() - cpu_profile_start_ticks_;
|
||||
++slot.count;
|
||||
cpu_profile_start_ticks_ = 0;
|
||||
}
|
||||
if (!active_) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -318,6 +451,7 @@ class ScopedCounterTimer {
|
|||
CounterId counter_id_;
|
||||
bool active_;
|
||||
Clock::time_point start_;
|
||||
uint64_t cpu_profile_start_ticks_ = 0;
|
||||
};
|
||||
|
||||
// Read a counter's current live value
|
||||
|
|
@ -406,6 +540,9 @@ class ScopedCounterTimer {
|
|||
(void)counter_id;
|
||||
(void)active;
|
||||
}
|
||||
|
||||
~ScopedCounterTimer() = default;
|
||||
|
||||
void Stop() {}
|
||||
|
||||
ScopedCounterTimer(const ScopedCounterTimer&) = delete;
|
||||
|
|
@ -487,7 +624,12 @@ class Profiler {
|
|||
|
||||
} // namespace rex::perf
|
||||
|
||||
// Perf counter macros -- compile to no-ops when counters are disabled.
|
||||
// Perf counter macros -- compile to no-ops when counters are disabled. CPU
|
||||
// profile scope timers are perf-build-only so shipped builds do not pay
|
||||
// per-scope constructor/destructor or atomic-check costs.
|
||||
#define REX_PERF_CONCAT_INNER(a, b) a##b
|
||||
#define REX_PERF_CONCAT(a, b) REX_PERF_CONCAT_INNER(a, b)
|
||||
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
|
||||
// Generic helpers for easily adding new counters
|
||||
|
|
@ -510,8 +652,6 @@ class Profiler {
|
|||
#define PROFILE_DRAW_FINGERPRINT(fingerprint) rex::perf::RecordDrawFingerprint(fingerprint)
|
||||
#define PROFILE_DRAW_STAGE_US(id, delta) rex::perf::IncrementCounter((id), (delta))
|
||||
#define PROFILE_GPU_TIMESTAMPED_DRAW() PERF_counter_inc(kGpuTimestampedDraws)
|
||||
#define REX_PERF_CONCAT_INNER(a, b) a##b
|
||||
#define REX_PERF_CONCAT(a, b) REX_PERF_CONCAT_INNER(a, b)
|
||||
#define PROFILE_SCOPE_COUNTER(id) \
|
||||
rex::perf::ScopedCounterTimer REX_PERF_CONCAT(perf_scope_timer_, __LINE__)(rex::perf::CounterId::id)
|
||||
#define PROFILE_SCOPE_COUNTER_IF(id, active) \
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ struct PathConfig {
|
|||
|
||||
namespace ui {
|
||||
class ConsoleDialog;
|
||||
class FpsOverlayDialog;
|
||||
class SettingsDialog;
|
||||
} // namespace ui
|
||||
|
||||
|
|
@ -238,6 +239,7 @@ class ReXApp : public ui::WindowedApp, public ui::WindowListener, public ui::Win
|
|||
std::unique_ptr<ui::DebugOverlayDialog> debug_overlay_;
|
||||
std::unique_ptr<ui::ConsoleDialog> console_overlay_;
|
||||
std::unique_ptr<ui::SettingsDialog> settings_overlay_;
|
||||
std::unique_ptr<ui::FpsOverlayDialog> fps_overlay_;
|
||||
ui::DebugOverlayDialog::FrameStatsProvider frame_stats_provider_;
|
||||
std::filesystem::path config_path_;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -39,6 +39,14 @@ class ImGuiDialog {
|
|||
|
||||
void Draw();
|
||||
|
||||
// Whether the dialog needs the UI to repaint continuously while it's open
|
||||
// (interactive dialogs - input handling, animations). Passive readouts
|
||||
// should return false so they only repaint when something else (such as a
|
||||
// new guest output frame) triggers a paint - a continuous repaint loop
|
||||
// presents without new guest frames, inflating externally measured FPS and
|
||||
// burning CPU/GPU.
|
||||
virtual bool WantsContinuousRepaint() const { return true; }
|
||||
|
||||
protected:
|
||||
ImGuiDialog(ImGuiDrawer* imgui_drawer);
|
||||
|
||||
|
|
|
|||
36
include/rex/ui/overlay/fps_overlay.h
Normal file
36
include/rex/ui/overlay/fps_overlay.h
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* @file rex/ui/overlay/fps_overlay.h
|
||||
*
|
||||
* @brief Minimal guest FPS readout overlay.
|
||||
*
|
||||
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* @license BSD 3-Clause License
|
||||
* See LICENSE file in the project root for full license text.
|
||||
*/
|
||||
#pragma once
|
||||
#include <rex/ui/imgui_dialog.h>
|
||||
#include <rex/ui/presenter.h>
|
||||
|
||||
namespace rex::ui {
|
||||
|
||||
// Compact corner readout of the guest frame rate measured by the presenter.
|
||||
// Unlike the interactive overlays, this one does not request continuous
|
||||
// repaints, so host presents stay paced by guest output refreshes and
|
||||
// external FPS measurement tools (e.g. RTSS) remain accurate while it's open.
|
||||
class FpsOverlayDialog : public ImGuiDialog {
|
||||
public:
|
||||
FpsOverlayDialog(ImGuiDrawer* imgui_drawer, const Presenter* presenter)
|
||||
: ImGuiDialog(imgui_drawer), presenter_(presenter) {}
|
||||
|
||||
bool WantsContinuousRepaint() const override { return false; }
|
||||
|
||||
protected:
|
||||
void OnDraw(ImGuiIO& io) override;
|
||||
|
||||
private:
|
||||
const Presenter* presenter_;
|
||||
};
|
||||
|
||||
} // namespace rex::ui
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
|
|
@ -339,6 +340,67 @@ class Presenter {
|
|||
bool RefreshGuestOutput(uint32_t frontbuffer_width, uint32_t frontbuffer_height,
|
||||
uint32_t display_aspect_ratio_x, uint32_t display_aspect_ratio_y,
|
||||
std::function<bool(GuestOutputRefreshContext& context)> refresher);
|
||||
|
||||
// Guest frame pacing measured at RefreshGuestOutput - one sample per guest
|
||||
// swap, independent of how many times the host paints (UI repaints while a
|
||||
// dialog is open present without a new guest frame, so host present rate is
|
||||
// not a valid guest FPS measurement). Thread-safe.
|
||||
struct GuestFrameStats {
|
||||
// Averaged over the sampling window (zero when no recent frames).
|
||||
double fps = 0.0;
|
||||
double frame_time_ms = 0.0;
|
||||
// Time the guest output producer (GPU emulation thread) reported spending
|
||||
// blocked on host GPU waits during the last frame, via
|
||||
// AddGuestFrameWaitMicroseconds. Near frame_time_ms = host GPU bound,
|
||||
// near zero = producer thread bound.
|
||||
double wait_ms = 0.0;
|
||||
// Host GPU time span of the most recently measured guest frame (first to
|
||||
// last command on the device timeline), via
|
||||
// SetGuestFrameGpuSpanMicroseconds. Zero when the backend doesn't report
|
||||
// it.
|
||||
double gpu_ms = 0.0;
|
||||
// Optional GPU time breakdown of the measured frame (guest draws,
|
||||
// resolves, render target dumps), via
|
||||
// SetGuestFrameGpuBreakdownMicroseconds. All zero when the backend isn't
|
||||
// measuring buckets.
|
||||
double gpu_draw_ms = 0.0;
|
||||
double gpu_resolve_ms = 0.0;
|
||||
double gpu_dump_ms = 0.0;
|
||||
// Total guest frames since startup.
|
||||
uint64_t frame_count = 0;
|
||||
};
|
||||
GuestFrameStats GetGuestFrameStats() const;
|
||||
bool GuestFrameStatsEnabled() const {
|
||||
return guest_frame_stats_enabled_.load(std::memory_order_relaxed);
|
||||
}
|
||||
void SetGuestFrameStatsEnabled(bool enabled);
|
||||
// Reports time the guest output producer spent blocked waiting for the host
|
||||
// GPU; accumulated into the current frame's stats and reset at the next
|
||||
// RefreshGuestOutput. Thread-safe.
|
||||
void AddGuestFrameWaitMicroseconds(uint64_t wait_us) {
|
||||
if (!GuestFrameStatsEnabled()) {
|
||||
return;
|
||||
}
|
||||
guest_frame_wait_accumulator_us_.fetch_add(wait_us, std::memory_order_relaxed);
|
||||
}
|
||||
// Reports the host GPU time span of a completed guest frame (measured by
|
||||
// the backend, typically a few frames behind real time). Thread-safe.
|
||||
void SetGuestFrameGpuSpanMicroseconds(uint64_t gpu_span_us) {
|
||||
if (!GuestFrameStatsEnabled()) {
|
||||
return;
|
||||
}
|
||||
guest_frame_gpu_span_us_.store(gpu_span_us, std::memory_order_relaxed);
|
||||
}
|
||||
// Reports the GPU time breakdown of a completed guest frame. Thread-safe.
|
||||
void SetGuestFrameGpuBreakdownMicroseconds(uint64_t draw_us, uint64_t resolve_us,
|
||||
uint64_t dump_us) {
|
||||
if (!GuestFrameStatsEnabled()) {
|
||||
return;
|
||||
}
|
||||
guest_frame_gpu_draw_us_.store(draw_us, std::memory_order_relaxed);
|
||||
guest_frame_gpu_resolve_us_.store(resolve_us, std::memory_order_relaxed);
|
||||
guest_frame_gpu_dump_us_.store(dump_us, std::memory_order_relaxed);
|
||||
}
|
||||
// The implementation must be callable from any thread, including from
|
||||
// multiple at the same time, and it should acquire the latest guest output
|
||||
// image via ConsumeGuestOutput.
|
||||
|
|
@ -968,6 +1030,26 @@ class Presenter {
|
|||
// rather than being blank.
|
||||
bool guest_output_active_last_refresh_ = false;
|
||||
|
||||
// Guest frame timestamps for GetGuestFrameStats, written by the refreshing
|
||||
// thread, read by any thread. Sized to cover the stats window even at very
|
||||
// high frame rates.
|
||||
static constexpr size_t kGuestFrameTimestampCount = 512;
|
||||
std::atomic<bool> guest_frame_stats_enabled_{false};
|
||||
mutable std::mutex guest_frame_stats_mutex_;
|
||||
std::array<std::chrono::steady_clock::time_point, kGuestFrameTimestampCount>
|
||||
guest_frame_timestamps_{};
|
||||
size_t guest_frame_timestamp_next_ = 0;
|
||||
size_t guest_frame_timestamp_count_ = 0;
|
||||
uint64_t guest_frame_total_count_ = 0;
|
||||
std::atomic<uint64_t> guest_frame_wait_accumulator_us_{0};
|
||||
// Captured from the accumulator at each guest output refresh; guarded by
|
||||
// guest_frame_stats_mutex_.
|
||||
uint64_t guest_frame_last_wait_us_ = 0;
|
||||
std::atomic<uint64_t> guest_frame_gpu_span_us_{0};
|
||||
std::atomic<uint64_t> guest_frame_gpu_draw_us_{0};
|
||||
std::atomic<uint64_t> guest_frame_gpu_resolve_us_{0};
|
||||
std::atomic<uint64_t> guest_frame_gpu_dump_us_{0};
|
||||
|
||||
// Ordered by the Z order, and then by the time of addition.
|
||||
// Note: All the iteration logic involving this Z ordering must be the same as
|
||||
// in input handling (in the input listeners in the Window), but in reverse.
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ class VulkanDevice {
|
|||
uint32_t maxViewportDimensions[2] = {4096, 4096};
|
||||
VkDeviceSize minUniformBufferOffsetAlignment = 256;
|
||||
VkDeviceSize minStorageBufferOffsetAlignment = 256;
|
||||
uint32_t maxTexelBufferElements = 65536;
|
||||
VkDeviceSize minTexelBufferOffsetAlignment = 256;
|
||||
uint32_t maxFramebufferWidth = 4096;
|
||||
uint32_t maxFramebufferHeight = 4096;
|
||||
VkSampleCountFlags framebufferColorSampleCounts = VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_4_BIT;
|
||||
|
|
@ -184,6 +186,7 @@ class VulkanDevice {
|
|||
// `ext_major_minor_` prefix rather than `ext_`).
|
||||
struct Extensions {
|
||||
bool ext_KHR_swapchain = false; // #2
|
||||
bool ext_KHR_push_descriptor = false; // #81
|
||||
bool ext_1_1_KHR_dedicated_allocation = false; // #128
|
||||
bool ext_EXT_shader_stencil_export = false; // #141
|
||||
bool ext_1_1_KHR_get_memory_requirements2 = false; // #147
|
||||
|
|
@ -214,6 +217,8 @@ class VulkanDevice {
|
|||
#include <rex/ui/vulkan/functions/device_1_0.inc>
|
||||
// VK_KHR_swapchain (#2)
|
||||
#include <rex/ui/vulkan/functions/device_khr_swapchain.inc>
|
||||
// VK_KHR_push_descriptor (#81)
|
||||
#include <rex/ui/vulkan/functions/device_khr_push_descriptor.inc>
|
||||
// VK_KHR_get_memory_requirements2 (#147, promoted to 1.1)
|
||||
#include <rex/ui/vulkan/functions/device_1_1_khr_get_memory_requirements2.inc>
|
||||
// VK_KHR_bind_memory2 (#158, promoted to 1.1)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ XE_UI_VULKAN_FUNCTION(vkCmdBindPipeline)
|
|||
XE_UI_VULKAN_FUNCTION(vkCmdBindVertexBuffers)
|
||||
XE_UI_VULKAN_FUNCTION(vkCmdClearAttachments)
|
||||
XE_UI_VULKAN_FUNCTION(vkCmdClearColorImage)
|
||||
XE_UI_VULKAN_FUNCTION(vkCmdClearDepthStencilImage)
|
||||
XE_UI_VULKAN_FUNCTION(vkCmdCopyBuffer)
|
||||
XE_UI_VULKAN_FUNCTION(vkCmdCopyBufferToImage)
|
||||
XE_UI_VULKAN_FUNCTION(vkCmdCopyImageToBuffer)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
// VK_KHR_push_descriptor functions used in Xenia.
|
||||
XE_UI_VULKAN_FUNCTION(vkCmdPushDescriptorSetKHR)
|
||||
|
|
@ -159,6 +159,13 @@ constexpr const char* kCounterNames[] = {
|
|||
"draw_stage_vertex_buffers_us",
|
||||
"draw_stage_barriers_us",
|
||||
"draw_stage_submit_us",
|
||||
"draw_stage_system_constants_us",
|
||||
"draw_sampler_fast_path_draws",
|
||||
"draw_texture_fast_path_draws",
|
||||
"draw_stage_analyze_us",
|
||||
"draw_stage_translate_us",
|
||||
"draw_stage_samplers_us",
|
||||
"draw_stage_pre_draw_us",
|
||||
"gpu_main_us",
|
||||
"gpu_depth_us",
|
||||
"gpu_copy_us",
|
||||
|
|
@ -181,6 +188,8 @@ constexpr const char* kCounterNames[] = {
|
|||
"gpu_resolve_query_us",
|
||||
"cpu_primary_buffer_us",
|
||||
"cpu_indirect_buffer_us",
|
||||
"cpu_guest_wait_us",
|
||||
"cpu_swap_us",
|
||||
"cpu_d3d12_begin_submission_us",
|
||||
"cpu_d3d12_begin_submission_fence_us",
|
||||
"cpu_d3d12_begin_submission_frame_open_us",
|
||||
|
|
@ -203,6 +212,7 @@ constexpr const char* kCounterNames[] = {
|
|||
"cpu_d3d12_paint_ui_us",
|
||||
"cpu_d3d12_present_wait_us",
|
||||
"cpu_d3d12_present_us",
|
||||
"gpu_thread_fence_wait_us",
|
||||
"texture_request_us",
|
||||
"texture_request_calls",
|
||||
"texture_fetches_requested",
|
||||
|
|
@ -337,6 +347,13 @@ constexpr bool kIsGauge[] = {
|
|||
false, // kDrawStageVertexBuffersUs
|
||||
false, // kDrawStageBarriersUs
|
||||
false, // kDrawStageSubmitUs
|
||||
false, // kDrawStageSystemConstantsUs
|
||||
false, // kDrawSamplerFastPathDraws
|
||||
false, // kDrawTextureFastPathDraws
|
||||
false, // kDrawStageAnalyzeUs
|
||||
false, // kDrawStageTranslateUs
|
||||
false, // kDrawStageSamplersUs
|
||||
false, // kDrawStagePreDrawUs
|
||||
false, // kGpuMainUs
|
||||
false, // kGpuDepthUs
|
||||
false, // kGpuCopyUs
|
||||
|
|
@ -359,6 +376,8 @@ constexpr bool kIsGauge[] = {
|
|||
false, // kGpuResolveQueryUs
|
||||
false, // kCpuPrimaryBufferUs
|
||||
false, // kCpuIndirectBufferUs
|
||||
false, // kCpuGuestWaitUs
|
||||
false, // kCpuSwapUs
|
||||
false, // kCpuD3D12BeginSubmissionUs
|
||||
false, // kCpuD3D12BeginSubmissionFenceUs
|
||||
false, // kCpuD3D12BeginSubmissionFrameOpenUs
|
||||
|
|
@ -381,6 +400,7 @@ constexpr bool kIsGauge[] = {
|
|||
false, // kCpuD3D12PaintUiUs
|
||||
false, // kCpuD3D12PresentWaitUs
|
||||
false, // kCpuD3D12PresentUs
|
||||
false, // kGpuThreadFenceWaitUs
|
||||
false, // kTextureRequestUs
|
||||
false, // kTextureRequestCalls
|
||||
false, // kTextureFetchesRequested
|
||||
|
|
@ -487,32 +507,7 @@ int g_csv_frame_count = 0;
|
|||
|
||||
} // anonymous namespace
|
||||
|
||||
const char* DrawBucketName(DrawBucket bucket) {
|
||||
switch (bucket) {
|
||||
case DrawBucket::kMainColorDepth:
|
||||
return "Main";
|
||||
case DrawBucket::kDepthOnly:
|
||||
return "Depth";
|
||||
case DrawBucket::kCopyResolve:
|
||||
return "Copy";
|
||||
case DrawBucket::kMemexport:
|
||||
return "MemExp";
|
||||
case DrawBucket::kNoPixelShader:
|
||||
return "NoPS";
|
||||
case DrawBucket::kCopyDump:
|
||||
return "CopyDump";
|
||||
case DrawBucket::kCopyResolveShader:
|
||||
return "CopyResolve";
|
||||
case DrawBucket::kCopyResolveFast32:
|
||||
return "CopyResolveFast32";
|
||||
case DrawBucket::kCopyResolveFull32:
|
||||
return "CopyResolveFull32";
|
||||
case DrawBucket::kResolveDownscale:
|
||||
return "ResolveDownscale";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
// DrawBucketName is defined inline in counter.h.
|
||||
|
||||
const char* CounterName(CounterId id) {
|
||||
auto idx = static_cast<size_t>(id);
|
||||
|
|
|
|||
|
|
@ -109,6 +109,13 @@ REXCVAR_DEFINE_BOOL(async_shader_compilation, true, "GPU",
|
|||
"pipelines are being prepared.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
REXCVAR_DEFINE_BOOL(gpu_cpu_profile, false, "GPU",
|
||||
"Log per-frame CPU time attribution for the GPU emulation thread "
|
||||
"(packet processing, draw stages, submission) every 120 frames")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
#endif
|
||||
|
||||
namespace rex::graphics {
|
||||
|
||||
using namespace rex::graphics::xenos;
|
||||
|
|
@ -1132,12 +1139,104 @@ bool CommandProcessor::ExecutePacketType3_XE_SWAP(memory::RingBuffer* reader, ui
|
|||
uint32_t frontbuffer_height = reader->ReadAndSwap<uint32_t>();
|
||||
reader->AdvanceRead((count - 4) * sizeof(uint32_t));
|
||||
|
||||
IssueSwap(frontbuffer_ptr, frontbuffer_width, frontbuffer_height);
|
||||
{
|
||||
PROFILE_SCOPE_COUNTER(kCpuSwapUs);
|
||||
IssueSwap(frontbuffer_ptr, frontbuffer_width, frontbuffer_height);
|
||||
}
|
||||
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
CpuProfileOnFrameEnd();
|
||||
#endif
|
||||
|
||||
++counter_;
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
void CommandProcessor::CpuProfileOnFrameEnd() {
|
||||
namespace cpu_profile = rex::perf::cpu_profile;
|
||||
if (!REXCVAR_GET(gpu_cpu_profile)) {
|
||||
if (cpu_profile::enabled.load(std::memory_order_relaxed)) {
|
||||
cpu_profile::enabled.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const uint64_t now_ticks = cpu_profile::Ticks();
|
||||
const std::chrono::steady_clock::time_point now_time = std::chrono::steady_clock::now();
|
||||
if (!cpu_profile::enabled.load(std::memory_order_relaxed)) {
|
||||
// Newly armed - start accumulating from the next frame.
|
||||
cpu_profile::enabled.store(true, std::memory_order_relaxed);
|
||||
cpu_profile::Reset();
|
||||
cpu_profile_frames_ = 0;
|
||||
cpu_profile_window_start_ticks_ = now_ticks;
|
||||
cpu_profile_window_start_time_ = now_time;
|
||||
REXGPU_INFO("CPU profile armed (gpu_cpu_profile): logging every {} frames",
|
||||
kCpuProfileLogFrameInterval);
|
||||
return;
|
||||
}
|
||||
if (++cpu_profile_frames_ < kCpuProfileLogFrameInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Scope timers record raw TSC ticks; calibrate them against the wall-clock
|
||||
// duration of the whole logging window.
|
||||
const int64_t window_us = std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
now_time - cpu_profile_window_start_time_)
|
||||
.count();
|
||||
const uint64_t window_ticks = now_ticks - cpu_profile_window_start_ticks_;
|
||||
const uint32_t frames = cpu_profile_frames_;
|
||||
if (window_us > 0 && window_ticks > 0) {
|
||||
const double us_per_tick = double(window_us) / double(window_ticks);
|
||||
using rex::perf::CounterId;
|
||||
auto avg_us = [&](CounterId id) -> uint64_t {
|
||||
return uint64_t(double(cpu_profile::slots[size_t(id)].ticks) * us_per_tick) / frames;
|
||||
};
|
||||
auto avg_count = [&](CounterId id) -> uint64_t {
|
||||
return cpu_profile::slots[size_t(id)].count / frames;
|
||||
};
|
||||
// Nesting: everything below is inside pm4. copy is inside draw (resolves
|
||||
// are initiated by draw packets); sub_begin/fence waits happen inside
|
||||
// draws; sub_end happens inside swap (frame-ending submission) unless
|
||||
// submitting per primary buffer end.
|
||||
const uint64_t pm4_us = avg_us(CounterId::kCpuPrimaryBufferUs);
|
||||
const uint64_t guest_wait_us = avg_us(CounterId::kCpuGuestWaitUs);
|
||||
const uint64_t draw_us = avg_us(CounterId::kDrawStageTotalUs);
|
||||
const uint64_t swap_us = avg_us(CounterId::kCpuSwapUs);
|
||||
const uint64_t accounted_us = guest_wait_us + draw_us + swap_us;
|
||||
REXGPU_INFO(
|
||||
"CPU profile (avg us/frame over {} frames): frame={} pm4={} draws={} | draw={} [anlz={} "
|
||||
"prim={} xlat={} smp={} rt={} pso={} tex={} fixfn={} sysconst={} bind={} vb={} barrier={} "
|
||||
"pre={} subm={}] copy={} ({}x) | sub_begin={} [fence_check={} fence_wait={}] sub_end={} "
|
||||
"({}x) [pipe_end={} barriers={} record={} close={} qsubmit={} signal={}] | swap={} "
|
||||
"guest_wait={} pm4_other={} | fast_smp={} fast_tex={}",
|
||||
frames, window_us / frames, pm4_us, avg_count(CounterId::kDrawStageTotalUs), draw_us,
|
||||
avg_us(CounterId::kDrawStageAnalyzeUs), avg_us(CounterId::kDrawStagePrimitiveUs),
|
||||
avg_us(CounterId::kDrawStageTranslateUs), avg_us(CounterId::kDrawStageSamplersUs),
|
||||
avg_us(CounterId::kDrawStageRenderTargetUs),
|
||||
avg_us(CounterId::kDrawStagePipelineUs), avg_us(CounterId::kDrawStageTextureUs),
|
||||
avg_us(CounterId::kDrawStageFixedFunctionUs),
|
||||
avg_us(CounterId::kDrawStageSystemConstantsUs), avg_us(CounterId::kDrawStageBindingsUs),
|
||||
avg_us(CounterId::kDrawStageVertexBuffersUs), avg_us(CounterId::kDrawStageBarriersUs),
|
||||
avg_us(CounterId::kDrawStagePreDrawUs), avg_us(CounterId::kDrawStageSubmitUs),
|
||||
avg_us(CounterId::kRtResolveUs),
|
||||
avg_count(CounterId::kRtResolveUs), avg_us(CounterId::kCpuD3D12BeginSubmissionUs),
|
||||
avg_us(CounterId::kCpuD3D12CheckFenceUs), avg_us(CounterId::kCpuD3D12FenceWaitUs),
|
||||
avg_us(CounterId::kCpuD3D12EndSubmissionUs), avg_count(CounterId::kCpuD3D12EndSubmissionUs),
|
||||
avg_us(CounterId::kCpuD3D12PipelineEndUs), avg_us(CounterId::kCpuD3D12SubmitBarriersUs),
|
||||
avg_us(CounterId::kCpuD3D12DeferredExecuteUs),
|
||||
avg_us(CounterId::kCpuD3D12CommandListCloseUs),
|
||||
avg_us(CounterId::kCpuD3D12ExecuteCommandListsUs), avg_us(CounterId::kCpuD3D12SignalUs),
|
||||
swap_us, guest_wait_us, pm4_us > accounted_us ? pm4_us - accounted_us : 0,
|
||||
avg_count(CounterId::kDrawSamplerFastPathDraws),
|
||||
avg_count(CounterId::kDrawTextureFastPathDraws));
|
||||
}
|
||||
cpu_profile::Reset();
|
||||
cpu_profile_frames_ = 0;
|
||||
cpu_profile_window_start_ticks_ = now_ticks;
|
||||
cpu_profile_window_start_time_ = now_time;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool CommandProcessor::ExecutePacketType3_INDIRECT_BUFFER(memory::RingBuffer* reader,
|
||||
uint32_t packet, uint32_t count) {
|
||||
// indirect buffer dispatch
|
||||
|
|
@ -1205,7 +1304,9 @@ bool CommandProcessor::ExecutePacketType3_WAIT_REG_MEM(memory::RingBuffer* reade
|
|||
break;
|
||||
}
|
||||
if (!matched) {
|
||||
// Wait.
|
||||
// Wait. Attributed separately so time spent blocked on guest-side
|
||||
// progress isn't read as command processing cost.
|
||||
PROFILE_SCOPE_COUNTER(kCpuGuestWaitUs);
|
||||
if (wait >= 0x100) {
|
||||
PrepareForWait();
|
||||
if (!REXCVAR_GET(vsync)) {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,11 @@ REXCVAR_DEFINE_BOOL(d3d12_submit_on_primary_buffer_end, true, "GPU/D3D12",
|
|||
"Submit command list when PM4 primary buffer ends")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
REXCVAR_DEFINE_BOOL(d3d12_gpu_timestamp_buckets, false, "GPU/D3D12",
|
||||
"Measure GPU time of draws and resolves with timestamp queries and report "
|
||||
"the breakdown in the FPS overlay and a periodic profile log")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
REXCVAR_DEFINE_BOOL(skate3_ultrawide_skip_shadow_targets, true, "Skate 3",
|
||||
"Do not apply Skate 3 Hor+ NDC correction while rendering likely shadow maps")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
|
@ -215,6 +220,18 @@ bool ShouldApplySkate3UltrawideHorPlus(
|
|||
return selected;
|
||||
}
|
||||
|
||||
// Frame-cached read of the cross-TU draw trace flag - the registry Query is a
|
||||
// mutex + string hash, too expensive to run for every draw.
|
||||
bool QuerySkate3UltrawideTraceDraws(uint64_t frame) {
|
||||
thread_local uint64_t cached_frame = UINT64_MAX;
|
||||
thread_local bool cached_value = false;
|
||||
if (cached_frame != frame) {
|
||||
cached_value = rex::cvar::Query<bool>("skate3_ultrawide_trace_draws");
|
||||
cached_frame = frame;
|
||||
}
|
||||
return cached_value;
|
||||
}
|
||||
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
class ScopedDrawStageTimer {
|
||||
public:
|
||||
|
|
@ -246,7 +263,9 @@ class ScopedDrawStageTimer {
|
|||
#define REX_D3D12_DRAW_STAGE_TIMER(counter_id) \
|
||||
ScopedDrawStageTimer REX_CONCAT(draw_stage_timer_, __LINE__)(rex::perf::CounterId::counter_id)
|
||||
#else
|
||||
#define REX_D3D12_DRAW_STAGE_TIMER(counter_id)
|
||||
// Without compiled-in perf counters, draw stage timers still feed the
|
||||
// runtime-gated rex::perf::cpu_profile attribution (gpu_cpu_profile cvar).
|
||||
#define REX_D3D12_DRAW_STAGE_TIMER(counter_id) PROFILE_SCOPE_COUNTER(counter_id)
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
|
@ -1929,9 +1948,7 @@ bool D3D12CommandProcessor::SetupContext() {
|
|||
pix_capture_requested_.store(false, std::memory_order_relaxed);
|
||||
pix_capturing_ = false;
|
||||
occlusion_query_resources_available_ = InitializeOcclusionQueryResources();
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
gpu_timestamp_resources_available_ = InitializeGpuTimestampResources();
|
||||
#endif
|
||||
|
||||
// Just not to expose uninitialized memory.
|
||||
std::memset(&system_constants_, 0, sizeof(system_constants_));
|
||||
|
|
@ -1942,9 +1959,7 @@ bool D3D12CommandProcessor::SetupContext() {
|
|||
void D3D12CommandProcessor::ShutdownContext() {
|
||||
AwaitAllQueueOperationsCompletion();
|
||||
InvalidateAllVertexBufferResidency();
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
ShutdownGpuTimestampResources();
|
||||
#endif
|
||||
ShutdownOcclusionQueryResources();
|
||||
|
||||
ui::d3d12::util::ReleaseAndNull(readback_buffer_);
|
||||
|
|
@ -2812,9 +2827,12 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3
|
|||
|
||||
// Update viewport, scissor, blend factor and stencil reference.
|
||||
UpdateFixedFunctionState(viewport_info, scissor, primitive_polygonal, normalized_depth_control);
|
||||
}
|
||||
|
||||
// Update system constants before uploading them.
|
||||
// TODO(Triang3l): With ROV, pass the disabled render target mask for safety.
|
||||
// Update system constants before uploading them.
|
||||
// TODO(Triang3l): With ROV, pass the disabled render target mask for safety.
|
||||
{
|
||||
REX_D3D12_DRAW_STAGE_TIMER(kDrawStageSystemConstantsUs);
|
||||
UpdateSystemConstantValues(memexport_used, primitive_polygonal,
|
||||
primitive_processing_result.host_vertex_shader_type,
|
||||
primitive_processing_result.line_loop_closing_index,
|
||||
|
|
@ -2983,7 +3001,9 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3
|
|||
const uint32_t skate3_ultrawide_draw_primitive_count = draw_util::EstimatePrimitiveCount(
|
||||
primitive_processing_result.host_primitive_type, host_draw_vertex_count);
|
||||
bool skate3_ultrawide_submit_draw = true;
|
||||
if (rex::cvar::Query<bool>("skate3_ultrawide_trace_draws")) {
|
||||
// Frame-cached: the registry Query takes the registry mutex and hashes the
|
||||
// name, far too expensive for a per-draw debug-flag check.
|
||||
if (QuerySkate3UltrawideTraceDraws(frame_current_)) {
|
||||
ultrawide_debug::DrawFingerprint fingerprint;
|
||||
fingerprint.bucket =
|
||||
memexport_used
|
||||
|
|
@ -3043,9 +3063,6 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3
|
|||
if (!skate3_ultrawide_submit_draw) {
|
||||
return true;
|
||||
}
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
const uint32_t host_draw_primitive_count = draw_util::EstimatePrimitiveCount(
|
||||
primitive_processing_result.host_primitive_type, host_draw_vertex_count);
|
||||
const rex::perf::DrawBucket draw_bucket =
|
||||
memexport_used
|
||||
? rex::perf::DrawBucket::kMemexport
|
||||
|
|
@ -3053,6 +3070,9 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3
|
|||
? rex::perf::DrawBucket::kDepthOnly
|
||||
: (pixel_shader == nullptr ? rex::perf::DrawBucket::kNoPixelShader
|
||||
: rex::perf::DrawBucket::kMainColorDepth));
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
const uint32_t host_draw_primitive_count = draw_util::EstimatePrimitiveCount(
|
||||
primitive_processing_result.host_primitive_type, host_draw_vertex_count);
|
||||
const bool collect_draw_fingerprints = rex::perf::ShouldCollectDrawFingerprints();
|
||||
rex::perf::DrawFingerprint draw_fingerprint;
|
||||
if (collect_draw_fingerprints) {
|
||||
|
|
@ -3105,12 +3125,10 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3
|
|||
if (collect_draw_fingerprints) {
|
||||
PROFILE_DRAW_FINGERPRINT(draw_fingerprint);
|
||||
}
|
||||
#endif
|
||||
uint32_t gpu_timestamp_query = BeginGpuTimestampedDraw(draw_bucket);
|
||||
#endif
|
||||
deferred_command_list_.D3DDrawInstanced(host_draw_vertex_count, 1, 0, 0);
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
EndGpuTimestampedDraw(gpu_timestamp_query);
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
D3D12_INDEX_BUFFER_VIEW index_buffer_view;
|
||||
|
|
@ -3187,12 +3205,10 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3
|
|||
if (collect_draw_fingerprints) {
|
||||
PROFILE_DRAW_FINGERPRINT(draw_fingerprint);
|
||||
}
|
||||
#endif
|
||||
uint32_t gpu_timestamp_query = BeginGpuTimestampedDraw(draw_bucket);
|
||||
#endif
|
||||
deferred_command_list_.D3DDrawIndexedInstanced(host_draw_vertex_count, 1, 0, 0, 0);
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
EndGpuTimestampedDraw(gpu_timestamp_query);
|
||||
#endif
|
||||
}
|
||||
if (scratch_index_buffer != nullptr) {
|
||||
ReleaseScratchGPUBuffer(scratch_index_buffer, D3D12_RESOURCE_STATE_INDEX_BUFFER);
|
||||
|
|
@ -3399,6 +3415,7 @@ bool D3D12CommandProcessor::IssueCopy() {
|
|||
#if XE_GPU_FINE_GRAINED_DRAW_SCOPES
|
||||
SCOPE_profile_cpu_f("gpu");
|
||||
#endif // XE_GPU_FINE_GRAINED_DRAW_SCOPES
|
||||
PROFILE_SCOPE_COUNTER(kRtResolveUs);
|
||||
if (!BeginSubmission(true)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -3407,8 +3424,14 @@ bool D3D12CommandProcessor::IssueCopy() {
|
|||
if (readback_mode == ReadbackResolveMode::kDisabled &&
|
||||
(!texture_cache_->IsDrawResolutionScaled() || gameplay_state_active)) {
|
||||
uint32_t written_address, written_length;
|
||||
return render_target_cache_->Resolve(*memory_, *shared_memory_, *texture_cache_,
|
||||
written_address, written_length);
|
||||
// Time the whole resolve (render target dump, copy and clear) for the GPU
|
||||
// profile breakdown.
|
||||
uint32_t gpu_timestamp_query =
|
||||
BeginGpuTimestampedDraw(rex::perf::DrawBucket::kCopyResolve);
|
||||
bool resolved = render_target_cache_->Resolve(*memory_, *shared_memory_, *texture_cache_,
|
||||
written_address, written_length);
|
||||
EndGpuTimestampedDraw(gpu_timestamp_query);
|
||||
return resolved;
|
||||
}
|
||||
return IssueCopy_ReadbackResolvePath();
|
||||
}
|
||||
|
|
@ -3662,7 +3685,16 @@ void D3D12CommandProcessor::CheckSubmissionFence(uint64_t await_submission) {
|
|||
fence_value, fence_completion_event_)))) {
|
||||
PROFILE_CMD_BUFFER_STALL();
|
||||
rex::perf::ScopedCounterTimer wait_timer(rex::perf::CounterId::kCpuD3D12FenceWaitUs);
|
||||
rex::perf::ScopedCounterTimer gpu_thread_wait_timer(
|
||||
rex::perf::CounterId::kGpuThreadFenceWaitUs, true);
|
||||
const std::chrono::steady_clock::time_point wait_start = std::chrono::steady_clock::now();
|
||||
WaitForSingleObject(fence_completion_event_, INFINITE);
|
||||
if (ui::Presenter* wait_presenter = graphics_system_->presenter()) {
|
||||
wait_presenter->AddGuestFrameWaitMicroseconds(uint64_t(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - wait_start)
|
||||
.count()));
|
||||
}
|
||||
queue_operations_done_since_submission_signal_ = false;
|
||||
} else {
|
||||
REXGPU_ERROR(
|
||||
|
|
@ -3682,7 +3714,18 @@ void D3D12CommandProcessor::CheckSubmissionFence(uint64_t await_submission) {
|
|||
submission_fence_->SetEventOnCompletion(await_submission, fence_completion_event_))) {
|
||||
PROFILE_CMD_BUFFER_STALL();
|
||||
rex::perf::ScopedCounterTimer wait_timer(rex::perf::CounterId::kCpuD3D12FenceWaitUs);
|
||||
rex::perf::ScopedCounterTimer gpu_thread_wait_timer(
|
||||
rex::perf::CounterId::kGpuThreadFenceWaitUs, true);
|
||||
// Also reported through the presenter so the FPS overlay can show the
|
||||
// blocked time in builds without perf counters.
|
||||
const std::chrono::steady_clock::time_point wait_start = std::chrono::steady_clock::now();
|
||||
WaitForSingleObject(fence_completion_event_, INFINITE);
|
||||
if (ui::Presenter* wait_presenter = graphics_system_->presenter()) {
|
||||
wait_presenter->AddGuestFrameWaitMicroseconds(uint64_t(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() -
|
||||
wait_start)
|
||||
.count()));
|
||||
}
|
||||
submission_completed_ = submission_fence_->GetCompletedValue();
|
||||
}
|
||||
}
|
||||
|
|
@ -3933,9 +3976,7 @@ bool D3D12CommandProcessor::BeginSubmission(bool is_guest_command) {
|
|||
|
||||
texture_cache_->BeginFrame();
|
||||
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
BeginGpuTimestampFrame();
|
||||
#endif
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
@ -3967,9 +4008,7 @@ bool D3D12CommandProcessor::EndSubmission(bool is_swap) {
|
|||
if (is_closing_frame) {
|
||||
{
|
||||
PROFILE_SCOPE_COUNTER(kCpuD3D12EndFrameUs);
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
EndGpuTimestampFrame();
|
||||
#endif
|
||||
|
||||
texture_cache_->EndFrame();
|
||||
|
||||
|
|
@ -4011,9 +4050,7 @@ bool D3D12CommandProcessor::EndSubmission(bool is_swap) {
|
|||
deferred_command_list_.Execute(command_list_, command_list_1_);
|
||||
}
|
||||
if (is_closing_frame) {
|
||||
#ifdef REXGLUE_ENABLE_PERF_COUNTERS
|
||||
ResolveGpuTimestampFrame(command_list_);
|
||||
#endif
|
||||
}
|
||||
{
|
||||
PROFILE_SCOPE_COUNTER(kCpuD3D12CommandListCloseUs);
|
||||
|
|
@ -5511,12 +5548,15 @@ void D3D12CommandProcessor::ShutdownGpuTimestampResources() {
|
|||
gpu_timestamp_query_heap_.Reset();
|
||||
gpu_timestamp_frequency_ = 0;
|
||||
gpu_timestamp_resources_available_ = false;
|
||||
gpu_timestamp_draws_enabled_ = false;
|
||||
for (auto& frame : gpu_timestamp_frames_) {
|
||||
frame = {};
|
||||
}
|
||||
}
|
||||
|
||||
void D3D12CommandProcessor::BeginGpuTimestampFrame() {
|
||||
gpu_timestamp_draws_enabled_ =
|
||||
rex::perf::IsCaptureRecording() || REXCVAR_GET(d3d12_gpu_timestamp_buckets);
|
||||
if (!gpu_timestamp_resources_available_) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -5532,7 +5572,7 @@ void D3D12CommandProcessor::BeginGpuTimestampFrame() {
|
|||
frame.bucket_start_queries.clear();
|
||||
frame.counter_ids.clear();
|
||||
frame.counter_start_queries.clear();
|
||||
if (rex::perf::IsCaptureRecording() && gpu_timestamp_query_heap_) {
|
||||
if (gpu_timestamp_draws_enabled_ && gpu_timestamp_query_heap_) {
|
||||
frame.frame_start_query = frame.query_count++;
|
||||
deferred_command_list_.D3DEndQuery(gpu_timestamp_query_heap_.Get(), D3D12_QUERY_TYPE_TIMESTAMP,
|
||||
frame.query_base + frame.frame_start_query);
|
||||
|
|
@ -5582,17 +5622,24 @@ void D3D12CommandProcessor::ProcessGpuTimestampResults() {
|
|||
continue;
|
||||
}
|
||||
const uint64_t* timestamps = gpu_timestamp_readback_mapping_ + frame.query_base;
|
||||
int64_t frame_span_us = 0;
|
||||
if (frame.frame_start_query != UINT32_MAX && frame.frame_end_query != UINT32_MAX) {
|
||||
uint64_t start = timestamps[frame.frame_start_query];
|
||||
uint64_t end = timestamps[frame.frame_end_query];
|
||||
if (end > start) {
|
||||
int64_t elapsed_us =
|
||||
static_cast<int64_t>((end - start) * uint64_t(1000000) / gpu_timestamp_frequency_);
|
||||
frame_span_us = elapsed_us;
|
||||
rex::perf::IncrementCounter(rex::perf::CounterId::kGpuCommandProcessorFrameUs,
|
||||
elapsed_us);
|
||||
rex::perf::IncrementCounter(rex::perf::CounterId::kGpuTimestampedFrames);
|
||||
if (ui::Presenter* presenter = graphics_system_->presenter()) {
|
||||
presenter->SetGuestFrameGpuSpanMicroseconds(uint64_t(elapsed_us));
|
||||
}
|
||||
}
|
||||
}
|
||||
uint64_t bucket_draw_us = 0;
|
||||
uint64_t bucket_resolve_us = 0;
|
||||
for (uint32_t i = 0; i < frame.buckets.size(); ++i) {
|
||||
if (i >= frame.bucket_start_queries.size()) {
|
||||
break;
|
||||
|
|
@ -5608,28 +5655,70 @@ void D3D12CommandProcessor::ProcessGpuTimestampResults() {
|
|||
}
|
||||
int64_t elapsed_us =
|
||||
static_cast<int64_t>((end - start) * uint64_t(1000000) / gpu_timestamp_frequency_);
|
||||
if (size_t(frame.buckets[i]) < size_t(rex::perf::DrawBucket::kCount)) {
|
||||
gpu_profile_bucket_us_[size_t(frame.buckets[i])] += uint64_t(elapsed_us);
|
||||
}
|
||||
rex::perf::CounterId counter_id;
|
||||
switch (frame.buckets[i]) {
|
||||
case rex::perf::DrawBucket::kMainColorDepth:
|
||||
counter_id = rex::perf::CounterId::kGpuMainUs;
|
||||
bucket_draw_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
case rex::perf::DrawBucket::kDepthOnly:
|
||||
counter_id = rex::perf::CounterId::kGpuDepthUs;
|
||||
bucket_draw_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
case rex::perf::DrawBucket::kCopyResolve:
|
||||
counter_id = rex::perf::CounterId::kGpuCopyUs;
|
||||
bucket_resolve_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
case rex::perf::DrawBucket::kMemexport:
|
||||
counter_id = rex::perf::CounterId::kGpuMemexportUs;
|
||||
bucket_draw_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
case rex::perf::DrawBucket::kNoPixelShader:
|
||||
counter_id = rex::perf::CounterId::kGpuNoPixelShaderUs;
|
||||
bucket_draw_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
rex::perf::IncrementCounter(counter_id, elapsed_us);
|
||||
}
|
||||
if (ui::Presenter* presenter = graphics_system_->presenter()) {
|
||||
// Zeros when bucket timestamps are disabled, which hides the breakdown
|
||||
// in the FPS overlay. Unlike on Vulkan, the draw buckets are individual
|
||||
// begin/end pairs around the draw commands, so barriers and other
|
||||
// in-between work are not included anywhere ("other" covers them).
|
||||
presenter->SetGuestFrameGpuBreakdownMicroseconds(bucket_draw_us, bucket_resolve_us, 0);
|
||||
}
|
||||
// Periodic full attribution log, mirroring the Vulkan backend's.
|
||||
if (frame_span_us > 0 && REXCVAR_GET(d3d12_gpu_timestamp_buckets)) {
|
||||
gpu_profile_span_us_ += uint64_t(frame_span_us);
|
||||
if (++gpu_profile_frames_ >= kGpuProfileLogFrameInterval) {
|
||||
uint64_t buckets_sum_us = 0;
|
||||
std::string breakdown;
|
||||
for (size_t bucket = 0; bucket < size_t(rex::perf::DrawBucket::kCount); ++bucket) {
|
||||
uint64_t total_us = gpu_profile_bucket_us_[bucket];
|
||||
buckets_sum_us += total_us;
|
||||
if (!total_us) {
|
||||
continue;
|
||||
}
|
||||
breakdown +=
|
||||
fmt::format(" {}={}", rex::perf::DrawBucketName(rex::perf::DrawBucket(bucket)),
|
||||
total_us / gpu_profile_frames_);
|
||||
}
|
||||
uint64_t span_avg_us = gpu_profile_span_us_ / gpu_profile_frames_;
|
||||
uint64_t sum_avg_us = buckets_sum_us / gpu_profile_frames_;
|
||||
REXGPU_INFO(
|
||||
"D3D12 GPU profile (avg us/frame over {} frames): span={} buckets={} untimed={} |{}",
|
||||
gpu_profile_frames_, span_avg_us, sum_avg_us,
|
||||
span_avg_us > sum_avg_us ? span_avg_us - sum_avg_us : 0, breakdown);
|
||||
std::memset(gpu_profile_bucket_us_, 0, sizeof(gpu_profile_bucket_us_));
|
||||
gpu_profile_span_us_ = 0;
|
||||
gpu_profile_frames_ = 0;
|
||||
}
|
||||
}
|
||||
for (uint32_t i = 0; i < frame.counter_ids.size(); ++i) {
|
||||
if (i >= frame.counter_start_queries.size()) {
|
||||
break;
|
||||
|
|
@ -5657,7 +5746,7 @@ void D3D12CommandProcessor::ProcessGpuTimestampResults() {
|
|||
|
||||
uint32_t D3D12CommandProcessor::BeginGpuTimestampedDraw(rex::perf::DrawBucket bucket) {
|
||||
if (!gpu_timestamp_resources_available_ || !gpu_timestamp_query_heap_ ||
|
||||
!rex::perf::IsCaptureRecording() || !frame_open_) {
|
||||
!gpu_timestamp_draws_enabled_ || !frame_open_) {
|
||||
return UINT32_MAX;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3968,6 +3968,24 @@ void D3D12RenderTargetCache::PerformTransfersAndResolveClears(
|
|||
DeferredCommandList& command_list = command_processor_.GetDeferredCommandList();
|
||||
|
||||
bool resolve_clear_needed = render_target_resolve_clear_values && resolve_clear_rectangle;
|
||||
|
||||
// Time ownership transfers and resolve clears for the GPU profile breakdown.
|
||||
// This is called with nothing to do for most draws - only time when there's
|
||||
// actual work.
|
||||
bool transfer_work_needed = resolve_clear_needed;
|
||||
if (!transfer_work_needed) {
|
||||
for (uint32_t i = 0; i < render_target_count; ++i) {
|
||||
if (render_targets[i] && !render_target_transfers[i].empty()) {
|
||||
transfer_work_needed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t gpu_timestamp_query =
|
||||
transfer_work_needed
|
||||
? command_processor_.BeginGpuTimestampedDraw(rex::perf::DrawBucket::kRtTransfer)
|
||||
: UINT32_MAX;
|
||||
|
||||
D3D12_RECT clear_rect;
|
||||
if (resolve_clear_needed) {
|
||||
// Assuming the rectangle is already clamped by the setup function from the
|
||||
|
|
@ -4223,6 +4241,7 @@ void D3D12RenderTargetCache::PerformTransfersAndResolveClears(
|
|||
current_temporary_descriptors_gpu_.resize(descriptor_count);
|
||||
if (!command_processor_.RequestOneUseSingleViewDescriptors(
|
||||
descriptor_count, current_temporary_descriptors_gpu_.data())) {
|
||||
command_processor_.EndGpuTimestampedDraw(gpu_timestamp_query);
|
||||
return;
|
||||
}
|
||||
for (uint32_t i = 0; i < descriptor_count; ++i) {
|
||||
|
|
@ -4849,6 +4868,8 @@ void D3D12RenderTargetCache::PerformTransfersAndResolveClears(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
command_processor_.EndGpuTimestampedDraw(gpu_timestamp_query);
|
||||
}
|
||||
|
||||
void D3D12RenderTargetCache::SetCommandListRenderTargets(
|
||||
|
|
|
|||
|
|
@ -1721,6 +1721,9 @@ bool D3D12TextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture,
|
|||
return false;
|
||||
}
|
||||
uint32_t descriptor_write_index = 0;
|
||||
// Time the load dispatches for the GPU profile breakdown.
|
||||
uint32_t gpu_timestamp_query =
|
||||
command_processor_.BeginGpuTimestampedDraw(rex::perf::DrawBucket::kTextureUpload);
|
||||
command_processor_.SetExternalPipeline(pipeline);
|
||||
command_list.D3DSetComputeRootSignature(load_root_signature_.Get());
|
||||
// Set up the destination descriptor.
|
||||
|
|
@ -1782,6 +1785,7 @@ bool D3D12TextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture,
|
|||
is_base ? d3d12_texture.GetGuestBaseSize() : d3d12_texture.GetGuestMipsSize();
|
||||
if (!MakeScaledResolveRangeCurrent(guest_address, guest_size_unscaled,
|
||||
load_shader_info.source_bpe_log2)) {
|
||||
command_processor_.EndGpuTimestampedDraw(gpu_timestamp_query);
|
||||
command_processor_.ReleaseScratchGPUBuffer(copy_buffer, copy_buffer_state);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1933,6 +1937,8 @@ bool D3D12TextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture,
|
|||
}
|
||||
}
|
||||
|
||||
command_processor_.EndGpuTimestampedDraw(gpu_timestamp_query);
|
||||
|
||||
command_processor_.ReleaseScratchGPUBuffer(copy_buffer, copy_buffer_state);
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1423,4 +1423,45 @@ void RenderTargetCache::ChangeOwnership(RenderTargetKey dest, uint32_t start_til
|
|||
}
|
||||
}
|
||||
|
||||
bool RenderTargetCache::IsOwnershipConfinedToRange(RenderTargetKey key,
|
||||
uint32_t start_tiles_base_relative,
|
||||
uint32_t length_tiles) const {
|
||||
if (key.IsEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// Convert to up to two absolute extents the same way as ChangeOwnership.
|
||||
uint32_t start_tiles = (key.base_tiles + start_tiles_base_relative) & (xenos::kEdramTileCount - 1);
|
||||
uint32_t end_tiles = start_tiles + length_tiles;
|
||||
uint32_t extent_starts[2] = {start_tiles, 0};
|
||||
uint32_t extent_ends[2] = {std::min(end_tiles, xenos::kEdramTileCount), 0};
|
||||
uint32_t extent_count = 1;
|
||||
if (end_tiles > xenos::kEdramTileCount) {
|
||||
extent_ends[extent_count++] = std::min(end_tiles & (xenos::kEdramTileCount - 1), start_tiles);
|
||||
}
|
||||
bool host_depth_encoding_different = key.is_depth && GetPath() == Path::kHostRenderTargets &&
|
||||
IsHostDepthEncodingDifferent(key.GetDepthFormat());
|
||||
for (const auto& range_pair : ownership_ranges_) {
|
||||
uint32_t range_start = range_pair.first;
|
||||
uint32_t range_end = range_pair.second.end_tiles;
|
||||
bool references_key = range_pair.second.render_target == key;
|
||||
if (!references_key && host_depth_encoding_different) {
|
||||
references_key = range_pair.second.GetHostDepthRenderTarget(key.GetDepthFormat()) == key;
|
||||
}
|
||||
if (!references_key) {
|
||||
continue;
|
||||
}
|
||||
bool contained = false;
|
||||
for (uint32_t i = 0; i < extent_count; ++i) {
|
||||
if (range_start >= extent_starts[i] && range_end <= extent_ends[i]) {
|
||||
contained = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!contained) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace rex::graphics
|
||||
|
|
|
|||
|
|
@ -725,6 +725,7 @@ void TextureCache::RequestTextures(uint32_t used_texture_mask) {
|
|||
}
|
||||
PERF_counter_add(kTextureBindingsChanged, changed_binding_count);
|
||||
#endif
|
||||
++texture_bindings_generation_;
|
||||
UpdateTextureBindingsImpl(bindings_changed);
|
||||
}
|
||||
DebugLogScaledTextureBindings(used_texture_mask);
|
||||
|
|
@ -837,6 +838,16 @@ bool TextureCache::DebugIsTeamProfileBackgroundTexture(const Texture& texture) c
|
|||
if (!DebugIsTeamProfileBackgroundCandidateKey(key) || key.scaled_resolve) {
|
||||
return false;
|
||||
}
|
||||
// Identifying the texture requires hashing up to 32 KB of guest memory, and
|
||||
// candidate keys (128x256 DXT4_5) can be bound across many draws per frame.
|
||||
// Never pay that cost unless one of the team-profile-background debug logs
|
||||
// is armed. The draws cvar is defined in the Vulkan command processor, so it
|
||||
// is read through the registry rather than REXCVAR_GET.
|
||||
if (REXCVAR_GET(vulkan_debug_log_team_profile_background_candidates_remaining) <= 0 &&
|
||||
REXCVAR_GET(vulkan_debug_log_team_profile_background_bindings_remaining) <= 0 &&
|
||||
rex::cvar::Query<int32_t>("vulkan_debug_log_team_profile_background_draws_remaining") <= 0) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t* data = shared_memory().DebugTranslatePhysical(key.base_page << 12);
|
||||
const uint32_t sample_length = std::min<uint32_t>(texture.GetGuestBaseSize(), UINT32_C(0x8000));
|
||||
if (data == nullptr || sample_length == 0) {
|
||||
|
|
@ -1671,8 +1682,11 @@ void TextureCache::ResetTextureBindings(bool from_destructor) {
|
|||
bindings_reset |= UINT32_C(1) << i;
|
||||
}
|
||||
texture_bindings_in_sync_ &= ~bindings_reset;
|
||||
if (!from_destructor && bindings_reset) {
|
||||
UpdateTextureBindingsImpl(bindings_reset);
|
||||
if (bindings_reset) {
|
||||
++texture_bindings_generation_;
|
||||
if (!from_destructor) {
|
||||
UpdateTextureBindingsImpl(bindings_reset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
|
@ -46,6 +47,7 @@
|
|||
#include <rex/graphics/vulkan/shader.h>
|
||||
#include <rex/graphics/vulkan/shared_memory.h>
|
||||
#include <rex/graphics/xenos.h>
|
||||
#include <rex/hash.h>
|
||||
#include <rex/kernel/xboxkrnl/video.h>
|
||||
#include <rex/system/kernel_state.h>
|
||||
#include <rex/types.h>
|
||||
|
|
@ -120,6 +122,12 @@ REXCVAR_DEFINE_BOOL(vulkan_submit_on_primary_buffer_end, true, "GPU/Vulkan",
|
|||
"Submit command buffer when PM4 primary buffer ends")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
REXCVAR_DEFINE_INT32(vulkan_gpu_timestamp_trace_frames, 0, "GPU/Vulkan",
|
||||
"Log every GPU timestamp bucket interval (start/end/duration) for this many "
|
||||
"frames, for auditing the time attribution")
|
||||
.range(0, 100)
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
REXCVAR_DEFINE_BOOL(vulkan_dynamic_rendering, true, "GPU/Vulkan",
|
||||
"Use VK_KHR_dynamic_rendering for Vulkan GPU emulation when supported by the "
|
||||
"device (falls back to render passes otherwise)")
|
||||
|
|
@ -129,6 +137,28 @@ REXCVAR_DEFINE_BOOL(vulkan_skip_inert_no_pixel_draws, false, "GPU/Vulkan",
|
|||
"occlusion side effects")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
REXCVAR_DEFINE_BOOL(vulkan_reuse_transient_texture_descriptors, true, "GPU/Vulkan",
|
||||
"Reuse texture/sampler descriptor sets within a frame when the bindings are "
|
||||
"unchanged instead of allocating and writing new ones for every draw")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
REXCVAR_DEFINE_BOOL(vulkan_push_constants_descriptors, true, "GPU/Vulkan",
|
||||
"Use VK_KHR_push_descriptor for the guest constant buffer descriptor set "
|
||||
"instead of allocating and writing a transient descriptor set whenever "
|
||||
"constants change (requires driver support; applied at startup)");
|
||||
|
||||
REXCVAR_DEFINE_BOOL(vulkan_reuse_unchanged_draw_bindings, true, "GPU/Vulkan",
|
||||
"Skip the per-draw sampler cache walks and texture descriptor "
|
||||
"gathering/hashing entirely when the previous draw in the same submission "
|
||||
"used the same shaders with identical sampler parameters and texture "
|
||||
"binding views")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
REXCVAR_DEFINE_BOOL(vulkan_gpu_timestamp_buckets, false, "GPU/Vulkan",
|
||||
"Measure per-bucket GPU times (guest draws, resolves, render target dumps) "
|
||||
"with timestamp queries and report them in the FPS overlay")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
// Skate 3 ultrawide cvars. These mirror the Direct3D 12 backend's definitions
|
||||
// so the Hor+ classifier behaves identically on Vulkan; the D3D12 translation
|
||||
// unit is not compiled on this platform, so without these the queries below
|
||||
|
|
@ -273,6 +303,18 @@ Skate3UltrawideDrawState SnapshotSkate3UltrawideDrawState(uint64_t frame) {
|
|||
return cached_state;
|
||||
}
|
||||
|
||||
// Frame-cached read of the cross-TU draw trace flag - the registry Query is a
|
||||
// mutex + string hash, too expensive to run for every draw.
|
||||
bool QuerySkate3UltrawideTraceDraws(uint64_t frame) {
|
||||
thread_local uint64_t cached_frame = UINT64_MAX;
|
||||
thread_local bool cached_value = false;
|
||||
if (cached_frame != frame) {
|
||||
cached_value = rex::cvar::Query<bool>("skate3_ultrawide_trace_draws");
|
||||
cached_frame = frame;
|
||||
}
|
||||
return cached_value;
|
||||
}
|
||||
|
||||
bool IsSkate3UltrawideDisplayViewport(const draw_util::ViewportInfo& viewport_info) {
|
||||
const uint32_t width = viewport_info.xy_extent[0];
|
||||
const uint32_t height = viewport_info.xy_extent[1];
|
||||
|
|
@ -1162,6 +1204,17 @@ bool VulkanCommandProcessor::SetupContext() {
|
|||
return false;
|
||||
}
|
||||
// Guest draw constants.
|
||||
// With VK_KHR_push_descriptor, the constant buffer descriptors are pushed
|
||||
// directly into the command buffer instead of allocating and writing a
|
||||
// transient descriptor set whenever any constant buffer changes (which is
|
||||
// nearly every draw due to guest float constant updates).
|
||||
constants_push_descriptors_used_ = vulkan_device->extensions().ext_KHR_push_descriptor &&
|
||||
REXCVAR_GET(vulkan_push_constants_descriptors);
|
||||
if (constants_push_descriptors_used_) {
|
||||
REXGPU_INFO(
|
||||
"VulkanCommandProcessor: using push descriptors for guest constant "
|
||||
"buffers");
|
||||
}
|
||||
VkDescriptorSetLayoutBinding
|
||||
descriptor_set_layout_bindings_constants[SpirvShaderTranslator::kConstantBufferCount] = {};
|
||||
for (uint32_t i = 0; i < SpirvShaderTranslator::kConstantBufferCount; ++i) {
|
||||
|
|
@ -1187,6 +1240,10 @@ bool VulkanCommandProcessor::SetupContext() {
|
|||
descriptor_set_layout_create_info.bindingCount =
|
||||
uint32_t(rex::countof(descriptor_set_layout_bindings_constants));
|
||||
descriptor_set_layout_create_info.pBindings = descriptor_set_layout_bindings_constants;
|
||||
if (constants_push_descriptors_used_) {
|
||||
descriptor_set_layout_create_info.flags |=
|
||||
VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR;
|
||||
}
|
||||
if (dfn.vkCreateDescriptorSetLayout(device, &descriptor_set_layout_create_info, nullptr,
|
||||
&descriptor_set_layout_constants_) != VK_SUCCESS) {
|
||||
REXGPU_ERROR(
|
||||
|
|
@ -1194,6 +1251,8 @@ bool VulkanCommandProcessor::SetupContext() {
|
|||
"constant buffers");
|
||||
return false;
|
||||
}
|
||||
descriptor_set_layout_create_info.flags &=
|
||||
~VkDescriptorSetLayoutCreateFlags(VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR);
|
||||
// Transient: storage buffer for compute shaders.
|
||||
VkDescriptorSetLayoutBinding descriptor_set_layout_binding_transient;
|
||||
descriptor_set_layout_binding_transient.binding = 0;
|
||||
|
|
@ -3492,11 +3551,26 @@ bool VulkanCommandProcessor::SubmitBarriers(bool force_end_render_pass) {
|
|||
|
||||
void VulkanCommandProcessor::SubmitBarriersAndEnterRenderTargetCacheRenderPass(
|
||||
VkRenderPass render_pass, const VulkanRenderTargetCache::Framebuffer* framebuffer) {
|
||||
SubmitBarriers(false);
|
||||
const ui::vulkan::VulkanDevice* vulkan_device = GetVulkanDevice();
|
||||
bool use_dynamic_rendering =
|
||||
REXCVAR_GET(vulkan_dynamic_rendering) && vulkan_device->properties().dynamicRendering;
|
||||
|
||||
// Time genuine render pass entries (pending barriers including image layout
|
||||
// transitions, pass end/begin and deferred resolve clear replay) separately
|
||||
// - reported in the FPS overlay's "other" line. Don't open a region on the
|
||||
// no-op path (same pass, no barriers), it runs for every draw.
|
||||
bool pass_entry_work =
|
||||
!pending_barriers_.empty() || current_pending_barrier_.src_stage_mask ||
|
||||
current_pending_barrier_.dst_stage_mask ||
|
||||
!(in_render_pass_ && current_framebuffer_ == framebuffer &&
|
||||
(use_dynamic_rendering ? current_render_pass_ == VK_NULL_HANDLE
|
||||
: current_render_pass_ == render_pass));
|
||||
if (pass_entry_work) {
|
||||
BeginGpuTimestampedRegion(rex::perf::DrawBucket::kRenderPassEntry);
|
||||
}
|
||||
|
||||
SubmitBarriers(false);
|
||||
|
||||
if (use_dynamic_rendering) {
|
||||
if (in_render_pass_ && current_framebuffer_ == framebuffer &&
|
||||
current_render_pass_ == VK_NULL_HANDLE) {
|
||||
|
|
@ -3561,6 +3635,11 @@ void VulkanCommandProcessor::SubmitBarriersAndEnterRenderTargetCacheRenderPass(
|
|||
VK_SUBPASS_CONTENTS_INLINE);
|
||||
}
|
||||
in_render_pass_ = true;
|
||||
|
||||
// Replay resolve clears deferred to the next render pass binding their
|
||||
// render targets - they ride the render pass entered for guest draws anyway
|
||||
// instead of requiring their own render passes during resolves.
|
||||
render_target_cache_->FlushDeferredResolveClearsIntoEnteredRenderPass(framebuffer->host_extent);
|
||||
}
|
||||
|
||||
void VulkanCommandProcessor::SubmitBarriersAndEnterRenderTargetCacheRenderPass(
|
||||
|
|
@ -4050,6 +4129,8 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
|
|||
|
||||
const ui::vulkan::VulkanDevice::Properties& device_properties = GetVulkanDevice()->properties();
|
||||
|
||||
rex::perf::ScopedCounterTimer analyze_stage_timer(rex::perf::CounterId::kDrawStageAnalyzeUs);
|
||||
|
||||
memexport_ranges_.clear();
|
||||
|
||||
// Vertex shader analysis.
|
||||
|
|
@ -4131,6 +4212,8 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
|
|||
VulkanShader::VulkanTranslation* vertex_shader_translation;
|
||||
VulkanShader::VulkanTranslation* pixel_shader_translation;
|
||||
bool memexport_writes_possible = memexport_used_vertex || memexport_used_pixel;
|
||||
bool draw_samplers_reused = false;
|
||||
analyze_stage_timer.Stop();
|
||||
|
||||
// Two iterations because a submission (even the current one - in which case
|
||||
// it needs to be ended, and a new one must be started) may need to be awaited
|
||||
|
|
@ -4168,6 +4251,8 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
|
|||
// Tessellation and rectangle expansion variants for rasterization are
|
||||
// produced by the primitive processor and are handled by the Vulkan
|
||||
// pipeline cache.
|
||||
rex::perf::ScopedCounterTimer translate_stage_timer(
|
||||
rex::perf::CounterId::kDrawStageTranslateUs);
|
||||
Shader::HostVertexShaderType host_vertex_shader_type =
|
||||
primitive_processing_result.host_vertex_shader_type;
|
||||
if (host_vertex_shader_type != Shader::HostVertexShaderType::kVertex &&
|
||||
|
|
@ -4199,16 +4284,53 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
|
|||
pixel_shader_translation)) {
|
||||
return draw_fail("shader_translation");
|
||||
}
|
||||
translate_stage_timer.Stop();
|
||||
rex::perf::ScopedCounterTimer samplers_stage_timer(
|
||||
rex::perf::CounterId::kDrawStageSamplersUs);
|
||||
|
||||
// Obtain the samplers. Note that the bindings don't depend on the shader
|
||||
// modification, so if on the second iteration of this loop it becomes
|
||||
// different for some reason (like a race condition with the guest in index
|
||||
// buffer processing in the primitive processor resulting in different host
|
||||
// vertex shader types), the bindings will stay the same.
|
||||
// TODO(Triang3l): Sampler caching and reuse for adjacent draws within one
|
||||
// submission.
|
||||
// Sampler reuse for adjacent draws within one submission (the upstream
|
||||
// "TODO(Triang3l): Sampler caching and reuse" here): if the previous draw
|
||||
// in this same submission used the same shaders and the sampler
|
||||
// parameters derived from the fetch constants are unchanged,
|
||||
// current_samplers_* already hold valid VkSamplers whose last-usage
|
||||
// submission indices are current - skip the per-sampler cache lookups.
|
||||
draw_samplers_reused = false;
|
||||
if (!i && REXCVAR_GET(vulkan_reuse_unchanged_draw_bindings) && draw_binding_state_valid_ &&
|
||||
draw_binding_submission_ == GetCurrentSubmission() &&
|
||||
draw_binding_vertex_shader_ == vertex_shader &&
|
||||
draw_binding_pixel_shader_ == pixel_shader) {
|
||||
draw_samplers_reused = true;
|
||||
for (uint32_t j = 0; draw_samplers_reused && j < 2; ++j) {
|
||||
const std::vector<std::pair<VulkanTextureCache::SamplerParameters, VkSampler>>&
|
||||
shader_samplers = j ? current_samplers_pixel_ : current_samplers_vertex_;
|
||||
const VulkanShader* shader = j ? pixel_shader : vertex_shader;
|
||||
if (!shader) {
|
||||
draw_samplers_reused = shader_samplers.empty();
|
||||
continue;
|
||||
}
|
||||
const std::vector<VulkanShader::SamplerBinding>& shader_sampler_bindings =
|
||||
shader->GetSamplerBindingsAfterTranslation();
|
||||
if (shader_samplers.size() != shader_sampler_bindings.size()) {
|
||||
draw_samplers_reused = false;
|
||||
break;
|
||||
}
|
||||
for (size_t k = 0; k < shader_sampler_bindings.size(); ++k) {
|
||||
if (shader_samplers[k].second == VK_NULL_HANDLE ||
|
||||
shader_samplers[k].first !=
|
||||
texture_cache_->GetSamplerParameters(shader_sampler_bindings[k])) {
|
||||
draw_samplers_reused = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t samplers_overflowed_count = 0;
|
||||
for (uint32_t j = 0; j < 2; ++j) {
|
||||
for (uint32_t j = 0; !draw_samplers_reused && j < 2; ++j) {
|
||||
std::vector<std::pair<VulkanTextureCache::SamplerParameters, VkSampler>>& shader_samplers =
|
||||
j ? current_samplers_pixel_ : current_samplers_vertex_;
|
||||
if (!i) {
|
||||
|
|
@ -4251,6 +4373,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
|
|||
}
|
||||
}
|
||||
}
|
||||
samplers_stage_timer.Stop();
|
||||
if (!samplers_overflowed_count) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -4267,6 +4390,12 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
|
|||
CheckSubmissionFenceAndDeviceLoss(sampler_overflow_await_submission);
|
||||
}
|
||||
|
||||
// Record the state the binding fast path of the next draw compares against.
|
||||
draw_binding_state_valid_ = true;
|
||||
draw_binding_submission_ = GetCurrentSubmission();
|
||||
draw_binding_vertex_shader_ = vertex_shader;
|
||||
draw_binding_pixel_shader_ = pixel_shader;
|
||||
|
||||
uint32_t normalized_color_mask =
|
||||
pixel_shader ? draw_util::GetNormalizedColorMask(regs, pixel_shader->writes_color_targets())
|
||||
: 0;
|
||||
|
|
@ -4310,9 +4439,29 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
|
|||
rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageTextureUs);
|
||||
texture_cache_->RequestTextures(used_texture_mask);
|
||||
}
|
||||
const bool debug_team_profile_bg_draw = texture_cache_->DebugFindTeamProfileBackgroundBinding(
|
||||
used_texture_mask, debug_team_profile_bg_fetch_index, debug_team_profile_bg_signed_view,
|
||||
&debug_team_profile_bg_texture_base, &debug_team_profile_bg_texture_length);
|
||||
// With the samplers reused and the texture cache's binding views unchanged,
|
||||
// UpdateBindings can keep the currently recorded texture descriptor sets
|
||||
// without regathering and rehashing the image infos.
|
||||
{
|
||||
uint64_t texture_bindings_generation = texture_cache_->texture_bindings_generation();
|
||||
texture_bindings_unchanged_this_draw_ =
|
||||
draw_samplers_reused &&
|
||||
draw_binding_texture_generation_ == texture_bindings_generation;
|
||||
draw_binding_texture_generation_ = texture_bindings_generation;
|
||||
if (draw_samplers_reused) {
|
||||
rex::perf::cpu_profile::CountEvent(rex::perf::CounterId::kDrawSamplerFastPathDraws);
|
||||
}
|
||||
if (texture_bindings_unchanged_this_draw_) {
|
||||
rex::perf::cpu_profile::CountEvent(rex::perf::CounterId::kDrawTextureFastPathDraws);
|
||||
}
|
||||
}
|
||||
// The binding scan hashes guest texture memory to identify the texture, so
|
||||
// only perform it while draw logging is actually armed.
|
||||
const bool debug_team_profile_bg_draw =
|
||||
REXCVAR_GET(vulkan_debug_log_team_profile_background_draws_remaining) > 0 &&
|
||||
texture_cache_->DebugFindTeamProfileBackgroundBinding(
|
||||
used_texture_mask, debug_team_profile_bg_fetch_index, debug_team_profile_bg_signed_view,
|
||||
&debug_team_profile_bg_texture_base, &debug_team_profile_bg_texture_length);
|
||||
|
||||
const VulkanPipelineCache::PipelineLayoutProvider* pipeline_layout_provider;
|
||||
// Set up the render targets - this may perform dispatches and draws.
|
||||
|
|
@ -4381,6 +4530,14 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
|
|||
descriptor_sets_kept = std::min(
|
||||
descriptor_sets_kept, uint32_t(SpirvShaderTranslator::kDescriptorSetTexturesPixel));
|
||||
}
|
||||
// Sets at the first incompatible index and above are disturbed by
|
||||
// binding a pipeline with the new layout and must be rebound. This was
|
||||
// previously masked by the texture descriptor sets being rewritten and
|
||||
// rebound on every draw.
|
||||
if (descriptor_sets_kept < uint32_t(SpirvShaderTranslator::kDescriptorSetCount)) {
|
||||
current_graphics_descriptor_sets_bound_up_to_date_ &=
|
||||
(UINT32_C(1) << descriptor_sets_kept) - 1;
|
||||
}
|
||||
} else {
|
||||
// No or unknown pipeline layout previously bound - all bindings are in an
|
||||
// indeterminate state.
|
||||
|
|
@ -4486,11 +4643,14 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
|
|||
}
|
||||
|
||||
// Update system constants before uploading them.
|
||||
UpdateSystemConstantValues(primitive_polygonal, primitive_processing_result,
|
||||
shader_32bit_index_dma, 0, viewport_info, used_texture_mask,
|
||||
vertex_shader->ucode_data_hash(),
|
||||
pixel_shader ? pixel_shader->ucode_data_hash() : 0,
|
||||
normalized_depth_control, normalized_color_mask);
|
||||
{
|
||||
rex::perf::ScopedCounterTimer stage_timer(rex::perf::CounterId::kDrawStageSystemConstantsUs);
|
||||
UpdateSystemConstantValues(primitive_polygonal, primitive_processing_result,
|
||||
shader_32bit_index_dma, 0, viewport_info, used_texture_mask,
|
||||
vertex_shader->ucode_data_hash(),
|
||||
pixel_shader ? pixel_shader->ucode_data_hash() : 0,
|
||||
normalized_depth_control, normalized_color_mask);
|
||||
}
|
||||
|
||||
// Update uniform buffers and descriptor sets after binding the pipeline with
|
||||
// the new layout.
|
||||
|
|
@ -4646,6 +4806,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
|
|||
render_target_cache_->last_update_framebuffer());
|
||||
}
|
||||
|
||||
rex::perf::ScopedCounterTimer pre_draw_stage_timer(rex::perf::CounterId::kDrawStagePreDrawUs);
|
||||
const uint32_t host_draw_vertex_count = primitive_processing_result.host_draw_vertex_count;
|
||||
const uint32_t host_draw_primitive_count = draw_util::EstimatePrimitiveCount(
|
||||
primitive_processing_result.host_primitive_type, host_draw_vertex_count);
|
||||
|
|
@ -4693,7 +4854,9 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
|
|||
}
|
||||
}
|
||||
bool skate3_ultrawide_submit_draw = true;
|
||||
if (rex::cvar::Query<bool>("skate3_ultrawide_trace_draws")) {
|
||||
// Frame-cached: the registry Query takes the registry mutex and hashes the
|
||||
// name, far too expensive for a per-draw debug-flag check.
|
||||
if (QuerySkate3UltrawideTraceDraws(frame_current_)) {
|
||||
ultrawide_debug::DrawFingerprint fingerprint;
|
||||
fingerprint.bucket =
|
||||
memexport_writes_possible
|
||||
|
|
@ -4753,6 +4916,7 @@ bool VulkanCommandProcessor::IssueDraw(xenos::PrimitiveType prim_type, uint32_t
|
|||
if (!skate3_ultrawide_submit_draw) {
|
||||
return true;
|
||||
}
|
||||
pre_draw_stage_timer.Stop();
|
||||
|
||||
// Draw.
|
||||
uint32_t gpu_timestamp_start = BeginGpuTimestampedDraw(draw_bucket);
|
||||
|
|
@ -5781,7 +5945,9 @@ bool VulkanCommandProcessor::InitializeGpuTimestampResources() {
|
|||
|
||||
if (!ui::vulkan::util::CreateDedicatedAllocationBuffer(
|
||||
vulkan_device,
|
||||
sizeof(uint64_t) * kMaxGpuTimestampQueriesPerFrame * kMaxFramesInFlight,
|
||||
// Two uint64s per query - the timestamp value and the availability
|
||||
// word.
|
||||
sizeof(uint64_t) * 2 * kMaxGpuTimestampQueriesPerFrame * kMaxFramesInFlight,
|
||||
VK_BUFFER_USAGE_TRANSFER_DST_BIT, ui::vulkan::util::MemoryPurpose::kReadback,
|
||||
gpu_timestamp_readback_buffer_, gpu_timestamp_readback_memory_,
|
||||
&gpu_timestamp_readback_memory_type_, &gpu_timestamp_readback_memory_size_)) {
|
||||
|
|
@ -5820,19 +5986,32 @@ void VulkanCommandProcessor::ShutdownGpuTimestampResources() {
|
|||
gpu_timestamp_readback_memory_type_ = UINT32_MAX;
|
||||
gpu_timestamp_readback_memory_size_ = 0;
|
||||
gpu_timestamp_resources_available_ = false;
|
||||
gpu_timestamp_draws_enabled_ = false;
|
||||
for (GpuTimestampFrame& frame : gpu_timestamp_frames_) {
|
||||
frame = {};
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanCommandProcessor::BeginGpuTimestampFrame() {
|
||||
const bool timestamp_diagnostics_enabled =
|
||||
rex::perf::IsCaptureRecording() || REXCVAR_GET(vulkan_gpu_timestamp_buckets) ||
|
||||
REXCVAR_GET(vulkan_gpu_timestamp_trace_frames) > 0;
|
||||
gpu_timestamp_draws_enabled_ = timestamp_diagnostics_enabled;
|
||||
if (!gpu_timestamp_resources_available_ || gpu_timestamp_query_pool_ == VK_NULL_HANDLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool presenter_stats_enabled =
|
||||
graphics_system_ && graphics_system_->presenter() &&
|
||||
graphics_system_->presenter()->GuestFrameStatsEnabled();
|
||||
if (!timestamp_diagnostics_enabled && !presenter_stats_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
GpuTimestampFrame& frame = gpu_timestamp_frames_[frame_current_ % kMaxFramesInFlight];
|
||||
frame.frame = frame_current_;
|
||||
frame.submission = 0;
|
||||
frame.first_submission = GetCurrentSubmission();
|
||||
frame.query_base =
|
||||
uint32_t((frame_current_ % kMaxFramesInFlight) * kMaxGpuTimestampQueriesPerFrame);
|
||||
frame.query_count = 0;
|
||||
|
|
@ -5846,7 +6025,12 @@ void VulkanCommandProcessor::BeginGpuTimestampFrame() {
|
|||
frame.bucket_start_queries.clear();
|
||||
frame.bucket_end_queries.clear();
|
||||
|
||||
if (!rex::perf::IsCaptureRecording()) {
|
||||
// The frame-level start/end pair is measured only while a visible overlay or
|
||||
// explicit timestamp diagnostic needs it.
|
||||
const ui::vulkan::VulkanDevice::Properties& device_properties =
|
||||
GetVulkanDevice()->properties();
|
||||
if (!device_properties.timestampComputeAndGraphics ||
|
||||
device_properties.timestampPeriod <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -5900,10 +6084,15 @@ void VulkanCommandProcessor::ResolveGpuTimestampFrame(VkCommandBuffer command_bu
|
|||
}
|
||||
|
||||
const ui::vulkan::VulkanDevice::Functions& dfn = GetVulkanDevice()->functions();
|
||||
// Copying with the availability word instead of VK_QUERY_RESULT_WAIT_BIT:
|
||||
// waiting hangs the device forever if any query in the range never gets its
|
||||
// timestamp written (this happened with bucket timestamps at 3x resolution
|
||||
// scale), while an unavailable query with the availability flag simply
|
||||
// yields availability == 0 and the sample is skipped at processing.
|
||||
dfn.vkCmdCopyQueryPoolResults(command_buffer, gpu_timestamp_query_pool_, frame.query_base,
|
||||
frame.query_count, gpu_timestamp_readback_buffer_,
|
||||
sizeof(uint64_t) * frame.query_base, sizeof(uint64_t),
|
||||
VK_QUERY_RESULT_64_BIT);
|
||||
sizeof(uint64_t) * 2 * frame.query_base, sizeof(uint64_t) * 2,
|
||||
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
|
||||
}
|
||||
|
||||
void VulkanCommandProcessor::ProcessGpuTimestampResults() {
|
||||
|
|
@ -5937,7 +6126,7 @@ void VulkanCommandProcessor::ProcessGpuTimestampResults() {
|
|||
memory_range.memory = gpu_timestamp_readback_memory_;
|
||||
memory_range.offset = 0;
|
||||
memory_range.size = std::min(
|
||||
rex::round_up(VkDeviceSize(sizeof(uint64_t) * kMaxGpuTimestampQueriesPerFrame *
|
||||
rex::round_up(VkDeviceSize(sizeof(uint64_t) * 2 * kMaxGpuTimestampQueriesPerFrame *
|
||||
kMaxFramesInFlight),
|
||||
properties.nonCoherentAtomSize),
|
||||
gpu_timestamp_readback_memory_size_);
|
||||
|
|
@ -5949,71 +6138,133 @@ void VulkanCommandProcessor::ProcessGpuTimestampResults() {
|
|||
if (!frame.pending || frame.submission == 0 || frame.submission > submission_completed_) {
|
||||
continue;
|
||||
}
|
||||
const uint64_t* timestamps = gpu_timestamp_readback_mapping_ + frame.query_base;
|
||||
// Two uint64s per query - the timestamp value and the availability word.
|
||||
const uint64_t* timestamps = gpu_timestamp_readback_mapping_ + size_t(frame.query_base) * 2;
|
||||
auto timestamp_delta_us = [&](uint32_t start_query, uint32_t end_query) -> int64_t {
|
||||
if (start_query >= frame.query_count || end_query >= frame.query_count) {
|
||||
return 0;
|
||||
}
|
||||
uint64_t start = timestamps[start_query];
|
||||
uint64_t end = timestamps[end_query];
|
||||
if (!timestamps[size_t(start_query) * 2 + 1] || !timestamps[size_t(end_query) * 2 + 1]) {
|
||||
// Query results unavailable - skip the sample.
|
||||
return 0;
|
||||
}
|
||||
uint64_t start = timestamps[size_t(start_query) * 2];
|
||||
uint64_t end = timestamps[size_t(end_query) * 2];
|
||||
if (end <= start) {
|
||||
return 0;
|
||||
}
|
||||
return int64_t(double(end - start) * timestamp_period_ns / 1000.0);
|
||||
};
|
||||
|
||||
int64_t frame_span_us = 0;
|
||||
if (frame.frame_start_query != UINT32_MAX && frame.frame_end_query != UINT32_MAX) {
|
||||
int64_t elapsed_us = timestamp_delta_us(frame.frame_start_query, frame.frame_end_query);
|
||||
if (elapsed_us > 0) {
|
||||
frame_span_us = elapsed_us;
|
||||
rex::perf::IncrementCounter(rex::perf::CounterId::kGpuCommandProcessorFrameUs,
|
||||
elapsed_us);
|
||||
rex::perf::IncrementCounter(rex::perf::CounterId::kGpuTimestampedFrames);
|
||||
if (ui::Presenter* presenter = graphics_system_->presenter()) {
|
||||
presenter->SetGuestFrameGpuSpanMicroseconds(uint64_t(elapsed_us));
|
||||
static bool gpu_span_logged = false;
|
||||
if (!gpu_span_logged) {
|
||||
gpu_span_logged = true;
|
||||
REXGPU_INFO("VulkanCommandProcessor: GPU frame span reporting active ({} us)",
|
||||
elapsed_us);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attribution audit trace - logs every bucket interval of the frame.
|
||||
int32_t trace_remaining = REXCVAR_GET(vulkan_gpu_timestamp_trace_frames);
|
||||
bool trace_this_frame = trace_remaining > 0 && frame_span_us > 0;
|
||||
uint64_t trace_frame_start_ticks = 0;
|
||||
if (trace_this_frame) {
|
||||
REXCVAR_SET(vulkan_gpu_timestamp_trace_frames, trace_remaining - 1);
|
||||
trace_frame_start_ticks = timestamps[size_t(frame.frame_start_query) * 2];
|
||||
REXGPU_INFO("GPU timestamp trace: frame={} queries={}/{} span_us={} intervals={}",
|
||||
frame.frame, frame.query_count, kMaxGpuTimestampQueriesPerFrame, frame_span_us,
|
||||
frame.buckets.size());
|
||||
}
|
||||
auto trace_ticks_to_us = [&](uint64_t ticks) -> int64_t {
|
||||
return int64_t(double(int64_t(ticks - trace_frame_start_ticks)) * timestamp_period_ns /
|
||||
1000.0);
|
||||
};
|
||||
|
||||
uint64_t bucket_draw_us = 0;
|
||||
uint64_t bucket_resolve_us = 0;
|
||||
uint64_t bucket_dump_us = 0;
|
||||
for (uint32_t i = 0; i < frame.buckets.size() && i < frame.bucket_start_queries.size() &&
|
||||
i < frame.bucket_end_queries.size();
|
||||
++i) {
|
||||
uint32_t start_query = frame.bucket_start_queries[i];
|
||||
uint32_t end_query = frame.bucket_end_queries[i];
|
||||
if (end_query == UINT32_MAX) {
|
||||
if (trace_this_frame) {
|
||||
REXGPU_INFO(" [{}] {} START-ONLY (no end query - budget exhausted or frame end)", i,
|
||||
rex::perf::DrawBucketName(frame.buckets[i]));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
int64_t elapsed_us = timestamp_delta_us(start_query, end_query);
|
||||
if (trace_this_frame && start_query < frame.query_count && end_query < frame.query_count) {
|
||||
REXGPU_INFO(" [{}] {} start={} end={} dur={}{}", i,
|
||||
rex::perf::DrawBucketName(frame.buckets[i]),
|
||||
trace_ticks_to_us(timestamps[size_t(start_query) * 2]),
|
||||
trace_ticks_to_us(timestamps[size_t(end_query) * 2]), elapsed_us,
|
||||
(!timestamps[size_t(start_query) * 2 + 1] ||
|
||||
!timestamps[size_t(end_query) * 2 + 1])
|
||||
? " UNAVAILABLE"
|
||||
: "");
|
||||
}
|
||||
if (elapsed_us <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (size_t(frame.buckets[i]) < size_t(rex::perf::DrawBucket::kCount)) {
|
||||
gpu_profile_bucket_us_[size_t(frame.buckets[i])] += uint64_t(elapsed_us);
|
||||
}
|
||||
rex::perf::CounterId counter_id;
|
||||
switch (frame.buckets[i]) {
|
||||
case rex::perf::DrawBucket::kMainColorDepth:
|
||||
counter_id = rex::perf::CounterId::kGpuMainUs;
|
||||
bucket_draw_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
case rex::perf::DrawBucket::kDepthOnly:
|
||||
counter_id = rex::perf::CounterId::kGpuDepthUs;
|
||||
bucket_draw_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
case rex::perf::DrawBucket::kCopyResolve:
|
||||
counter_id = rex::perf::CounterId::kGpuCopyUs;
|
||||
bucket_resolve_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
case rex::perf::DrawBucket::kMemexport:
|
||||
counter_id = rex::perf::CounterId::kGpuMemexportUs;
|
||||
bucket_draw_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
case rex::perf::DrawBucket::kNoPixelShader:
|
||||
counter_id = rex::perf::CounterId::kGpuNoPixelShaderUs;
|
||||
bucket_draw_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
case rex::perf::DrawBucket::kCopyDump:
|
||||
counter_id = rex::perf::CounterId::kGpuCopyDumpUs;
|
||||
bucket_dump_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
case rex::perf::DrawBucket::kCopyResolveShader:
|
||||
counter_id = rex::perf::CounterId::kGpuCopyResolveUs;
|
||||
bucket_resolve_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
case rex::perf::DrawBucket::kCopyResolveFast32:
|
||||
counter_id = rex::perf::CounterId::kGpuCopyResolveFast32Us;
|
||||
bucket_resolve_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
case rex::perf::DrawBucket::kCopyResolveFull32:
|
||||
counter_id = rex::perf::CounterId::kGpuCopyResolveFull32Us;
|
||||
bucket_resolve_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
case rex::perf::DrawBucket::kResolveDownscale:
|
||||
counter_id = rex::perf::CounterId::kGpuResolveDownscaleUs;
|
||||
bucket_resolve_us += uint64_t(elapsed_us);
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
|
|
@ -6021,6 +6272,47 @@ void VulkanCommandProcessor::ProcessGpuTimestampResults() {
|
|||
rex::perf::IncrementCounter(counter_id, elapsed_us);
|
||||
rex::perf::IncrementCounter(rex::perf::CounterId::kGpuTimestampedDraws);
|
||||
}
|
||||
if (ui::Presenter* presenter = graphics_system_->presenter()) {
|
||||
// Zeros when bucket timestamps are disabled, which hides the breakdown
|
||||
// in the FPS overlay.
|
||||
presenter->SetGuestFrameGpuBreakdownMicroseconds(bucket_draw_us, bucket_resolve_us,
|
||||
bucket_dump_us);
|
||||
}
|
||||
|
||||
// Periodic full attribution log - the per-bucket sums were accumulated
|
||||
// above for every bucket including ones not shown in the FPS overlay.
|
||||
if (frame_span_us > 0 && REXCVAR_GET(vulkan_gpu_timestamp_buckets)) {
|
||||
gpu_profile_span_us_ += uint64_t(frame_span_us);
|
||||
if (frame.submission >= frame.first_submission) {
|
||||
gpu_profile_submissions_ += frame.submission - frame.first_submission + 1;
|
||||
}
|
||||
if (++gpu_profile_frames_ >= kGpuProfileLogFrameInterval) {
|
||||
uint64_t buckets_sum_us = 0;
|
||||
std::string breakdown;
|
||||
for (size_t bucket = 0; bucket < size_t(rex::perf::DrawBucket::kCount); ++bucket) {
|
||||
uint64_t total_us = gpu_profile_bucket_us_[bucket];
|
||||
buckets_sum_us += total_us;
|
||||
if (!total_us) {
|
||||
continue;
|
||||
}
|
||||
breakdown += fmt::format(" {}={}", rex::perf::DrawBucketName(rex::perf::DrawBucket(bucket)),
|
||||
total_us / gpu_profile_frames_);
|
||||
}
|
||||
uint64_t span_avg_us = gpu_profile_span_us_ / gpu_profile_frames_;
|
||||
uint64_t sum_avg_us = buckets_sum_us / gpu_profile_frames_;
|
||||
REXGPU_INFO(
|
||||
"Vulkan GPU profile (avg us/frame over {} frames): span={} buckets={} untimed={} "
|
||||
"submissions={} queries={}/{} |{}",
|
||||
gpu_profile_frames_, span_avg_us, sum_avg_us,
|
||||
span_avg_us > sum_avg_us ? span_avg_us - sum_avg_us : 0,
|
||||
gpu_profile_submissions_ / gpu_profile_frames_, frame.query_count,
|
||||
kMaxGpuTimestampQueriesPerFrame, breakdown);
|
||||
std::memset(gpu_profile_bucket_us_, 0, sizeof(gpu_profile_bucket_us_));
|
||||
gpu_profile_span_us_ = 0;
|
||||
gpu_profile_submissions_ = 0;
|
||||
gpu_profile_frames_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
frame.pending = false;
|
||||
frame.active_bucket_valid = false;
|
||||
|
|
@ -6033,7 +6325,7 @@ void VulkanCommandProcessor::ProcessGpuTimestampResults() {
|
|||
|
||||
uint32_t VulkanCommandProcessor::BeginGpuTimestampedDraw(rex::perf::DrawBucket bucket) {
|
||||
if (!gpu_timestamp_resources_available_ || gpu_timestamp_query_pool_ == VK_NULL_HANDLE ||
|
||||
!rex::perf::IsCaptureRecording() || !frame_open_) {
|
||||
!gpu_timestamp_draws_enabled_ || !frame_open_) {
|
||||
return UINT32_MAX;
|
||||
}
|
||||
|
||||
|
|
@ -6342,9 +6634,21 @@ void VulkanCommandProcessor::CheckSubmissionFenceAndDeviceLoss(uint64_t await_su
|
|||
{
|
||||
rex::perf::ScopedCounterTimer fence_wait_timer(
|
||||
rex::perf::CounterId::kCpuD3D12FenceWaitUs);
|
||||
rex::perf::ScopedCounterTimer gpu_thread_wait_timer(
|
||||
rex::perf::CounterId::kGpuThreadFenceWaitUs, true);
|
||||
// Also reported through the presenter so the FPS overlay can show the
|
||||
// blocked time in builds without perf counters - cheap around a
|
||||
// blocking wait.
|
||||
const std::chrono::steady_clock::time_point wait_start = std::chrono::steady_clock::now();
|
||||
wait_result =
|
||||
dfn.vkWaitForFences(device, uint32_t(await_submission - submission_completed_),
|
||||
submissions_in_flight_fences_.data(), VK_TRUE, UINT64_MAX);
|
||||
if (ui::Presenter* wait_presenter = graphics_system_->presenter()) {
|
||||
wait_presenter->AddGuestFrameWaitMicroseconds(uint64_t(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() -
|
||||
wait_start)
|
||||
.count()));
|
||||
}
|
||||
}
|
||||
if (wait_result == VK_SUCCESS) {
|
||||
fences_awaited += await_submission - submission_completed_;
|
||||
|
|
@ -6598,6 +6902,11 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) {
|
|||
shared_memory_and_edram_descriptor_set_;
|
||||
current_graphics_descriptor_set_values_up_to_date_ =
|
||||
UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetSharedMemoryAndEdram;
|
||||
// Sets cached for reuse belong to the previous frame and may be recycled
|
||||
// once it completes.
|
||||
texture_transient_descriptor_set_reuse_cache_.clear();
|
||||
current_texture_descriptor_set_hash_valid_[0] = false;
|
||||
current_texture_descriptor_set_hash_valid_[1] = false;
|
||||
|
||||
// Reclaim pool pages - no need to do this every small submission since some
|
||||
// may be reused.
|
||||
|
|
@ -6952,6 +7261,11 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) {
|
|||
}
|
||||
|
||||
void VulkanCommandProcessor::ClearTransientDescriptorPools() {
|
||||
texture_transient_descriptor_set_reuse_cache_.clear();
|
||||
current_texture_descriptor_set_hash_valid_[0] = false;
|
||||
current_texture_descriptor_set_hash_valid_[1] = false;
|
||||
draw_binding_state_valid_ = false;
|
||||
texture_bindings_unchanged_this_draw_ = false;
|
||||
texture_transient_descriptor_sets_free_.clear();
|
||||
texture_transient_descriptor_sets_used_.clear();
|
||||
transient_descriptor_allocator_textures_.Reset();
|
||||
|
|
@ -8052,70 +8366,190 @@ bool VulkanCommandProcessor::UpdateBindings(const VulkanShader* vertex_shader,
|
|||
sampler_count_pixel = 0;
|
||||
texture_count_pixel = 0;
|
||||
}
|
||||
// TODO(Triang3l): Reuse texture and sampler bindings if not changed.
|
||||
current_graphics_descriptor_set_values_up_to_date_ &=
|
||||
~((UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetTexturesVertex) |
|
||||
(UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetTexturesPixel));
|
||||
// Fill the texture and sampler image infos. These are also the change
|
||||
// detection key for descriptor set reuse, so they're gathered whenever the
|
||||
// shaders use any textures or samplers, not only when writing.
|
||||
bool have_vertex_textures = texture_count_vertex || sampler_count_vertex;
|
||||
bool have_pixel_textures = texture_count_pixel || sampler_count_pixel;
|
||||
// Fast path: IssueDraw established that this draw's shaders, samplers and
|
||||
// texture binding views are identical to the previous draw's in this
|
||||
// submission, and the recorded texture descriptor sets still hold valid
|
||||
// values - keep them without regathering and rehashing the image infos.
|
||||
const bool texture_descriptor_fast_path =
|
||||
texture_bindings_unchanged_this_draw_ &&
|
||||
REXCVAR_GET(vulkan_reuse_transient_texture_descriptors) &&
|
||||
(!have_vertex_textures ||
|
||||
(current_texture_descriptor_set_hash_valid_[0] &&
|
||||
(current_graphics_descriptor_set_values_up_to_date_ &
|
||||
(UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetTexturesVertex)))) &&
|
||||
(!have_pixel_textures ||
|
||||
(current_texture_descriptor_set_hash_valid_[1] &&
|
||||
(current_graphics_descriptor_set_values_up_to_date_ &
|
||||
(UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetTexturesPixel))));
|
||||
size_t vertex_texture_image_info_offset = 0;
|
||||
size_t vertex_sampler_image_info_offset = 0;
|
||||
size_t pixel_texture_image_info_offset = 0;
|
||||
size_t pixel_sampler_image_info_offset = 0;
|
||||
if (!texture_descriptor_fast_path) {
|
||||
descriptor_write_image_info_.clear();
|
||||
descriptor_write_image_info_.reserve(
|
||||
(have_vertex_textures ? texture_count_vertex + sampler_count_vertex : 0) +
|
||||
(have_pixel_textures ? texture_count_pixel + sampler_count_pixel : 0));
|
||||
vertex_texture_image_info_offset = descriptor_write_image_info_.size();
|
||||
if (have_vertex_textures && texture_count_vertex) {
|
||||
for (const VulkanShader::TextureBinding& texture_binding : textures_vertex) {
|
||||
VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back();
|
||||
descriptor_image_info.imageView = texture_cache_->GetActiveBindingOrNullImageView(
|
||||
texture_binding.fetch_constant, texture_binding.dimension,
|
||||
bool(texture_binding.is_signed));
|
||||
descriptor_image_info.imageLayout = descriptor_image_info.imageView != VK_NULL_HANDLE
|
||||
? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
|
||||
: VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
}
|
||||
vertex_sampler_image_info_offset = descriptor_write_image_info_.size();
|
||||
if (have_vertex_textures && sampler_count_vertex) {
|
||||
for (const std::pair<VulkanTextureCache::SamplerParameters, VkSampler>& sampler_pair :
|
||||
current_samplers_vertex_) {
|
||||
VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back();
|
||||
descriptor_image_info.sampler = sampler_pair.second;
|
||||
}
|
||||
}
|
||||
pixel_texture_image_info_offset = descriptor_write_image_info_.size();
|
||||
if (have_pixel_textures && texture_count_pixel) {
|
||||
for (const VulkanShader::TextureBinding& texture_binding : *textures_pixel) {
|
||||
VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back();
|
||||
descriptor_image_info.imageView = texture_cache_->GetActiveBindingOrNullImageView(
|
||||
texture_binding.fetch_constant, texture_binding.dimension,
|
||||
bool(texture_binding.is_signed));
|
||||
descriptor_image_info.imageLayout = descriptor_image_info.imageView != VK_NULL_HANDLE
|
||||
? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
|
||||
: VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
}
|
||||
pixel_sampler_image_info_offset = descriptor_write_image_info_.size();
|
||||
if (have_pixel_textures && sampler_count_pixel) {
|
||||
for (const std::pair<VulkanTextureCache::SamplerParameters, VkSampler>& sampler_pair :
|
||||
current_samplers_pixel_) {
|
||||
VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back();
|
||||
descriptor_image_info.sampler = sampler_pair.second;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Texture/sampler descriptor set reuse (resolving the upstream
|
||||
// "TODO(Triang3l): Reuse texture and sampler bindings if not changed"):
|
||||
// hash the would-be contents, then either keep the currently recorded set,
|
||||
// adopt a set written with identical contents earlier this frame, or fall
|
||||
// through to allocating and writing a new one. The image info structs are
|
||||
// value-initialized, so hashing only the meaningful fields is stable.
|
||||
const bool reuse_texture_descriptor_sets =
|
||||
REXCVAR_GET(vulkan_reuse_transient_texture_descriptors);
|
||||
auto hash_texture_bindings = [](bool is_vertex, uint32_t texture_count, uint32_t sampler_count,
|
||||
const VkDescriptorImageInfo* texture_image_info,
|
||||
const VkDescriptorImageInfo* sampler_image_info) -> uint64_t {
|
||||
XXH3_state_t hash_state;
|
||||
XXH3_64bits_reset(&hash_state);
|
||||
const uint32_t layout_dimensions[3] = {uint32_t(is_vertex), texture_count, sampler_count};
|
||||
XXH3_64bits_update(&hash_state, layout_dimensions, sizeof(layout_dimensions));
|
||||
for (uint32_t i = 0; i < texture_count; ++i) {
|
||||
XXH3_64bits_update(&hash_state, &texture_image_info[i].imageView, sizeof(VkImageView));
|
||||
}
|
||||
for (uint32_t i = 0; i < sampler_count; ++i) {
|
||||
XXH3_64bits_update(&hash_state, &sampler_image_info[i].sampler, sizeof(VkSampler));
|
||||
}
|
||||
return XXH3_64bits_digest(&hash_state);
|
||||
};
|
||||
// Indexed [0] = vertex, [1] = pixel.
|
||||
uint64_t texture_descriptor_set_hashes[2] = {};
|
||||
bool texture_descriptor_set_contents_current[2] = {};
|
||||
VkDescriptorSet texture_descriptor_set_cached[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
|
||||
if (texture_descriptor_fast_path) {
|
||||
// The recorded sets hold exactly this draw's contents; the stored hashes
|
||||
// also remain accurate for later slow-path draws.
|
||||
texture_descriptor_set_contents_current[0] = have_vertex_textures;
|
||||
texture_descriptor_set_contents_current[1] = have_pixel_textures;
|
||||
} else if (reuse_texture_descriptor_sets) {
|
||||
if (have_vertex_textures) {
|
||||
texture_descriptor_set_hashes[0] = hash_texture_bindings(
|
||||
true, texture_count_vertex, sampler_count_vertex,
|
||||
descriptor_write_image_info_.data() + vertex_texture_image_info_offset,
|
||||
descriptor_write_image_info_.data() + vertex_sampler_image_info_offset);
|
||||
if (current_texture_descriptor_set_hash_valid_[0] &&
|
||||
current_texture_descriptor_set_hashes_[0] == texture_descriptor_set_hashes[0]) {
|
||||
texture_descriptor_set_contents_current[0] = true;
|
||||
} else {
|
||||
auto reuse_it =
|
||||
texture_transient_descriptor_set_reuse_cache_.find(texture_descriptor_set_hashes[0]);
|
||||
if (reuse_it != texture_transient_descriptor_set_reuse_cache_.end()) {
|
||||
texture_descriptor_set_cached[0] = reuse_it->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (have_pixel_textures) {
|
||||
texture_descriptor_set_hashes[1] = hash_texture_bindings(
|
||||
false, texture_count_pixel, sampler_count_pixel,
|
||||
descriptor_write_image_info_.data() + pixel_texture_image_info_offset,
|
||||
descriptor_write_image_info_.data() + pixel_sampler_image_info_offset);
|
||||
if (current_texture_descriptor_set_hash_valid_[1] &&
|
||||
current_texture_descriptor_set_hashes_[1] == texture_descriptor_set_hashes[1]) {
|
||||
texture_descriptor_set_contents_current[1] = true;
|
||||
} else {
|
||||
auto reuse_it =
|
||||
texture_transient_descriptor_set_reuse_cache_.find(texture_descriptor_set_hashes[1]);
|
||||
if (reuse_it != texture_transient_descriptor_set_reuse_cache_.end()) {
|
||||
texture_descriptor_set_cached[1] = reuse_it->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
current_texture_descriptor_set_hash_valid_[0] = false;
|
||||
current_texture_descriptor_set_hash_valid_[1] = false;
|
||||
}
|
||||
|
||||
constexpr uint32_t kTexturesVertexBit = UINT32_C(1)
|
||||
<< SpirvShaderTranslator::kDescriptorSetTexturesVertex;
|
||||
constexpr uint32_t kTexturesPixelBit = UINT32_C(1)
|
||||
<< SpirvShaderTranslator::kDescriptorSetTexturesPixel;
|
||||
if (texture_descriptor_set_contents_current[0]) {
|
||||
// The recorded set already holds these contents (also recovers the value
|
||||
// validity if a previous draw failed after invalidating it).
|
||||
current_graphics_descriptor_set_values_up_to_date_ |= kTexturesVertexBit;
|
||||
} else if (texture_descriptor_set_cached[0] != VK_NULL_HANDLE) {
|
||||
current_graphics_descriptor_sets_[SpirvShaderTranslator::kDescriptorSetTexturesVertex] =
|
||||
texture_descriptor_set_cached[0];
|
||||
current_graphics_descriptor_set_values_up_to_date_ |= kTexturesVertexBit;
|
||||
// A different set object must be rebound.
|
||||
current_graphics_descriptor_sets_bound_up_to_date_ &= ~kTexturesVertexBit;
|
||||
current_texture_descriptor_set_hashes_[0] = texture_descriptor_set_hashes[0];
|
||||
current_texture_descriptor_set_hash_valid_[0] = true;
|
||||
} else {
|
||||
current_graphics_descriptor_set_values_up_to_date_ &= ~kTexturesVertexBit;
|
||||
}
|
||||
if (texture_descriptor_set_contents_current[1]) {
|
||||
current_graphics_descriptor_set_values_up_to_date_ |= kTexturesPixelBit;
|
||||
} else if (texture_descriptor_set_cached[1] != VK_NULL_HANDLE) {
|
||||
current_graphics_descriptor_sets_[SpirvShaderTranslator::kDescriptorSetTexturesPixel] =
|
||||
texture_descriptor_set_cached[1];
|
||||
current_graphics_descriptor_set_values_up_to_date_ |= kTexturesPixelBit;
|
||||
current_graphics_descriptor_sets_bound_up_to_date_ &= ~kTexturesPixelBit;
|
||||
current_texture_descriptor_set_hashes_[1] = texture_descriptor_set_hashes[1];
|
||||
current_texture_descriptor_set_hash_valid_[1] = true;
|
||||
} else {
|
||||
current_graphics_descriptor_set_values_up_to_date_ &= ~kTexturesPixelBit;
|
||||
}
|
||||
|
||||
// Make sure new descriptor sets are bound to the command buffer.
|
||||
|
||||
current_graphics_descriptor_sets_bound_up_to_date_ &=
|
||||
current_graphics_descriptor_set_values_up_to_date_;
|
||||
|
||||
// Fill the texture and sampler write image infos.
|
||||
|
||||
bool write_vertex_textures =
|
||||
(texture_count_vertex || sampler_count_vertex) &&
|
||||
!(current_graphics_descriptor_set_values_up_to_date_ &
|
||||
(UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetTexturesVertex));
|
||||
have_vertex_textures &&
|
||||
!(current_graphics_descriptor_set_values_up_to_date_ & kTexturesVertexBit);
|
||||
bool write_pixel_textures =
|
||||
(texture_count_pixel || sampler_count_pixel) &&
|
||||
!(current_graphics_descriptor_set_values_up_to_date_ &
|
||||
(UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetTexturesPixel));
|
||||
descriptor_write_image_info_.clear();
|
||||
descriptor_write_image_info_.reserve(
|
||||
(write_vertex_textures ? texture_count_vertex + sampler_count_vertex : 0) +
|
||||
(write_pixel_textures ? texture_count_pixel + sampler_count_pixel : 0));
|
||||
size_t vertex_texture_image_info_offset = descriptor_write_image_info_.size();
|
||||
if (write_vertex_textures && texture_count_vertex) {
|
||||
for (const VulkanShader::TextureBinding& texture_binding : textures_vertex) {
|
||||
VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back();
|
||||
descriptor_image_info.imageView = texture_cache_->GetActiveBindingOrNullImageView(
|
||||
texture_binding.fetch_constant, texture_binding.dimension,
|
||||
bool(texture_binding.is_signed));
|
||||
descriptor_image_info.imageLayout = descriptor_image_info.imageView != VK_NULL_HANDLE
|
||||
? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
|
||||
: VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
}
|
||||
size_t vertex_sampler_image_info_offset = descriptor_write_image_info_.size();
|
||||
if (write_vertex_textures && sampler_count_vertex) {
|
||||
for (const std::pair<VulkanTextureCache::SamplerParameters, VkSampler>& sampler_pair :
|
||||
current_samplers_vertex_) {
|
||||
VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back();
|
||||
descriptor_image_info.sampler = sampler_pair.second;
|
||||
}
|
||||
}
|
||||
size_t pixel_texture_image_info_offset = descriptor_write_image_info_.size();
|
||||
if (write_pixel_textures && texture_count_pixel) {
|
||||
for (const VulkanShader::TextureBinding& texture_binding : *textures_pixel) {
|
||||
VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back();
|
||||
descriptor_image_info.imageView = texture_cache_->GetActiveBindingOrNullImageView(
|
||||
texture_binding.fetch_constant, texture_binding.dimension,
|
||||
bool(texture_binding.is_signed));
|
||||
descriptor_image_info.imageLayout = descriptor_image_info.imageView != VK_NULL_HANDLE
|
||||
? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
|
||||
: VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
}
|
||||
size_t pixel_sampler_image_info_offset = descriptor_write_image_info_.size();
|
||||
if (write_pixel_textures && sampler_count_pixel) {
|
||||
for (const std::pair<VulkanTextureCache::SamplerParameters, VkSampler>& sampler_pair :
|
||||
current_samplers_pixel_) {
|
||||
VkDescriptorImageInfo& descriptor_image_info = descriptor_write_image_info_.emplace_back();
|
||||
descriptor_image_info.sampler = sampler_pair.second;
|
||||
}
|
||||
}
|
||||
have_pixel_textures &&
|
||||
!(current_graphics_descriptor_set_values_up_to_date_ & kTexturesPixelBit);
|
||||
|
||||
// Write the new descriptor sets.
|
||||
|
||||
|
|
@ -8129,8 +8563,15 @@ bool VulkanCommandProcessor::UpdateBindings(const VulkanShader* vertex_shader,
|
|||
assert_not_zero(current_graphics_descriptor_set_values_up_to_date_ &
|
||||
(UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetSharedMemoryAndEdram));
|
||||
// Constant buffers.
|
||||
if (!(current_graphics_descriptor_set_values_up_to_date_ &
|
||||
(UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetConstants))) {
|
||||
if (constants_push_descriptors_used_) {
|
||||
// The buffer infos are complete at this point; the descriptors themselves
|
||||
// are pushed into the command buffer right before binding (where the
|
||||
// bound-up-to-date bit, cleared above when any constant buffer was
|
||||
// rewritten or by pipeline layout/command buffer changes, is checked).
|
||||
current_graphics_descriptor_set_values_up_to_date_ |=
|
||||
UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetConstants;
|
||||
} else if (!(current_graphics_descriptor_set_values_up_to_date_ &
|
||||
(UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetConstants))) {
|
||||
VkDescriptorSet constants_descriptor_set;
|
||||
if (!constants_transient_descriptors_free_.empty()) {
|
||||
constants_descriptor_set = constants_transient_descriptors_free_.back();
|
||||
|
|
@ -8207,6 +8648,26 @@ bool VulkanCommandProcessor::UpdateBindings(const VulkanShader* vertex_shader,
|
|||
// Only make valid if all descriptor sets have been allocated and written
|
||||
// successfully.
|
||||
current_graphics_descriptor_set_values_up_to_date_ |= write_descriptor_set_bits;
|
||||
// Register the newly written texture sets for reuse by later draws in this
|
||||
// frame. This must happen only after the vkUpdateDescriptorSets call - a
|
||||
// set staged for writing but abandoned by an early return would otherwise
|
||||
// be reused with undefined contents.
|
||||
if (reuse_texture_descriptor_sets) {
|
||||
if (write_vertex_textures) {
|
||||
current_texture_descriptor_set_hashes_[0] = texture_descriptor_set_hashes[0];
|
||||
current_texture_descriptor_set_hash_valid_[0] = true;
|
||||
texture_transient_descriptor_set_reuse_cache_.emplace(
|
||||
texture_descriptor_set_hashes[0],
|
||||
current_graphics_descriptor_sets_[SpirvShaderTranslator::kDescriptorSetTexturesVertex]);
|
||||
}
|
||||
if (write_pixel_textures) {
|
||||
current_texture_descriptor_set_hashes_[1] = texture_descriptor_set_hashes[1];
|
||||
current_texture_descriptor_set_hash_valid_[1] = true;
|
||||
texture_transient_descriptor_set_reuse_cache_.emplace(
|
||||
texture_descriptor_set_hashes[1],
|
||||
current_graphics_descriptor_sets_[SpirvShaderTranslator::kDescriptorSetTexturesPixel]);
|
||||
}
|
||||
}
|
||||
|
||||
// Bind the new descriptor sets.
|
||||
uint32_t descriptor_sets_needed = (UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetCount) - 1;
|
||||
|
|
@ -8216,6 +8677,21 @@ bool VulkanCommandProcessor::UpdateBindings(const VulkanShader* vertex_shader,
|
|||
if (!texture_count_pixel && !sampler_count_pixel) {
|
||||
descriptor_sets_needed &= ~(UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetTexturesPixel);
|
||||
}
|
||||
if (constants_push_descriptors_used_) {
|
||||
// The constants set index uses push descriptors - record the push when
|
||||
// its contents or the command buffer binding state changed, and exclude
|
||||
// it from the regular vkCmdBindDescriptorSets ranges.
|
||||
constexpr uint32_t kConstantsDescriptorBit =
|
||||
UINT32_C(1) << SpirvShaderTranslator::kDescriptorSetConstants;
|
||||
if (!(current_graphics_descriptor_sets_bound_up_to_date_ & kConstantsDescriptorBit)) {
|
||||
deferred_command_buffer_.CmdVkPushUniformBufferDescriptorSet(
|
||||
VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
current_guest_graphics_pipeline_layout_->GetPipelineLayout(),
|
||||
SpirvShaderTranslator::kDescriptorSetConstants,
|
||||
SpirvShaderTranslator::kConstantBufferCount, current_constant_buffer_infos_);
|
||||
current_graphics_descriptor_sets_bound_up_to_date_ |= kConstantsDescriptorBit;
|
||||
}
|
||||
}
|
||||
uint32_t descriptor_sets_remaining =
|
||||
descriptor_sets_needed & ~current_graphics_descriptor_sets_bound_up_to_date_;
|
||||
uint32_t descriptor_set_index;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
|
@ -138,6 +139,16 @@ void DeferredCommandBuffer::Execute(VkCommandBuffer command_buffer) {
|
|||
rex::align(sizeof(ArgsVkClearColorImage), alignof(VkImageSubresourceRange))));
|
||||
} break;
|
||||
|
||||
case Command::kVkClearDepthStencilImage: {
|
||||
auto& args = *reinterpret_cast<const ArgsVkClearDepthStencilImage*>(stream);
|
||||
dfn.vkCmdClearDepthStencilImage(
|
||||
command_buffer, args.image, args.image_layout, &args.depth_stencil, args.range_count,
|
||||
reinterpret_cast<const VkImageSubresourceRange*>(
|
||||
reinterpret_cast<const uint8_t*>(stream) +
|
||||
rex::align(sizeof(ArgsVkClearDepthStencilImage),
|
||||
alignof(VkImageSubresourceRange))));
|
||||
} break;
|
||||
|
||||
case Command::kVkCopyBuffer: {
|
||||
auto& args = *reinterpret_cast<const ArgsVkCopyBuffer*>(stream);
|
||||
dfn.vkCmdCopyBuffer(command_buffer, args.src_buffer, args.dst_buffer, args.region_count,
|
||||
|
|
@ -266,6 +277,34 @@ void DeferredCommandBuffer::Execute(VkCommandBuffer command_buffer) {
|
|||
reinterpret_cast<const uint8_t*>(stream) + sizeof(ArgsVkPushConstants));
|
||||
} break;
|
||||
|
||||
case Command::kVkPushUniformBufferDescriptorSet: {
|
||||
auto& args = *reinterpret_cast<const ArgsVkPushUniformBufferDescriptorSet*>(stream);
|
||||
size_t offset_bytes = rex::align(sizeof(ArgsVkPushUniformBufferDescriptorSet),
|
||||
alignof(VkDescriptorBufferInfo));
|
||||
const VkDescriptorBufferInfo* buffer_infos = reinterpret_cast<const VkDescriptorBufferInfo*>(
|
||||
reinterpret_cast<const uint8_t*>(stream) + offset_bytes);
|
||||
constexpr uint32_t kMaxPushedUniformBuffers = 8;
|
||||
assert_true(args.buffer_count <= kMaxPushedUniformBuffers);
|
||||
VkWriteDescriptorSet writes[kMaxPushedUniformBuffers];
|
||||
uint32_t write_count = std::min(args.buffer_count, kMaxPushedUniformBuffers);
|
||||
for (uint32_t i = 0; i < write_count; ++i) {
|
||||
VkWriteDescriptorSet& write = writes[i];
|
||||
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
||||
write.pNext = nullptr;
|
||||
// dstSet is ignored for push descriptors.
|
||||
write.dstSet = VK_NULL_HANDLE;
|
||||
write.dstBinding = i;
|
||||
write.dstArrayElement = 0;
|
||||
write.descriptorCount = 1;
|
||||
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
||||
write.pImageInfo = nullptr;
|
||||
write.pBufferInfo = &buffer_infos[i];
|
||||
write.pTexelBufferView = nullptr;
|
||||
}
|
||||
dfn.vkCmdPushDescriptorSetKHR(command_buffer, args.pipeline_bind_point, args.layout,
|
||||
args.set, write_count, writes);
|
||||
} break;
|
||||
|
||||
case Command::kVkResetQueryPool: {
|
||||
auto& args = *reinterpret_cast<const ArgsVkResetQueryPool*>(stream);
|
||||
dfn.vkCmdResetQueryPool(command_buffer, args.query_pool, args.first_query,
|
||||
|
|
|
|||
|
|
@ -3025,12 +3025,12 @@ bool VulkanPipelineCache::EnsurePipelineCreated(const PipelineCreationArguments&
|
|||
// be created from the disk storage.
|
||||
|
||||
if (creation_arguments.pixel_shader) {
|
||||
REXGPU_INFO("Creating graphics pipeline state with VS {:016X}, PS {:016X}",
|
||||
creation_arguments.vertex_shader->shader().ucode_data_hash(),
|
||||
creation_arguments.pixel_shader->shader().ucode_data_hash());
|
||||
REXGPU_DEBUG("Creating graphics pipeline state with VS {:016X}, PS {:016X}",
|
||||
creation_arguments.vertex_shader->shader().ucode_data_hash(),
|
||||
creation_arguments.pixel_shader->shader().ucode_data_hash());
|
||||
} else {
|
||||
REXGPU_INFO("Creating graphics pipeline state with VS {:016X}",
|
||||
creation_arguments.vertex_shader->shader().ucode_data_hash());
|
||||
REXGPU_DEBUG("Creating graphics pipeline state with VS {:016X}",
|
||||
creation_arguments.vertex_shader->shader().ucode_data_hash());
|
||||
}
|
||||
|
||||
const PipelineDescription& description = creation_arguments.pipeline->first;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -345,6 +345,9 @@ bool VulkanSharedMemory::UploadRanges(
|
|||
if (upload_page_ranges.empty()) {
|
||||
return true;
|
||||
}
|
||||
// Attribute the GPU time of the upload copies (and the barriers around
|
||||
// them) to the shared memory upload profiling bucket.
|
||||
command_processor_.BeginGpuTimestampedRegion(rex::perf::DrawBucket::kSharedMemoryUpload);
|
||||
// upload_page_ranges are sorted, use them to determine the range for the
|
||||
// ordering barrier.
|
||||
Use(Usage::kTransferDestination,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -20,6 +20,7 @@ set(REXUI_CORE_SOURCES
|
|||
set(REXUI_OVERLAY_SOURCES
|
||||
overlay/debug_overlay.cpp
|
||||
overlay/console_overlay.cpp
|
||||
overlay/fps_overlay.cpp
|
||||
overlay/acquire_wizard_overlay.cpp
|
||||
overlay/install_wizard_overlay.cpp
|
||||
overlay/simple_settings_overlay.cpp
|
||||
|
|
@ -145,6 +146,11 @@ if(WIN32)
|
|||
target_link_libraries(rexui PUBLIC
|
||||
dwmapi
|
||||
Shcore
|
||||
# The backend-agnostic Presenter drives a DXGI UI tick thread on Windows
|
||||
# (display-refresh sync), so DXGI is required regardless of the graphics
|
||||
# backend selected (e.g. Vulkan-on-Windows builds with D3D12 disabled).
|
||||
dxgi
|
||||
dxguid
|
||||
)
|
||||
elseif(APPLE)
|
||||
target_link_libraries(rexui PUBLIC SDL3::SDL3)
|
||||
|
|
@ -163,11 +169,9 @@ else()
|
|||
)
|
||||
endif()
|
||||
|
||||
# D3D12 backend dependencies
|
||||
# D3D12 backend dependencies (dxgi/dxguid linked unconditionally on Windows above)
|
||||
if(REXGLUE_USE_D3D12)
|
||||
target_link_libraries(rexui PUBLIC
|
||||
dxgi
|
||||
dxguid
|
||||
dxc-headers
|
||||
)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
* @modified Tom Clay, 2026 - Adapted for ReXGlue runtime
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
|
@ -408,7 +409,8 @@ void ImGuiDrawer::Draw(UIDrawContext& ui_draw_context) {
|
|||
// it now if needed.
|
||||
DetachIfLastDialogRemoved();
|
||||
|
||||
if (!dialogs_.empty()) {
|
||||
if (std::any_of(dialogs_.cbegin(), dialogs_.cend(),
|
||||
[](const ImGuiDialog* dialog) { return dialog->WantsContinuousRepaint(); })) {
|
||||
// Repaint (and handle input) continuously if still active.
|
||||
presenter_->RequestUIPaintFromUIThread();
|
||||
}
|
||||
|
|
|
|||
66
src/ui/overlay/fps_overlay.cpp
Normal file
66
src/ui/overlay/fps_overlay.cpp
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* @file ui/overlay/fps_overlay.cpp
|
||||
*
|
||||
* @brief Minimal guest FPS readout overlay. See fps_overlay.h for
|
||||
* details.
|
||||
*
|
||||
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* @license BSD 3-Clause License
|
||||
* See LICENSE file in the project root for full license text.
|
||||
*/
|
||||
#include <rex/ui/overlay/fps_overlay.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace rex::ui {
|
||||
|
||||
void FpsOverlayDialog::OnDraw(ImGuiIO& io) {
|
||||
Presenter::GuestFrameStats stats;
|
||||
if (presenter_) {
|
||||
stats = presenter_->GetGuestFrameStats();
|
||||
}
|
||||
|
||||
constexpr float kMargin = 10.0f;
|
||||
ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x - kMargin, kMargin), ImGuiCond_Always,
|
||||
ImVec2(1.0f, 0.0f));
|
||||
ImGui::SetNextWindowBgAlpha(0.4f);
|
||||
ImGui::PushFont(nullptr, 20.0f);
|
||||
if (ImGui::Begin("##fps_overlay", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs |
|
||||
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoFocusOnAppearing |
|
||||
ImGuiWindowFlags_AlwaysAutoResize)) {
|
||||
if (stats.frame_count > 0 && stats.fps > 0.0) {
|
||||
ImGui::Text("%.0f FPS", stats.fps);
|
||||
ImGui::PushFont(nullptr, 14.0f);
|
||||
ImGui::Text("%.2f ms", stats.frame_time_ms);
|
||||
// Time the GPU emulation thread spent blocked on host GPU fences last
|
||||
// frame: near the frame time = GPU-bound, near zero = thread-bound.
|
||||
ImGui::Text("wait %.2f ms", stats.wait_ms);
|
||||
if (stats.gpu_ms > 0.0) {
|
||||
// Device-timeline span of a recent frame (first to last command,
|
||||
// including idle gaps between submissions).
|
||||
ImGui::Text("gpu %.2f ms", stats.gpu_ms);
|
||||
double gpu_known_ms = stats.gpu_draw_ms + stats.gpu_resolve_ms + stats.gpu_dump_ms;
|
||||
if (gpu_known_ms > 0.0) {
|
||||
// "other" covers everything not in a bucket: render target
|
||||
// ownership transfers, barriers, and idle gaps.
|
||||
ImGui::Text("draw %.2f res %.2f", stats.gpu_draw_ms, stats.gpu_resolve_ms);
|
||||
ImGui::Text("dump %.2f other %.2f", stats.gpu_dump_ms,
|
||||
std::max(0.0, stats.gpu_ms - gpu_known_ms));
|
||||
}
|
||||
}
|
||||
ImGui::PopFont();
|
||||
} else {
|
||||
ImGui::TextUnformatted("-- FPS");
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
} // namespace rex::ui
|
||||
|
|
@ -585,6 +585,16 @@ bool Presenter::RefreshGuestOutput(
|
|||
return false;
|
||||
}
|
||||
guest_output_active_last_refresh_ = true;
|
||||
if (GuestFrameStatsEnabled()) {
|
||||
std::lock_guard<std::mutex> stats_lock(guest_frame_stats_mutex_);
|
||||
guest_frame_timestamps_[guest_frame_timestamp_next_] = std::chrono::steady_clock::now();
|
||||
guest_frame_timestamp_next_ = (guest_frame_timestamp_next_ + 1) % kGuestFrameTimestampCount;
|
||||
guest_frame_timestamp_count_ =
|
||||
std::min(guest_frame_timestamp_count_ + 1, kGuestFrameTimestampCount);
|
||||
++guest_frame_total_count_;
|
||||
guest_frame_last_wait_us_ =
|
||||
guest_frame_wait_accumulator_us_.exchange(0, std::memory_order_relaxed);
|
||||
}
|
||||
} else {
|
||||
// Request presenting a blank image if there was a true image previously,
|
||||
// but not now.
|
||||
|
|
@ -687,6 +697,93 @@ bool Presenter::RefreshGuestOutput(
|
|||
return is_active;
|
||||
}
|
||||
|
||||
void Presenter::SetGuestFrameStatsEnabled(bool enabled) {
|
||||
const bool was_enabled = GuestFrameStatsEnabled();
|
||||
if (enabled == was_enabled) {
|
||||
return;
|
||||
}
|
||||
if (!enabled) {
|
||||
guest_frame_stats_enabled_.store(false, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> stats_lock(guest_frame_stats_mutex_);
|
||||
guest_frame_timestamps_ = {};
|
||||
guest_frame_timestamp_next_ = 0;
|
||||
guest_frame_timestamp_count_ = 0;
|
||||
guest_frame_total_count_ = 0;
|
||||
guest_frame_last_wait_us_ = 0;
|
||||
guest_frame_wait_accumulator_us_.store(0, std::memory_order_relaxed);
|
||||
guest_frame_gpu_span_us_.store(0, std::memory_order_relaxed);
|
||||
guest_frame_gpu_draw_us_.store(0, std::memory_order_relaxed);
|
||||
guest_frame_gpu_resolve_us_.store(0, std::memory_order_relaxed);
|
||||
guest_frame_gpu_dump_us_.store(0, std::memory_order_relaxed);
|
||||
guest_frame_stats_enabled_.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
Presenter::GuestFrameStats Presenter::GetGuestFrameStats() const {
|
||||
if (!GuestFrameStatsEnabled()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Average over up to the last second of frames. Measuring the window from
|
||||
// the oldest in-window frame to the present time (rather than to the newest
|
||||
// frame) makes the reported FPS decay naturally when the guest stops
|
||||
// producing frames instead of freezing at the last value.
|
||||
constexpr std::chrono::steady_clock::duration kWindow = std::chrono::seconds(1);
|
||||
const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||
const std::chrono::steady_clock::time_point window_start = now - kWindow;
|
||||
|
||||
GuestFrameStats stats;
|
||||
std::lock_guard<std::mutex> stats_lock(guest_frame_stats_mutex_);
|
||||
stats.frame_count = guest_frame_total_count_;
|
||||
stats.wait_ms = double(guest_frame_last_wait_us_) / 1000.0;
|
||||
stats.gpu_ms = double(guest_frame_gpu_span_us_.load(std::memory_order_relaxed)) / 1000.0;
|
||||
stats.gpu_draw_ms = double(guest_frame_gpu_draw_us_.load(std::memory_order_relaxed)) / 1000.0;
|
||||
stats.gpu_resolve_ms =
|
||||
double(guest_frame_gpu_resolve_us_.load(std::memory_order_relaxed)) / 1000.0;
|
||||
stats.gpu_dump_ms = double(guest_frame_gpu_dump_us_.load(std::memory_order_relaxed)) / 1000.0;
|
||||
size_t frames_in_window = 0;
|
||||
std::chrono::steady_clock::time_point oldest_in_window;
|
||||
std::chrono::steady_clock::time_point newest_in_window;
|
||||
for (size_t i = 0; i < guest_frame_timestamp_count_; ++i) {
|
||||
// Walk from the newest sample backwards.
|
||||
size_t index =
|
||||
(guest_frame_timestamp_next_ + kGuestFrameTimestampCount - 1 - i) %
|
||||
kGuestFrameTimestampCount;
|
||||
const std::chrono::steady_clock::time_point timestamp = guest_frame_timestamps_[index];
|
||||
if (timestamp < window_start) {
|
||||
break;
|
||||
}
|
||||
if (!frames_in_window) {
|
||||
newest_in_window = timestamp;
|
||||
}
|
||||
oldest_in_window = timestamp;
|
||||
++frames_in_window;
|
||||
}
|
||||
if (!frames_in_window) {
|
||||
return stats;
|
||||
}
|
||||
const double span_to_now_seconds =
|
||||
std::chrono::duration_cast<std::chrono::duration<double>>(now - oldest_in_window).count();
|
||||
if (span_to_now_seconds > 0.0) {
|
||||
stats.fps = double(frames_in_window) / span_to_now_seconds;
|
||||
}
|
||||
if (frames_in_window >= 2) {
|
||||
const double span_between_frames_seconds =
|
||||
std::chrono::duration_cast<std::chrono::duration<double>>(newest_in_window -
|
||||
oldest_in_window)
|
||||
.count();
|
||||
if (span_between_frames_seconds > 0.0) {
|
||||
stats.frame_time_ms =
|
||||
span_between_frames_seconds * 1000.0 / double(frames_in_window - 1);
|
||||
}
|
||||
} else if (stats.fps > 0.0) {
|
||||
stats.frame_time_ms = 1000.0 / stats.fps;
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
void Presenter::SetGuestOutputPaintConfigFromUIThread(const GuestOutputPaintConfig& new_config) {
|
||||
// For simplicity, this may be called externally repeatedly.
|
||||
// Lock the mutex only when something has been modified, and also don't
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include <rex/platform.h>
|
||||
#include <rex/ui/overlay/console_overlay.h>
|
||||
#include <rex/ui/overlay/debug_overlay.h>
|
||||
#include <rex/ui/overlay/fps_overlay.h>
|
||||
#include <rex/ui/overlay/settings_overlay.h>
|
||||
#include <rex/graphics/graphics_system.h>
|
||||
#if REX_HAS_VULKAN
|
||||
|
|
@ -208,6 +209,9 @@ void StartForcedExitWatchdog(const char* reason) {
|
|||
REXCVAR_DEFINE_BOOL(advanced_settings_overlay_enabled, true, "UI/Advanced",
|
||||
"Enable the developer cvar browser on F4");
|
||||
|
||||
REXCVAR_DEFINE_BOOL(show_fps_counter, false, "UI", "Show the guest FPS counter overlay")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
// --- ReXApp ---
|
||||
|
||||
ReXApp::~ReXApp() = default;
|
||||
|
|
@ -548,6 +552,31 @@ bool ReXApp::SetupPresentation() {
|
|||
imgui_drawer_ = std::make_unique<rex::ui::ImGuiDrawer>(
|
||||
window_.get(), 64, [this](ImFontAtlas* atlas) { OnConfigureFonts(atlas); });
|
||||
imgui_drawer_->SetPresenterAndImmediateDrawer(presenter, immediate_drawer_.get());
|
||||
if (!frame_stats_provider_) {
|
||||
frame_stats_provider_ = [presenter]() {
|
||||
ui::Presenter::GuestFrameStats stats = presenter->GetGuestFrameStats();
|
||||
return ui::FrameStats{stats.frame_time_ms, stats.fps, stats.frame_count};
|
||||
};
|
||||
}
|
||||
auto update_guest_frame_stats_enabled = [this, presenter]() {
|
||||
presenter->SetGuestFrameStatsEnabled(fps_overlay_ != nullptr || debug_overlay_ != nullptr);
|
||||
};
|
||||
rex::ui::RegisterBind("bind_fps_counter", "F2", "Toggle FPS counter", [this, presenter] {
|
||||
if (fps_overlay_) {
|
||||
fps_overlay_.reset();
|
||||
} else {
|
||||
fps_overlay_ =
|
||||
std::make_unique<ui::FpsOverlayDialog>(imgui_drawer_.get(), presenter);
|
||||
}
|
||||
REXCVAR_SET(show_fps_counter, fps_overlay_ != nullptr);
|
||||
presenter->SetGuestFrameStatsEnabled(fps_overlay_ != nullptr || debug_overlay_ != nullptr);
|
||||
if (!config_path_.empty()) {
|
||||
rex::cvar::SaveConfigValues(config_path_, {"show_fps_counter"});
|
||||
}
|
||||
});
|
||||
if (REXCVAR_GET(show_fps_counter)) {
|
||||
fps_overlay_ = std::make_unique<ui::FpsOverlayDialog>(imgui_drawer_.get(), presenter);
|
||||
}
|
||||
rex::ui::RegisterBind("bind_debug_overlay", "F3", "Toggle debug overlay", [this] {
|
||||
if (debug_overlay_) {
|
||||
debug_overlay_.reset();
|
||||
|
|
@ -555,7 +584,13 @@ bool ReXApp::SetupPresentation() {
|
|||
debug_overlay_ = std::make_unique<ui::DebugOverlayDialog>(
|
||||
imgui_drawer_.get(), frame_stats_provider_, GetBuildStamp());
|
||||
}
|
||||
auto* graphics_system = static_cast<rex::graphics::GraphicsSystem*>(config_.graphics.get());
|
||||
if (graphics_system && graphics_system->presenter()) {
|
||||
graphics_system->presenter()->SetGuestFrameStatsEnabled(
|
||||
fps_overlay_ != nullptr || debug_overlay_ != nullptr);
|
||||
}
|
||||
});
|
||||
update_guest_frame_stats_enabled();
|
||||
rex::ui::RegisterBind("bind_console", "Backtick", "Toggle console overlay", [this] {
|
||||
if (console_overlay_) {
|
||||
console_overlay_.reset();
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ REXCVAR_DEFINE_BOOL(vulkan_require_fragment_stores_and_atomics, true, "UI/Vulkan
|
|||
"Deprecated and ignored for parity; fragmentStoresAndAtomics is always "
|
||||
"required for Vulkan GPU emulation")
|
||||
.lifecycle(rex::cvar::Lifecycle::kInitOnly);
|
||||
REXCVAR_DEFINE_BOOL(vulkan_robust_buffer_access, true, "UI/Vulkan",
|
||||
"Enable robustBufferAccess (bounds checking on all shader buffer accesses, "
|
||||
"including the per-vertex guest memory fetches). Disabling can improve GPU "
|
||||
"performance but out-of-bounds accesses from invalid guest fetch constants "
|
||||
"then return undefined data and may crash the GPU - experimental")
|
||||
.lifecycle(rex::cvar::Lifecycle::kInitOnly);
|
||||
REXCVAR_DEFINE_BOOL(vulkan_require_vertex_pipeline_stores_and_atomics, true, "UI/Vulkan",
|
||||
"Deprecated and ignored for parity; vertexPipelineStoresAndAtomics is always "
|
||||
"required for Vulkan GPU emulation")
|
||||
|
|
@ -218,6 +224,8 @@ std::unique_ptr<VulkanDevice> VulkanDevice::CreateIfSupported(
|
|||
XE_UI_VULKAN_LOCAL_PROMOTED_EXTENSION(KHR_sampler_mirror_clamp_to_edge, 1, 2)
|
||||
// #70. Must be enabled for VK_KHR_sampler_ycbcr_conversion.
|
||||
XE_UI_VULKAN_LOCAL_PROMOTED_EXTENSION(KHR_maintenance1, 1, 1)
|
||||
// #81. Used for guest draw constant buffer descriptors.
|
||||
XE_UI_VULKAN_STRUCT_EXTENSION(KHR_push_descriptor)
|
||||
// #141.
|
||||
XE_UI_VULKAN_STRUCT_EXTENSION(EXT_shader_stencil_export)
|
||||
// #148.
|
||||
|
|
@ -613,6 +621,8 @@ std::unique_ptr<VulkanDevice> VulkanDevice::CreateIfSupported(
|
|||
XE_UI_VULKAN_LIMIT(maxViewportDimensions[1])
|
||||
XE_UI_VULKAN_LIMIT(minUniformBufferOffsetAlignment)
|
||||
XE_UI_VULKAN_LIMIT(minStorageBufferOffsetAlignment)
|
||||
XE_UI_VULKAN_LIMIT(maxTexelBufferElements)
|
||||
XE_UI_VULKAN_LIMIT(minTexelBufferOffsetAlignment)
|
||||
XE_UI_VULKAN_LIMIT(maxFramebufferWidth)
|
||||
XE_UI_VULKAN_LIMIT(maxFramebufferHeight)
|
||||
XE_UI_VULKAN_ENUM_LIMIT(framebufferColorSampleCounts, SampleCountFlags)
|
||||
|
|
@ -632,6 +642,11 @@ std::unique_ptr<VulkanDevice> VulkanDevice::CreateIfSupported(
|
|||
|
||||
if (with_gpu_emulation) {
|
||||
XE_UI_VULKAN_FEATURE(robustBufferAccess)
|
||||
if (!REXCVAR_GET(vulkan_robust_buffer_access)) {
|
||||
enabled_features.robustBufferAccess = VK_FALSE;
|
||||
device->properties_.robustBufferAccess = VK_FALSE;
|
||||
REXLOG_INFO("* robustBufferAccess disabled by vulkan_robust_buffer_access=false");
|
||||
}
|
||||
XE_UI_VULKAN_FEATURE(fullDrawIndexUint32)
|
||||
XE_UI_VULKAN_FEATURE(independentBlend)
|
||||
XE_UI_VULKAN_FEATURE(geometryShader)
|
||||
|
|
@ -806,6 +821,9 @@ std::unique_ptr<VulkanDevice> VulkanDevice::CreateIfSupported(
|
|||
if (device->extensions_.ext_KHR_swapchain) {
|
||||
#include <rex/ui/vulkan/functions/device_khr_swapchain.inc>
|
||||
}
|
||||
if (device->extensions_.ext_KHR_push_descriptor) {
|
||||
#include <rex/ui/vulkan/functions/device_khr_push_descriptor.inc>
|
||||
}
|
||||
#undef XE_UI_VULKAN_FUNCTION_PROMOTED
|
||||
|
||||
#undef XE_UI_VULKAN_FUNCTION
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
#include <rex/ui/vulkan/instance.h>
|
||||
#include <rex/ui/vulkan/presenter.h>
|
||||
|
||||
REXCVAR_DEFINE_BOOL(vulkan_log_debug_messages, true, "UI/Vulkan", "Log Vulkan debug messages");
|
||||
REXCVAR_DEFINE_BOOL(vulkan_log_debug_messages, false, "UI/Vulkan", "Log Vulkan debug messages");
|
||||
|
||||
namespace rex {
|
||||
namespace ui {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue