From 397fc9284178b7626e77642c6ea24bfa69a620a8 Mon Sep 17 00:00:00 2001 From: goldislead <69987043+goldislead@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:39:41 -0700 Subject: [PATCH] [GPU] Rewrite hardware occlusion (ZPD) implementation --- src/xenia/gpu/command_processor.cc | 618 +++++++++++++++++- src/xenia/gpu/command_processor.h | 229 ++++++- .../gpu/d3d12/d3d12_command_processor.cc | 526 ++++++--------- src/xenia/gpu/d3d12/d3d12_command_processor.h | 110 ++-- src/xenia/gpu/d3d12/d3d12_zpd_query_pool.cc | 263 ++++++++ src/xenia/gpu/d3d12/d3d12_zpd_query_pool.h | 98 +++ src/xenia/gpu/d3d12/pipeline_cache.cc | 31 + src/xenia/gpu/d3d12/pipeline_cache.h | 5 + src/xenia/gpu/gpu_flags.cc | 32 +- src/xenia/gpu/gpu_flags.h | 8 +- .../gpu/pm4_command_processor_implement.h | 182 +++++- .../gpu/vulkan/vulkan_command_processor.cc | 560 +++++++--------- .../gpu/vulkan/vulkan_command_processor.h | 94 +-- src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc | 22 + src/xenia/gpu/vulkan/vulkan_pipeline_cache.h | 4 + src/xenia/gpu/vulkan/vulkan_zpd_query_pool.cc | 397 +++++++++++ src/xenia/gpu/vulkan/vulkan_zpd_query_pool.h | 108 +++ src/xenia/gpu/xenos_report_controller.cc | 213 ++++++ src/xenia/gpu/xenos_report_controller.h | 132 ++++ src/xenia/gpu/xenos_zpd_report.h | 128 ++++ src/xenia/ui/config_helpers.h | 1 + src/xenia/ui/imgui_performance_dialog.cc | 86 ++- src/xenia/ui/imgui_performance_dialog.h | 5 +- .../device_1_2_ext_host_query_reset.inc | 3 + src/xenia/ui/vulkan/vulkan_device.cc | 22 + src/xenia/ui/vulkan/vulkan_device.h | 7 + 26 files changed, 3033 insertions(+), 851 deletions(-) create mode 100644 src/xenia/gpu/d3d12/d3d12_zpd_query_pool.cc create mode 100644 src/xenia/gpu/d3d12/d3d12_zpd_query_pool.h create mode 100644 src/xenia/gpu/vulkan/vulkan_zpd_query_pool.cc create mode 100644 src/xenia/gpu/vulkan/vulkan_zpd_query_pool.h create mode 100644 src/xenia/gpu/xenos_report_controller.cc create mode 100644 src/xenia/gpu/xenos_report_controller.h create mode 100644 src/xenia/gpu/xenos_zpd_report.h create mode 100644 src/xenia/ui/vulkan/functions/device_1_2_ext_host_query_reset.inc diff --git a/src/xenia/gpu/command_processor.cc b/src/xenia/gpu/command_processor.cc index 873dbcfd2..27d2abdda 100644 --- a/src/xenia/gpu/command_processor.cc +++ b/src/xenia/gpu/command_processor.cc @@ -22,6 +22,7 @@ #include "xenia/gpu/packet_disassembler.h" #include "xenia/gpu/sampler_info.h" #include "xenia/gpu/texture_info.h" +#include "xenia/gpu/xenos_zpd_report.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/user_module.h" #if !defined(NDEBUG) @@ -52,6 +53,19 @@ DEFINE_bool(clear_memory_page_state, true, "(Disable for minor performance boost, but may break rendering)", "GPU"); +DEFINE_string( + occlusion_query, "fake", + "Controls hardware occlusion query behavior for EVENT_WRITE_ZPD.\n" + "Used for effects like lens flares, object culling, and auto-exposure.\n" + "ROV render path currently supports fake mode only.\n" + " fake: Write a fake result without asking the GPU. Safe for most games,\n" + " though some effects may look slightly wrong. (default)\n" + " fast: Ask the GPU but don't wait for the answer. Writes a cached\n" + " result immediately and updates it when the GPU catches up.\n" + " strict: Ask the GPU and wait for the real result before continuing.\n" + " Most accurate, but may be somewhat less performant.", + "GPU"); + DEFINE_string( readback_resolve, "fast", "Controls CPU readback of render-to-texture resolve results.\n" @@ -127,6 +141,22 @@ static void SetReadbackResolveCvar(const std::string& mode) { OVERRIDE_string(readback_resolve, mode); } +static ZPDMode ParseZPDMode() { + const std::string& mode = cvars::occlusion_query; + if (mode == "strict") { + return ZPDMode::kStrict; + } else if (mode == "fast") { + return ZPDMode::kFast; + } else { + // Default to "fake" for any unrecognized value. + return ZPDMode::kFake; + } +} + +static void SetZPDModeCvar(const std::string& mode) { + OVERRIDE_string(occlusion_query, mode); +} + using namespace xe::gpu::xenos; CommandProcessor::CommandProcessor(GraphicsSystem* graphics_system, @@ -143,6 +173,8 @@ CommandProcessor::CommandProcessor(GraphicsSystem* graphics_system, assert_not_null(write_ptr_index_event_); // Parse and cache readback resolve mode once cached_readback_resolve_mode_ = ParseReadbackResolveMode(); + // Parse and cache ZPD mode once. + cached_zpd_mode_ = ParseZPDMode(); } CommandProcessor::~CommandProcessor() = default; @@ -332,6 +364,42 @@ void CommandProcessor::SetReadbackResolveMode(ReadbackResolveMode mode) { } } +void CommandProcessor::SetZPDMode(ZPDMode mode) { + if (cached_zpd_mode_ == mode) { + return; + } + cached_zpd_mode_ = mode; + const char* mode_str = "fake"; + switch (mode) { + case ZPDMode::kFast: + mode_str = "fast"; + break; + case ZPDMode::kStrict: + mode_str = "strict"; + break; + default: + break; + } + SetZPDModeCvar(mode_str); + + // Save to per-game config if a title is loaded. + uint32_t title_id = kernel_state_ ? kernel_state_->title_id() : 0; + if (title_id != 0) { + toml::table config_table = config::LoadGameConfig(title_id); + + if (!config_table.contains("GPU")) { + config_table.insert("GPU", toml::table{}); + } + + auto* gpu_table = config_table["GPU"].as_table(); + if (gpu_table) { + gpu_table->insert_or_assign("occlusion_query", mode_str); + } + + config::SaveGameConfig(title_id, config_table); + } +} + void CommandProcessor::SetDesiredSwapPostEffect( SwapPostEffect swap_post_effect) { if (swap_post_effect_desired_ == swap_post_effect) { @@ -505,9 +573,17 @@ bool CommandProcessor::Restore(ByteStream* stream) { return true; } -bool CommandProcessor::SetupContext() { return true; } +bool CommandProcessor::SetupContext() { + ResetZPDState(); + zpd_report_controller_ = std::make_unique( + &CommandProcessor::ZPDReportCallback, this); + return true; +} -void CommandProcessor::ShutdownContext() {} +void CommandProcessor::ShutdownContext() { + ResetZPDState(); + zpd_report_controller_.reset(); +} void CommandProcessor::InitializeRingBuffer(uint32_t ptr, uint32_t size_log2) { read_ptr_index_ = 0; @@ -916,7 +992,12 @@ void CommandProcessor::MakeCoherent() { regs_volatile[XE_GPU_REG_COHER_STATUS_HOST] = 0; } -void CommandProcessor::PrepareForWait() { trace_writer_.Flush(); } +void CommandProcessor::PrepareForWait() { + trace_writer_.Flush(); + // Give strict ZPD a chance to retire an pending report before the guest's + // loop polls again. + PumpPendingRetire(); +} void CommandProcessor::ReturnFromWait() {} @@ -931,6 +1012,537 @@ void CommandProcessor::InitializeTrace() { trace_writer_.WriteGammaRamp(gamma_ramp_256_entry_table(), gamma_ramp_pwl_rgb(), gamma_ramp_rw_component_); } + +bool CommandProcessor::BeginZPDReport(uint32_t report_address) { + if (GetZPDMode() == ZPDMode::kFake || zpd_batch_fake_) { + return false; + } + + // Track any delta to carry forward if the same slot is immediately reused. + uint32_t carried_cached_delta = 0; + uint32_t carried_from_slot_base = 0; + + if (zpd_active_segment_.logical_active) { + // New BEGIN while a prior report is open. Hardware has one register for + // the query address, so a new BEGIN implicitly ends the prior one. + if (zpd_active_segment_.end_record) { + EndZPDReport(zpd_active_segment_.end_record, true); + } else { + if (cvars::occlusion_query_log) { + XELOGI( + "ZPD: BeginZPDReport forcing close without end record " + "handle={}", + zpd_active_segment_.report_handle); + } + + carried_from_slot_base = zpd_active_segment_.slot_base; + + auto dying_report = + logical_zpd_reports_.find(zpd_active_segment_.report_handle); + // Carry prior delta forward so the slot doesn't briefly look occluded. + if (dying_report != logical_zpd_reports_.end()) { + carried_cached_delta = dying_report->second.cached_delta; + } + + if (zpd_active_segment_.segment_active) { + if (DiscardZPDQuery(zpd_active_segment_.query_index, + zpd_active_segment_.query_generation)) { + zpd_stats_.segments_ended++; + } else { + zpd_stats_.failed++; + } + } + logical_zpd_reports_.erase(zpd_active_segment_.report_handle); + zpd_active_segment_ = {}; + } + } + + uint32_t slot_base = XenosZPDReport::GetSlotBase(report_address); + uint32_t begin_record = XenosZPDReport::GetBeginRecordBase(slot_base); + uint32_t end_record = XenosZPDReport::GetEndRecordBase(slot_base); + XenosReportController::BeginReportResult begin_report_result = + zpd_report_controller_->BeginReport(report_address); + XenosReportController::ReportHandle report_handle = + begin_report_result.report_handle; + if (report_handle == XenosReportController::kInvalidReportHandle) { + return false; + } + + ZPDReport& logical = logical_zpd_reports_[report_handle]; + logical.begin_record = begin_record; + logical.end_record = end_record; + logical.begin_value = begin_report_result.begin_value; + logical.accumulated_samples = 0; + logical.last_segment_end_submission = 0; + logical.pending_segments = 0; + logical.cached_delta = 0; + logical.ended = false; + + if (slot_base == carried_from_slot_base && carried_cached_delta != 0) { + logical.cached_delta = carried_cached_delta; + } + + zpd_active_segment_.report_handle = report_handle; + zpd_active_segment_.slot_base = slot_base; + zpd_active_segment_.begin_record = begin_record; + zpd_active_segment_.end_record = end_record; + zpd_active_segment_.query_index = UINT32_MAX; + zpd_active_segment_.query_generation = 0; + zpd_active_segment_.segment_active = false; + // Opens lazily. OpenQuerySegment will open it at the next valid opportunity. + zpd_active_segment_.segment_pending_begin = true; + zpd_active_segment_.logical_active = true; + + zpd_stats_.logical_begun++; + OpenQuerySegment(true); + return true; +} + +// Guest END closes the logical lifetime, but the final value may still depend +// on in flight query segments. +bool CommandProcessor::EndZPDReport(uint32_t report_address, + bool guest_forced_end) { + if (GetZPDMode() == ZPDMode::kFake || zpd_batch_fake_) { + return false; + } + + XenosReportController::ReportHandle report_handle = + zpd_active_segment_.report_handle; + uint32_t stored_end_record = zpd_active_segment_.end_record; + uint32_t report_record_base = XenosZPDReport::GetRecordBase(report_address); + if (!report_record_base) { + report_record_base = stored_end_record; + } + + if (zpd_active_segment_.segment_active) { + CloseQuerySegment(); + } + + zpd_active_segment_.segment_pending_begin = false; + + if (!report_record_base) { + logical_zpd_reports_.erase(report_handle); + if (cvars::occlusion_query_log) { + XELOGI( + "ZPD: EndZPDReport dropping handle={} with unknown record " + "base forced={}", + report_handle, guest_forced_end); + } + zpd_active_segment_ = {}; + return false; + } + + bool resolved_immediately = false; + uint32_t begin_record = 0; + uint32_t begin_value = 0; + uint32_t final_value = 0; + uint32_t cached_delta = 1; + + auto it = logical_zpd_reports_.find(report_handle); + if (it == logical_zpd_reports_.end()) { + zpd_active_segment_ = {}; + return false; + } + + ZPDReport& logical = it->second; + logical.ended = true; + logical.end_record = report_record_base; + begin_record = logical.begin_record; + begin_value = logical.begin_value; + + if (logical.pending_segments == 0) { + resolved_immediately = true; + final_value = NormalizeSampleCount(logical.accumulated_samples); + + cached_delta = (final_value == 0 && logical.cached_delta != 0) + ? logical.cached_delta + : final_value; + logical.cached_delta = cached_delta; + fast_zpd_report_cached_values_[report_record_base] = cached_delta; + final_value = cached_delta; + } else { + if (logical.cached_delta != 0) { + cached_delta = logical.cached_delta; + } + auto cache_it = fast_zpd_report_cached_values_.find(report_record_base); + if (cache_it != fast_zpd_report_cached_values_.end()) { + cached_delta = cache_it->second; + } + } + + zpd_report_controller_->QueueReportWrite(report_record_base, report_handle); + if (resolved_immediately) { + zpd_report_controller_->SetReportResolved(report_handle, final_value); + zpd_report_controller_->RetireReports(); + } + + bool has_cross_slot_end = + stored_end_record && stored_end_record != report_record_base; + if (has_cross_slot_end) { + WriteZPDReport(0, stored_end_record, 0, begin_value, false); + } + + if (GetZPDMode() == ZPDMode::kFast) { + bool write_begin = begin_record && report_record_base && + begin_record != report_record_base; + uint32_t speculative = + (write_begin && cached_delta == 0) ? 1 : cached_delta; + WriteZPDReport(begin_record, report_record_base, begin_value, speculative, + write_begin); + } else if (!resolved_immediately) { + PumpQueryResolves(); + + if (zpd_report_controller_->HasQueuedWriteForAddress(report_record_base) && + zpd_pending_retire_handle_ != report_handle) { + zpd_pending_retire_handle_ = report_handle; + zpd_pending_retire_stalls_ = 0; + } + } + + zpd_stats_.logical_ended++; + zpd_active_segment_ = {}; + return true; +} + +void CommandProcessor::OpenQuerySegment(bool can_close_submission) { + if (GetZPDMode() == ZPDMode::kFake || zpd_batch_fake_ || + !zpd_active_segment_.logical_active || + !zpd_active_segment_.segment_pending_begin || !CanOpenZPDQuery()) { + return; + } + + EnsureZPDQueryResources(); + + if (!IsZPDQueryPoolReady()) { + zpd_stats_.failed++; + return; + } + + // Frees any slots from completed submissions before asking for new ones. + PumpQueryResolves(); + + uint32_t query_index = UINT32_MAX; + uint32_t query_generation = 0; + QueryOpenResult open_result = + OpenZPDQuery(query_index, query_generation, can_close_submission); + switch (open_result) { + case QueryOpenResult::kOpened: + break; + case QueryOpenResult::kDeferred: + return; + case QueryOpenResult::kPoolExhausted: { + zpd_stats_.pool_exhausted++; + if (GetZPDMode() == ZPDMode::kFast) { + // Fast mode favors forward progress over accuracy. Keep a minimal + // accumulated value instead of waiting for a slot to become available. + auto it = logical_zpd_reports_.find(zpd_active_segment_.report_handle); + if (it != logical_zpd_reports_.end()) { + it->second.accumulated_samples = + std::max(it->second.accumulated_samples, uint64_t{1}); + } + zpd_active_segment_.segment_pending_begin = false; + return; + } + zpd_stats_.failed++; + return; + } + case QueryOpenResult::kFailed: + default: + zpd_stats_.failed++; + return; + } + + zpd_active_segment_.query_index = query_index; + zpd_active_segment_.query_generation = query_generation; + zpd_active_segment_.segment_active = true; + zpd_active_segment_.segment_pending_begin = false; + zpd_stats_.segments_begun++; +} + +// Closes the active host segment without ending the logical report. +// BeginQuery/EndQuery can't cross D3D12 command list or Vulkan render pass +// boundaries. The result accumulates across all pieces. +void CommandProcessor::CloseQuerySegment() { + if (GetZPDMode() == ZPDMode::kFake || !zpd_active_segment_.segment_active) { + return; + } + + uint64_t submission = 0; + uint32_t query_index = zpd_active_segment_.query_index; + uint32_t query_generation = zpd_active_segment_.query_generation; + if (!CloseZPDQuery(query_index, query_generation, submission)) { + zpd_active_segment_.segment_active = false; + zpd_active_segment_.segment_pending_begin = + zpd_active_segment_.logical_active && !zpd_batch_fake_; + zpd_active_segment_.query_index = UINT32_MAX; + zpd_active_segment_.query_generation = 0; + zpd_stats_.failed++; + return; + } + + PendingQueryResolve resolve; + resolve.submission = submission; + resolve.query_index = query_index; + resolve.query_generation = query_generation; + resolve.report_handle = zpd_active_segment_.report_handle; + + zpd_resolves_in_flight_.push_back(resolve); + + auto it = logical_zpd_reports_.find(resolve.report_handle); + if (it != logical_zpd_reports_.end()) { + it->second.pending_segments++; + it->second.last_segment_end_submission = resolve.submission; + } + + zpd_active_segment_.segment_active = false; + + if (zpd_batch_fake_) { + zpd_active_segment_.logical_active = false; + } + + zpd_active_segment_.segment_pending_begin = + zpd_active_segment_.logical_active && !zpd_batch_fake_; + zpd_active_segment_.query_index = UINT32_MAX; + zpd_active_segment_.query_generation = 0; + zpd_stats_.segments_ended++; +} + +void CommandProcessor::DrainQueryResolves(uint64_t completed_submission) { + if (GetZPDMode() == ZPDMode::kFake) { + return; + } + + ZPDSubmissionBridge* submission_bridge = GetZPDSubmissionBridge(); + std::vector ready_resolves; + bool any_resolved = false; + + if (submission_bridge != nullptr) { + submission_bridge->PrepareReadback(completed_submission); + } + + while (!zpd_resolves_in_flight_.empty()) { + PendingQueryResolve resolve = zpd_resolves_in_flight_.front(); + if (resolve.submission > completed_submission) { + break; + } + zpd_resolves_in_flight_.pop_front(); + ready_resolves.push_back(resolve); + } + + for (const PendingQueryResolve& resolve : ready_resolves) { + uint64_t raw_samples = GetZPDQueryResult(resolve.query_index); + bool is_valid = + IsZPDQueryResultValid(resolve.query_index, resolve.query_generation); + + if (is_valid) { + ReleaseZPDQuery(resolve.query_index, resolve.query_generation); + + auto it = logical_zpd_reports_.find(resolve.report_handle); + if (it != logical_zpd_reports_.end()) { + ZPDReport& logical = it->second; + + if (logical.pending_segments) { + logical.pending_segments--; + } + + if (zpd_batch_fake_) { + if (logical.pending_segments == 0) { + logical_zpd_reports_.erase(it); + } + } else { + logical.accumulated_samples += raw_samples; + if (logical.ended && logical.pending_segments == 0) { + uint32_t final_value = + NormalizeSampleCount(logical.accumulated_samples); + + logical.cached_delta = final_value; + if (logical.end_record) { + fast_zpd_report_cached_values_[logical.end_record] = final_value; + } + zpd_report_controller_->SetReportResolved(resolve.report_handle, + final_value); + any_resolved = true; + } + } + } + } + } + + if (any_resolved) { + zpd_report_controller_->RetireReports(); + } +} + +void CommandProcessor::PumpQueryResolves() { + if (GetZPDMode() == ZPDMode::kFake) { + return; + } + + ZPDSubmissionBridge* submission_bridge = GetZPDSubmissionBridge(); + if (submission_bridge == nullptr) { + return; + } + + ZPDSubmissionState submission_state = submission_bridge->GetState(); + if (submission_state.completed_submission == 0) { + return; + } + + DrainQueryResolves(submission_state.completed_submission); +} + +bool CommandProcessor::AwaitQueryResolve( + XenosReportController::ReportHandle report_handle) { + if (GetZPDMode() == ZPDMode::kFake) { + return false; + } + // Stop stalling here if the batched fake ripcord has been pulled. + if (zpd_batch_fake_) { + return true; + } + + ZPDSubmissionBridge* submission_bridge = GetZPDSubmissionBridge(); + if (submission_bridge == nullptr) { + return false; + } + + auto it = logical_zpd_reports_.find(report_handle); + if (it == logical_zpd_reports_.end()) { + return false; + } + + uint64_t wait_for_submission = it->second.last_segment_end_submission; + if (wait_for_submission == 0) { + if (it->second.pending_segments == 0 && it->second.ended) { + zpd_report_controller_->RetireReports(); + return true; + } + return false; + } + + ZPDSubmissionState submission_state = submission_bridge->GetState(); + + uint64_t current_submission = submission_state.current_submission; + if (current_submission != 0 && wait_for_submission >= current_submission) { + if (submission_bridge->EnsureProgress()) { + submission_state = submission_bridge->GetState(); + current_submission = submission_state.current_submission; + } + } + + uint64_t completed_before = submission_state.completed_submission; + if (wait_for_submission > completed_before && + wait_for_submission < current_submission) { + submission_bridge->AwaitSubmission(wait_for_submission); + uint64_t completed_after = + submission_bridge->GetState().completed_submission; + if (completed_after > completed_before) { + DrainQueryResolves(completed_after); + } + } + + it = logical_zpd_reports_.find(report_handle); + if (it == logical_zpd_reports_.end()) { + return true; + } + return it->second.pending_segments == 0 && it->second.ended; +} + +void CommandProcessor::PumpPendingRetire() { + if (zpd_batch_fake_) { + zpd_pending_retire_handle_ = XenosReportController::kInvalidReportHandle; + zpd_pending_retire_stalls_ = 0; + return; + } + + XenosReportController::ReportHandle handle_to_await = + zpd_pending_retire_handle_; + + if (AwaitQueryResolve(handle_to_await)) { + zpd_pending_retire_handle_ = XenosReportController::kInvalidReportHandle; + zpd_pending_retire_stalls_ = 0; + return; + } + + auto logical_report = logical_zpd_reports_.find(handle_to_await); + if (logical_report == logical_zpd_reports_.end()) { + // If the report is already gone it retired through another path. + // Clear so we don't spin on a handle that no longer exists. + zpd_pending_retire_handle_ = XenosReportController::kInvalidReportHandle; + zpd_pending_retire_stalls_ = 0; + return; + } + + // Give up after kStrictZPDRetireMaxStalls. It's better to abandon the + // sentinel than risk hanging PM4 forever. + if (++zpd_pending_retire_stalls_ >= kStrictZPDRetireMaxStalls) { + if (cvars::occlusion_query_log) { + XELOGI( + "ZPD: PumpPendingRetire stall limit reached " + "handle={}, abandoning", + handle_to_await); + } + logical_zpd_reports_.erase(logical_report); + zpd_pending_retire_handle_ = XenosReportController::kInvalidReportHandle; + zpd_pending_retire_stalls_ = 0; + } +} + +void CommandProcessor::WriteZPDReport(uint32_t begin_record, + uint32_t end_record, uint32_t begin_value, + uint32_t delta_value, + bool write_begin_record) { + xenos::xe_gpu_depth_sample_counts* begin = + begin_record + ? memory_->TranslatePhysical( + begin_record) + : nullptr; + xenos::xe_gpu_depth_sample_counts* end = + memory_->TranslatePhysical( + end_record); + + XenosZPDReport::WriteReportDelta(begin, end, begin_value, delta_value, + write_begin_record); +} + +void CommandProcessor::ZPDReportCallback( + XenosReportController::ReportHandle report_handle, uint32_t slot_base, + uint32_t begin_value, uint32_t delta_value, void* callback_context) { + CommandProcessor* processor = + reinterpret_cast(callback_context); + + // The controller passes slot_base. The END record lives at slot_base + // so slot_base is already the correct target address. GetRecordBase is a + // no-op here but keeps the call explicit. + uint32_t end_record = XenosZPDReport::GetEndRecordBase(slot_base); + uint32_t begin_record = XenosZPDReport::GetBeginRecordBase(slot_base); + auto existing_report = processor->logical_zpd_reports_.find(report_handle); + if (existing_report != processor->logical_zpd_reports_.end()) { + begin_record = existing_report->second.begin_record; + processor->logical_zpd_reports_.erase(existing_report); + } else if (cvars::occlusion_query_log) { + XELOGI( + "ZPD: ZPDReportCallback missing logical report " + "handle={} end_record=0x{:08X}", + report_handle, end_record); + } + + processor->WriteZPDReport(begin_record, end_record, begin_value, delta_value, + begin_record != 0); +} + +uint32_t CommandProcessor::NormalizeSampleCount(uint64_t samples) const { + if (samples == 0) { + return 0; + } + + uint64_t scale = zpd_draw_resolution_scale_x_ * zpd_draw_resolution_scale_y_; + // Round, don't truncate. 1 guest sample at 2x = 4 host samples, need >= 1. + uint64_t normalized = scale <= 1 ? samples : (samples + (scale >> 1)) / scale; + + return static_cast(std::min(normalized, UINT32_MAX)); +} + #define COMMAND_PROCESSOR CommandProcessor #include "pm4_command_processor_implement.h" } // namespace gpu diff --git a/src/xenia/gpu/command_processor.h b/src/xenia/gpu/command_processor.h index 7e04bbb79..4afff7259 100644 --- a/src/xenia/gpu/command_processor.h +++ b/src/xenia/gpu/command_processor.h @@ -12,17 +12,22 @@ #include #include +#include #include #include #include #include #include +#include #include +#include "xenia/base/math.h" #include "xenia/base/ring_buffer.h" #include "xenia/gpu/register_file.h" #include "xenia/gpu/trace_writer.h" #include "xenia/gpu/xenos.h" +#include "xenia/gpu/xenos_report_controller.h" +#include "xenia/gpu/xenos_zpd_report.h" #include "xenia/kernel/xthread.h" #include "xenia/memory.h" #include "xenia/ui/presenter.h" @@ -46,13 +51,22 @@ enum class ReadbackResolveMode { kFull // Immediate sync with GPU stall (full) }; +// Occlusion queries - ZPD report mode. +enum class ZPDMode { + kFake, // Fake sample counts, no real GPU queries (fake) + kFast, // Real queries with speculative cached writes (fast) + kStrict, // Real queries, waits before writeback. May hang. (strict) +}; + void SaveGPUSetting(GPUSetting setting, uint64_t value); bool GetGPUSetting(GPUSetting setting); -// Occlusion query pool size for both D3D12 and Vulkan backends. -// Queries complete synchronously with GPU stalls. -// 512 slots = 4KB of readback buffer memory. -constexpr uint32_t kMaxOcclusionQueries = 512; +// Shared pool capacity for D3D12 and Vulkan. +constexpr uint32_t kZPDQueryPoolCapacity = 8192; + +// Backstop for strict mode. Abandon any pending retires after this many polls +// so EVENT_WRITE_ZPD doesn't keep spinning on an unresolved report. +constexpr uint32_t kStrictZPDRetireMaxStalls = 16; class GraphicsSystem; class Shader; @@ -123,6 +137,11 @@ class CommandProcessor { // Set readback resolve mode (updates both cvar and cached value) void SetReadbackResolveMode(ReadbackResolveMode mode); + // Get cached ZPD mode (avoids string parsing every frame). + ZPDMode GetZPDMode() const { return cached_zpd_mode_; } + // Set ZPD mode (updates both cvar and cached value). + void SetZPDMode(ZPDMode mode); + // "Desired" is for the external thread managing the post-processing effect. SwapPostEffect GetDesiredSwapPostEffect() const { return swap_post_effect_desired_; @@ -281,6 +300,167 @@ class CommandProcessor { virtual void OnPrimaryBufferEnd() {} + // TODO(boma): Add tracking for VIZ & EXT queries. + enum class QueryOpenResult { + kOpened, + kDeferred, + kPoolExhausted, + kFailed, + }; + + // One active guest report slot. May span multiple host query segments split + // across submissions or render passes, final value is the normalized sum. + struct ZPDReport { + // Raw host count across all segments, normalized at retirement. + uint64_t accumulated_samples = 0; + uint64_t last_segment_end_submission = 0; + uint32_t begin_record = 0; + uint32_t end_record = 0; + // Snapshotted from controller at BEGIN. + uint32_t begin_value = 0; + uint32_t pending_segments = 0; + // Last known delta. Carried forward on forced close so slot doesn't + // briefly look fully occluded. + uint32_t cached_delta = 0; + bool ended = false; + }; + + // TODO(boma): Replace with a map keyed by slot_base for concurrent slots. + struct ActiveZPDSegment { + XenosReportController::ReportHandle report_handle = + XenosReportController::kInvalidReportHandle; + uint32_t slot_base = 0; + uint32_t begin_record = 0; + uint32_t end_record = 0; + uint32_t query_index = UINT32_MAX; + uint32_t query_generation = 0; + bool segment_active = false; + bool segment_pending_begin = false; + bool logical_active = false; + }; + + struct PendingQueryResolve { + uint64_t submission = 0; + uint32_t query_index = UINT32_MAX; + uint32_t query_generation = 0; + XenosReportController::ReportHandle report_handle = + XenosReportController::kInvalidReportHandle; + }; + + // Logged by the backend every 100 frames if ZPD logging cvar is true. + struct ZPDStats { + uint64_t logical_begun = 0; + uint64_t logical_ended = 0; + uint64_t segments_begun = 0; + uint64_t segments_ended = 0; + uint64_t pool_exhausted = 0; + uint64_t failed = 0; + uint64_t last_log_frame = 0; + + void Reset(uint64_t current_frame) { + *this = {}; + last_log_frame = current_frame; + } + }; + + virtual void EnsureZPDQueryResources() {} + virtual void ShutdownZPDQueryResources() {} + + virtual bool IsZPDQueryPoolReady() const { return false; } + virtual bool CanOpenZPDQuery() const { return true; } + + virtual QueryOpenResult OpenZPDQuery(uint32_t& out_host_index, + uint32_t& out_host_generation, + bool can_close_submission) { + return QueryOpenResult::kFailed; + } + + virtual bool CloseZPDQuery(uint32_t host_index, uint32_t host_generation, + uint64_t& out_submission) { + return false; + } + + // Closes a query without queuing a resolve and returns the slot to the pool. + // Used for forced closes when a new BEGIN collides with an active segment. + virtual bool DiscardZPDQuery(uint32_t host_index, uint32_t host_generation) { + return false; + } + + virtual uint64_t GetZPDQueryResult(uint32_t host_index) { return 0; } + virtual void ReleaseZPDQuery(uint32_t host_index, uint32_t host_generation) {} + + virtual bool IsZPDQueryResultValid(uint32_t host_index, + uint32_t host_generation) const { + return true; + } + + struct ZPDSubmissionState { + uint64_t current_submission = 0; + uint64_t completed_submission = 0; + }; + + class ZPDSubmissionBridge { + public: + virtual ~ZPDSubmissionBridge() = default; + virtual ZPDSubmissionState GetState() const = 0; + virtual void PrepareReadback(uint64_t completed_submission) {} + virtual bool EnsureProgress() = 0; + virtual void AwaitSubmission(uint64_t submission) = 0; + }; + + virtual ZPDSubmissionBridge* GetZPDSubmissionBridge() { return nullptr; } + + bool BeginZPDReport(uint32_t report_address); + bool EndZPDReport(uint32_t report_address, bool guest_forced_end); + // Opens a new host query segment when CanOpenZPDQuery is true. + void OpenQuerySegment(bool can_close_submission); + // Closes the current segment at a submission or render pass boundary. + // The logical report stays open and a new segment will open at the next + // opportunity. + void CloseQuerySegment(); + // Reads results from completed submissions, accumulates the raw sample + // deltas into logical reports, retires any that are done. + void DrainQueryResolves(uint64_t completed_submission); + + // Non-blocking retirement pump. Returns if nothing is ready. + void PumpQueryResolves(); + + // Blocks on the fence for report_handle's last segment, then pumps. + // Can't be called from EVENT_WRITE_ZPD or the retirement callback. + bool AwaitQueryResolve(XenosReportController::ReportHandle report_handle); + + // Writes guest report with begin_value read from guest memory. + // Orphan END path only when no controller snapshot is available. + void WriteZPDReport(uint32_t begin_record, uint32_t end_record, + uint32_t begin_value, uint32_t delta_value, + bool write_begin_record); + + // Called from PrepareForWait so strict mode can retire before guest loops + // again. Gives up after kStrictZPDRetireMaxStalls. + void PumpPendingRetire(); + + // Divides host count by draw resolution scale. + uint32_t NormalizeSampleCount(uint64_t samples) const; + + static void ZPDReportCallback( + XenosReportController::ReportHandle report_handle, uint32_t slot_base, + uint32_t begin_value, uint32_t delta_value, void* callback_context); + + void ResetZPDState() { + zpd_active_segment_ = {}; + logical_zpd_reports_.clear(); + fast_zpd_report_cached_values_.clear(); + zpd_batch_fake_ = false; + zpd_batch_fake_count_ = 0; + zpd_batch_page_ = 0; + zpd_batch_last_record_ = 0; + zpd_batch_run_ = 0; + fake_zpd_sample_count_ = 0; + zpd_pending_retire_handle_ = XenosReportController::kInvalidReportHandle; + zpd_pending_retire_stalls_ = 0; + zpd_resolves_in_flight_.clear(); + } + #include "pm4_command_processor_declare.h" virtual Shader* LoadShader(xenos::ShaderType shader_type, @@ -296,7 +476,6 @@ class CommandProcessor { return false; } virtual bool IssueCopy() { return false; } - virtual bool SupportsGuestOcclusionQueries() const { return false; } // Debug marker stubs for base class (overridden by D3D12/Vulkan backends). bool debug_markers_enabled() const { return false; } @@ -317,6 +496,43 @@ class CommandProcessor { GraphicsSystem* graphics_system_ = nullptr; RegisterFile* XE_RESTRICT register_file_ = nullptr; + std::unique_ptr zpd_report_controller_; + std::unordered_map + logical_zpd_reports_; + ActiveZPDSegment zpd_active_segment_{}; + std::deque zpd_resolves_in_flight_; + + // Cached delta per END. + // Fast mode uses this for speculative writeback and orphaned END replay. + std::unordered_map fast_zpd_report_cached_values_; + + // PM4 batched query ripcord. If the guest walks the page of pending 0x20 + // checkpoints then permanenetly switch to cumulative fake mode. + bool zpd_batch_fake_ = false; + uint32_t zpd_batch_fake_count_ = 0; + uint32_t zpd_batch_page_ = 0; + uint32_t zpd_batch_last_record_ = 0; + uint32_t zpd_batch_run_ = 0; + + // Strict mode defers guest completion until the queued END has retired. + XenosReportController::ReportHandle zpd_pending_retire_handle_ = + XenosReportController::kInvalidReportHandle; + uint32_t zpd_pending_retire_stalls_ = 0; + + // Set by the backend when resolution scale changes. + uint32_t zpd_draw_resolution_scale_x_ = 1; + uint32_t zpd_draw_resolution_scale_y_ = 1; + + uint32_t zpd_draw_resolution_scale_x() const { + return zpd_draw_resolution_scale_x_; + } + uint32_t zpd_draw_resolution_scale_y() const { + return zpd_draw_resolution_scale_y_; + } + + uint32_t fake_zpd_sample_count_ = 0; + ZPDStats zpd_stats_; + TraceWriter trace_writer_; enum class TraceState { kDisabled, @@ -364,6 +580,9 @@ class CommandProcessor { ReadbackResolveMode cached_readback_resolve_mode_ = ReadbackResolveMode::kFast; + // Cached ZPD occlusion query mode (defaults to fake) + ZPDMode cached_zpd_mode_ = ZPDMode::kFake; + // For host frame rate limiting at IssueSwap uint64_t last_swap_time_ = 0; diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.cc b/src/xenia/gpu/d3d12/d3d12_command_processor.cc index f5de017bb..60d511846 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.cc +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.cc @@ -22,11 +22,13 @@ #include "xenia/emulator.h" #include "xenia/gpu/d3d12/d3d12_graphics_system.h" #include "xenia/gpu/d3d12/d3d12_shader.h" +#include "xenia/gpu/d3d12/d3d12_zpd_query_pool.h" #include "xenia/gpu/draw_util.h" #include "xenia/gpu/gpu_flags.h" #include "xenia/gpu/packet_disassembler.h" #include "xenia/gpu/registers.h" #include "xenia/gpu/xenos.h" +#include "xenia/gpu/xenos_report_controller.h" #include "xenia/kernel/kernel_state.h" #include "xenia/ui/d3d12/d3d12_presenter.h" #include "xenia/ui/d3d12/d3d12_util.h" @@ -145,63 +147,6 @@ void D3D12CommandProcessor::RestoreEdramSnapshot(const void* snapshot) { render_target_cache_->RestoreEdramSnapshot(snapshot); } -void D3D12CommandProcessor::PrepareForWait() { - CheckSubmissionCompletion(0); - CommandProcessor::PrepareForWait(); -} - -void D3D12CommandProcessor::ReturnFromWait() { - CheckSubmissionCompletion(0); - CommandProcessor::ReturnFromWait(); -} - -bool D3D12CommandProcessor::ExecutePacketType3_EVENT_WRITE_ZPD(uint32_t packet, - uint32_t count) { - if (!cvars::occlusion_query_enable || !occlusion_query_resources_available_) { - return CommandProcessor::ExecutePacketType3_EVENT_WRITE_ZPD(packet, count); - } - - const uint32_t kQueryFinished = xe::byte_swap(0xFFFFFEED); - assert_true(count == 1); - uint32_t initiator = reader_.ReadAndSwap(); - D3D12CommandProcessor::WriteEventInitiator(initiator & 0x3F); - - // Get the current query ID from the PA_SC_VIZ_QUERY register - auto viz_query = register_file_->Get(); - - uint32_t sample_count_addr = - register_file_->values[XE_GPU_REG_RB_SAMPLE_COUNT_ADDR]; - auto* sample_counts = - memory_->TranslatePhysical( - sample_count_addr); - if (!sample_counts) { - return CommandProcessor::ExecutePacketType3_EVENT_WRITE_ZPD(packet, count); - } - - bool is_end_via_z_pass = sample_counts->ZPass_A == kQueryFinished && - sample_counts->ZPass_B == kQueryFinished; - bool is_end_via_z_fail = sample_counts->ZFail_A == kQueryFinished && - sample_counts->ZFail_B == kQueryFinished; - bool is_end = is_end_via_z_pass || is_end_via_z_fail; - - if (!is_end) { - if (!BeginGuestOcclusionQuery(sample_count_addr)) { - return CommandProcessor::ExecutePacketType3_EVENT_WRITE_ZPD(packet, - count); - } - // Don't clear sample_counts here - the query is async and games may poll it - return true; - } - - if (!EndGuestOcclusionQuery(sample_count_addr, sample_counts)) { - // Query failed - fall back to fake implementation - occlusion_query_stats_.queries_failed++; - return CommandProcessor::ExecutePacketType3_EVENT_WRITE_ZPD(packet, count); - } - - return true; -} - bool D3D12CommandProcessor::PushTransitionBarrier( ID3D12Resource* resource, D3D12_RESOURCE_STATES old_state, D3D12_RESOURCE_STATES new_state, UINT subresource) { @@ -1249,6 +1194,9 @@ bool D3D12CommandProcessor::SetupContext() { return false; } + zpd_draw_resolution_scale_x_ = draw_resolution_scale_x; + zpd_draw_resolution_scale_y_ = draw_resolution_scale_y; + pipeline_cache_ = std::make_unique(*this, *register_file_, *render_target_cache_.get(), bindless_resources_used_); @@ -1725,7 +1673,9 @@ bool D3D12CommandProcessor::SetupContext() { uint32_t(SystemBindlessView::kGammaRampPWLSRV))); } - InitializeOcclusionQueryResources(); + // Initialize the ZPD occlusion query pool and resources. + zpd_host_query_pool_ = std::make_unique(); + EnsureZPDQueryResources(); pix_capture_requested_.store(false, std::memory_order_relaxed); pix_capturing_ = false; @@ -1761,7 +1711,8 @@ void D3D12CommandProcessor::ShutdownContext() { ui::d3d12::util::ReleaseAndNull(memexport_readback_buffer_); memexport_readback_buffer_size_ = 0; - ShutdownOcclusionQueryResources(); + ShutdownZPDQueryResources(); + zpd_host_query_pool_.reset(); ui::d3d12::util::ReleaseAndNull(scratch_buffer_); scratch_buffer_size_ = 0; @@ -2655,6 +2606,10 @@ void D3D12CommandProcessor::IssueSwap(uint32_t frontbuffer_ptr, } void D3D12CommandProcessor::OnPrimaryBufferEnd() { + // Pump any completed resolves now since the guest is likely about to poll. + PumpQueryResolves(); + PumpPendingRetire(); + if (cvars::submit_on_primary_buffer_end && submission_open_ && CanEndSubmissionImmediately()) { EndSubmission(false); @@ -2816,13 +2771,31 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, if (cvars::async_shader_compilation) { if (pipeline_cache_->GetD3D12PipelineByHandle(pipeline_handle) == nullptr) { - XELOGI( - "Skipping draw - pipeline not ready: VS {:016X} mod {:016X}, PS " - "{:016X} mod {:016X}", - vertex_shader->ucode_data_hash(), vertex_shader_modification.value, - pixel_shader ? pixel_shader->ucode_data_hash() : 0, - pixel_shader_modification.value); - return true; + if (!zpd_active_segment_.logical_active) { + XELOGI( + "Skipping draw - pipeline not ready: VS {:016X} mod {:016X}, PS " + "{:016X} mod {:016X}", + vertex_shader->ucode_data_hash(), vertex_shader_modification.value, + pixel_shader ? pixel_shader->ucode_data_hash() : 0, + pixel_shader_modification.value); + return true; + } + if (cvars::occlusion_query_log) { + XELOGI( + "ZPD: Awaiting pending D3D12 pipeline for active query draw " + "VS={:016X} PS={:016X}", + vertex_shader ? vertex_shader->ucode_data_hash() : 0, + pixel_shader ? pixel_shader->ucode_data_hash() : 0); + } + if (pipeline_cache_->AwaitD3D12PipelineByHandle(pipeline_handle) == + nullptr) { + XELOGE( + "IssueDraw: Pipeline unavailable after await for active query draw " + "VS={:016X} PS={:016X}", + vertex_shader ? vertex_shader->ucode_data_hash() : 0, + pixel_shader ? pixel_shader->ucode_data_hash() : 0); + return false; + } } // Re-fetch root signature now that pipeline is ready. root_signature = pipeline_cache_->GetRootSignatureByHandle(pipeline_handle); @@ -3824,8 +3797,9 @@ void D3D12CommandProcessor::CheckSubmissionCompletion( texture_cache_->CompletedSubmissionUpdated(completed_submission); - // Process async occlusion queries that completed - ProcessReadyOcclusionQueries(completed_submission); + // Pull completed query resolves so logical ZPD reports can be completed and + // retire without extra waits. + DrainQueryResolves(completed_submission); } bool D3D12CommandProcessor::BeginSubmission(bool is_guest_command) { @@ -3886,6 +3860,11 @@ bool D3D12CommandProcessor::BeginSubmission(bool is_guest_command) { // fulfilled). deferred_command_list_.Reset(); + // Resume the active query segment. + if (GetZPDMode() != ZPDMode::kFake && zpd_active_segment_.logical_active) { + OpenQuerySegment(false); + } + // Reset cached state of the command list. ff_viewport_update_needed_ = true; ff_scissor_update_needed_ = true; @@ -3914,28 +3893,24 @@ bool D3D12CommandProcessor::BeginSubmission(bool is_guest_command) { if (is_opening_frame) { frame_open_ = true; - // Log occlusion query stats every 100 frames - if (cvars::occlusion_query_enable && occlusion_query_resources_available_ && - frame_current_ - occlusion_query_stats_.last_log_frame >= 100) { + // Log guest ZPD report stats every 100 frames. + if (GetZPDMode() != ZPDMode::kFake && cvars::occlusion_query_log && + zpd_host_query_pool_ && zpd_host_query_pool_->capacity() && + zpd_report_controller_ && + frame_current_ - zpd_stats_.last_log_frame >= 100) { + XenosReportController::Stats report_stats = + zpd_report_controller_->stats(); XELOGI( "Occlusion Query Stats (last 100 frames): " - "Begun={}, Ended={}, Failed={}, Sync={}, " - "CursorWraps={}, MaxCursor={}/{}", - occlusion_query_stats_.queries_begun, - occlusion_query_stats_.queries_ended, - occlusion_query_stats_.queries_failed, - occlusion_query_stats_.queries_resolved_sync, - occlusion_query_stats_.cursor_wraps, - occlusion_query_stats_.max_cursor_value, kMaxOcclusionQueries); + "LogicalBegun={}, LogicalEnded={}, SegBegun={}, SegEnded={}, " + "WritesRetired={}, PoolExhausted={}, Failed={}", + zpd_stats_.logical_begun, zpd_stats_.logical_ended, + zpd_stats_.segments_begun, zpd_stats_.segments_ended, + report_stats.writes_retired, zpd_stats_.pool_exhausted, + zpd_stats_.failed); - // Reset stats for next interval - occlusion_query_stats_.queries_begun = 0; - occlusion_query_stats_.queries_ended = 0; - occlusion_query_stats_.queries_failed = 0; - occlusion_query_stats_.queries_resolved_sync = 0; - occlusion_query_stats_.cursor_wraps = 0; - occlusion_query_stats_.max_cursor_value = 0; - occlusion_query_stats_.last_log_frame = frame_current_; + zpd_report_controller_->ResetStats(); + zpd_stats_.Reset(frame_current_); } // Reset bindings that depend on the data stored in the pools. @@ -4021,18 +3996,11 @@ bool D3D12CommandProcessor::EndSubmission(bool is_swap) { if (submission_open_) { assert_false(scratch_buffer_used_); - // We can't close the command list with an active query - D3D12 requirement - // Force-end it and wait for the result immediately to avoid data loss - if (active_occlusion_query_.valid && cvars::occlusion_query_enable && - occlusion_query_resources_available_) { - // Translate the address to get the pointer - auto* sample_counts = - memory_->TranslatePhysical( - active_occlusion_query_.sample_count_address); - // Call EndGuestOcclusionQuery which will do a synchronous wait - // This ensures we get the complete result before closing the submission - EndGuestOcclusionQuery(active_occlusion_query_.sample_count_address, - sample_counts); + // We can't close the command list with an active query - D3D12 requirement. + // Close the active segment and emit ResolveQueryData before executing. + if (GetZPDMode() != ZPDMode::kFake) { + CloseQuerySegment(); + RecordZPDResolveBatch(); } pipeline_cache_->EndSubmission(); @@ -4076,6 +4044,12 @@ bool D3D12CommandProcessor::EndSubmission(bool is_swap) { submission_open_ = false; + // Pump ZPD query process. This drains any resolves that became readable + // from completed work and retires reports unblocked by those resolves. + // Strict mode may block here before any guest visible progress continues. + PumpQueryResolves(); + PumpPendingRetire(); + // Queue operations done directly (like UpdateTileMappings) will be awaited // alongside the last submission if needed. queue_operations_done_since_submission_signal_ = false; @@ -5760,276 +5734,143 @@ ID3D12Resource* D3D12CommandProcessor::RequestReadbackBuffer(uint32_t size) { return memexport_readback_buffer_; } -bool D3D12CommandProcessor::InitializeOcclusionQueryResources() { - active_occlusion_query_ = {}; - occlusion_query_cursor_ = 0; - occlusion_query_stats_ = {}; - pending_occlusion_queries_.clear(); - occlusion_query_resources_available_ = false; - occlusion_query_heap_.Reset(); - occlusion_query_readback_.Reset(); - - ID3D12Device* device = GetD3D12Provider().GetDevice(); - if (!device) { - return false; +void D3D12CommandProcessor::EnsureZPDQueryResources() { + if (GetZPDMode() == ZPDMode::kFake) { + return; } - D3D12_QUERY_HEAP_DESC heap_desc; - heap_desc.Type = D3D12_QUERY_HEAP_TYPE_OCCLUSION; - heap_desc.Count = kMaxOcclusionQueries; - heap_desc.NodeMask = 0; - if (FAILED(device->CreateQueryHeap(&heap_desc, - IID_PPV_ARGS(&occlusion_query_heap_)))) { - XELOGW( - "D3D12CommandProcessor: Failed to create the occlusion query heap, " - "falling back to fake sample counts."); - return false; - } - - D3D12_RESOURCE_DESC buffer_desc; - ui::d3d12::util::FillBufferResourceDesc( - buffer_desc, sizeof(uint64_t) * kMaxOcclusionQueries, - D3D12_RESOURCE_FLAG_NONE); - if (FAILED(device->CreateCommittedResource( - &ui::d3d12::util::kHeapPropertiesReadback, - GetD3D12Provider().GetHeapFlagCreateNotZeroed(), &buffer_desc, - D3D12_RESOURCE_STATE_COPY_DEST, nullptr, - IID_PPV_ARGS(&occlusion_query_readback_)))) { - XELOGW( - "D3D12CommandProcessor: Failed to allocate the occlusion query " - "readback buffer, falling back to fake sample counts."); - occlusion_query_heap_.Reset(); - return false; - } - - // Map the readback buffer persistently for the lifetime of the resource - D3D12_RANGE read_range = {0, sizeof(uint64_t) * kMaxOcclusionQueries}; - void* mapping = nullptr; - if (FAILED(occlusion_query_readback_->Map(0, &read_range, &mapping))) { - XELOGW( - "D3D12CommandProcessor: Failed to map the occlusion query readback " - "buffer, falling back to fake sample counts."); - occlusion_query_readback_.Reset(); - occlusion_query_heap_.Reset(); - return false; - } - occlusion_query_readback_mapping_ = reinterpret_cast(mapping); - - occlusion_query_resources_available_ = true; - return true; + bool can_recreate = !zpd_active_segment_.logical_active && + !zpd_active_segment_.segment_active && + !zpd_host_query_pool_->has_pending_resolve_batch() && + zpd_resolves_in_flight_.empty(); + zpd_host_query_pool_->EnsureInitialized(GetD3D12Provider(), + kZPDQueryPoolCapacity, can_recreate); } -void D3D12CommandProcessor::ShutdownOcclusionQueryResources() { - DisableHostOcclusionQueries(); +bool D3D12CommandProcessor::CanOpenZPDQuery() const { return submission_open_; } - if (occlusion_query_readback_ && occlusion_query_readback_mapping_) { - occlusion_query_readback_->Unmap(0, nullptr); - occlusion_query_readback_mapping_ = nullptr; +CommandProcessor::QueryOpenResult D3D12CommandProcessor::OpenZPDQuery( + uint32_t& out_host_index, uint32_t& out_host_generation, + bool can_close_submission) { + bool is_pool_exhausted = !zpd_host_query_pool_->has_free_indices(); + + if (is_pool_exhausted) { + DrainQueryResolves(GetCompletedSubmission()); + is_pool_exhausted = !zpd_host_query_pool_->has_free_indices(); } - occlusion_query_heap_.Reset(); - occlusion_query_readback_.Reset(); -} + bool waited_for_submission = false; -void D3D12CommandProcessor::DisableHostOcclusionQueries() { - // End any active query first to avoid D3D12 validation errors - if (active_occlusion_query_.valid && occlusion_query_heap_) { - if (BeginSubmission(true)) { - deferred_command_list_.D3DEndQuery(occlusion_query_heap_.Get(), - D3D12_QUERY_TYPE_OCCLUSION, - active_occlusion_query_.host_index); - // Don't resolve - we're abandoning the result - EndSubmission(false); + if (is_pool_exhausted) { + if (GetZPDMode() == ZPDMode::kFast) { + return QueryOpenResult::kPoolExhausted; + } + + uint64_t wait_for = 0; + if (!zpd_resolves_in_flight_.empty()) { + wait_for = zpd_resolves_in_flight_.front().submission; + } + + uint64_t completed_submission = GetCompletedSubmission(); + if (wait_for > completed_submission) { + if (wait_for >= GetCurrentSubmission()) { + if (can_close_submission) { + if (!EndSubmission(false)) { + return QueryOpenResult::kFailed; + } + } + + return QueryOpenResult::kDeferred; + } + + if (cvars::occlusion_query_log) { + XELOGI("ZPD: Stall awaiting submission={} completed_before={}", + wait_for, completed_submission); + } + + completion_timeline_->AwaitSubmissionAndUpdateCompleted(wait_for); + waited_for_submission = true; + DrainQueryResolves(GetCompletedSubmission()); + is_pool_exhausted = !zpd_host_query_pool_->has_free_indices(); } } - active_occlusion_query_ = {}; - pending_occlusion_queries_.clear(); - occlusion_query_cursor_ = 0; + + if (is_pool_exhausted) { + return waited_for_submission ? QueryOpenResult::kPoolExhausted + : QueryOpenResult::kDeferred; + } + + if (!zpd_host_query_pool_->AcquireQueryIndex(out_host_index, + out_host_generation)) { + return QueryOpenResult::kFailed; + } + + zpd_host_query_pool_->BeginQuery(deferred_command_list_, out_host_index); + return QueryOpenResult::kOpened; } -bool D3D12CommandProcessor::AcquireOcclusionQueryIndex( - uint32_t& host_index_out) { - if (occlusion_query_cursor_ >= kMaxOcclusionQueries) { - // Reset cursor - all queries complete synchronously now - occlusion_query_cursor_ = 0; - occlusion_query_stats_.cursor_wraps++; - } - host_index_out = occlusion_query_cursor_++; - - // Track max cursor value to see how many slots are actually used - if (occlusion_query_cursor_ > occlusion_query_stats_.max_cursor_value) { - occlusion_query_stats_.max_cursor_value = occlusion_query_cursor_; - } - +bool D3D12CommandProcessor::CloseZPDQuery(uint32_t host_index, + uint32_t /*unused*/, + uint64_t& out_submission) { + zpd_host_query_pool_->EndQuery(deferred_command_list_, host_index); + zpd_host_query_pool_->QueueQueryResolve(host_index); + out_submission = GetCurrentSubmission(); return true; } -bool D3D12CommandProcessor::BeginGuestOcclusionQuery( - uint32_t sample_count_address) { - if (!cvars::occlusion_query_enable || !occlusion_query_resources_available_) { - return false; - } - - if (active_occlusion_query_.valid) { - // Can't begin a new query while one is active - this would violate D3D12 - // rules - XELOGW( - "D3D12CommandProcessor: Occlusion query begin issued while another " - "query is active at address 0x{:08X}", - active_occlusion_query_.sample_count_address); - // Just end the current query without starting a new one - const uint32_t host_index = active_occlusion_query_.host_index; - active_occlusion_query_.valid = false; - active_occlusion_query_.cache_serviced = false; - - // End the orphaned query to avoid D3D12 errors - if (submission_open_ && host_index != UINT32_MAX) { - deferred_command_list_.D3DEndQuery( - occlusion_query_heap_.Get(), D3D12_QUERY_TYPE_OCCLUSION, host_index); - // Don't resolve - we're abandoning this query - } - // Try again now that the active query is cleared - } - - uint32_t host_index = 0; - if (!AcquireOcclusionQueryIndex(host_index)) { - return false; - } - if (!BeginSubmission(true)) { - return false; - } - deferred_command_list_.D3DBeginQuery(occlusion_query_heap_.Get(), - D3D12_QUERY_TYPE_OCCLUSION, host_index); - - auto viz_query = register_file_->Get(); - - active_occlusion_query_.sample_count_address = sample_count_address; - active_occlusion_query_.query_id = viz_query.viz_query_id; - active_occlusion_query_.host_index = host_index; - active_occlusion_query_.valid = true; - active_occlusion_query_.cache_serviced = false; - - occlusion_query_stats_.queries_begun++; - return true; -} - -bool D3D12CommandProcessor::EndGuestOcclusionQuery( - uint32_t sample_count_address, - xenos::xe_gpu_depth_sample_counts* sample_counts) { - if (!cvars::occlusion_query_enable || !occlusion_query_resources_available_) { - return false; - } - - // Check if we have an active query - if (!active_occlusion_query_.valid) { - // No active query - might have been ended at submission boundary - return false; - } - - const uint32_t host_index = active_occlusion_query_.host_index; - - // Mark as invalid BEFORE ending to prevent restart in BeginSubmission - active_occlusion_query_.valid = false; - active_occlusion_query_.cache_serviced = false; - - // Issue END query - if (!BeginSubmission(true)) { - return false; - } - - deferred_command_list_.D3DEndQuery(occlusion_query_heap_.Get(), - D3D12_QUERY_TYPE_OCCLUSION, host_index); - InsertDebugMarker("Occlusion Query Readback: index %u", host_index); - deferred_command_list_.D3DResolveQueryData( - occlusion_query_heap_.Get(), D3D12_QUERY_TYPE_OCCLUSION, host_index, 1, - occlusion_query_readback_.Get(), sizeof(uint64_t) * host_index); - - // Force submission and sync wait - guest expects result immediately +bool D3D12CommandProcessor::DiscardZPDQuery(uint32_t host_index, + uint32_t host_generation) { + // D3D12 requires a paired EndQuery before the slot can be released. + // EndSubmission flushes it so the slot can be freed without a resolve. + zpd_host_query_pool_->EndQuery(deferred_command_list_, host_index); if (!EndSubmission(false)) { return false; } - - uint64_t query_submission = GetCurrentSubmission() - 1; - - // Wait for GPU to complete - CheckSubmissionCompletion handles the waiting - // internally via the completion timeline - CheckSubmissionCompletion(query_submission); - if (GetCompletedSubmission() < query_submission) { - XELOGE("Failed to wait for occlusion query completion"); - occlusion_query_stats_.queries_failed++; - return false; - } - - // Read result and write to guest memory - if (!occlusion_query_readback_mapping_) { - XELOGE("Occlusion query readback buffer not mapped"); - occlusion_query_stats_.queries_failed++; - return false; - } - - uint64_t samples = occlusion_query_readback_mapping_[host_index]; - samples = NormalizeOcclusionSamples(samples); - WriteGuestOcclusionResult(sample_counts, samples); - - occlusion_query_stats_.queries_resolved_sync++; - occlusion_query_stats_.queries_ended++; + zpd_host_query_pool_->ReleaseQueryIndex(host_index, host_generation); return true; } -uint64_t D3D12CommandProcessor::NormalizeOcclusionSamples( - uint64_t samples) const { - if (samples == 0 || !texture_cache_) { - return samples; - } - uint64_t scale_x = texture_cache_->draw_resolution_scale_x(); - uint64_t scale_y = texture_cache_->draw_resolution_scale_y(); - uint64_t scale = scale_x * scale_y; - if (scale <= 1) { - return samples; - } - return (samples + (scale >> 1)) / scale; +CommandProcessor::ZPDSubmissionBridge* +D3D12CommandProcessor::GetZPDSubmissionBridge() { + return &zpd_submission_bridge_; } -void D3D12CommandProcessor::WriteGuestOcclusionResult( - xenos::xe_gpu_depth_sample_counts* sample_counts, uint64_t samples) { - if (!sample_counts) { - return; - } - uint32_t clamped = - samples > uint64_t(UINT32_MAX) ? UINT32_MAX : uint32_t(samples); - sample_counts->Total_A = clamped; - sample_counts->Total_B = 0; - sample_counts->ZPass_A = clamped; - sample_counts->ZPass_B = 0; - sample_counts->ZFail_A = 0; - sample_counts->ZFail_B = 0; - sample_counts->StencilFail_A = 0; - sample_counts->StencilFail_B = 0; +CommandProcessor::ZPDSubmissionState +D3D12CommandProcessor::ZPDSubmissionBridge::GetState() const { + return {command_processor_.GetCurrentSubmission(), + command_processor_.GetCompletedSubmission()}; } -void D3D12CommandProcessor::ProcessReadyOcclusionQueries( - uint64_t completed_submission) { - if (!cvars::occlusion_query_enable || !occlusion_query_resources_available_ || - pending_occlusion_queries_.empty()) { - return; +bool D3D12CommandProcessor::ZPDSubmissionBridge::EnsureProgress() { + if (!command_processor_.submission_open_) { + return false; } - // Process all queries whose submission has completed - while (!pending_occlusion_queries_.empty() && - pending_occlusion_queries_.front().submission <= - completed_submission) { - PendingOcclusionQuery query = pending_occlusion_queries_.front(); - pending_occlusion_queries_.pop_front(); - - // Read result from persistent mapping - uint64_t samples = occlusion_query_readback_mapping_[query.host_index]; - samples = NormalizeOcclusionSamples(samples); - - // Write to guest memory - WriteGuestOcclusionResult(query.sample_counts, samples); - - // Note: Don't increment stats here - caller decides if async or sync + if (!command_processor_.CanEndSubmissionImmediately() && + !command_processor_.pipeline_cache_->IsCreatingPipelines()) { + return false; } + if (!command_processor_.CanEndSubmissionImmediately()) { + if (cvars::occlusion_query_log) { + XELOGI( + "ZPD: Awaiting pending D3D12 pipeline for active query retirement"); + } + command_processor_.pipeline_cache_->AwaitPipelineCompletion(); + } + + return command_processor_.CanEndSubmissionImmediately() && + command_processor_.EndSubmission(false); +} + +void D3D12CommandProcessor::ZPDSubmissionBridge::AwaitSubmission( + uint64_t submission) { + command_processor_.completion_timeline_->AwaitSubmissionAndUpdateCompleted( + submission); +} + +void D3D12CommandProcessor::RecordZPDResolveBatch() { + zpd_host_query_pool_->FlushResolveBatch(deferred_command_list_, + submission_open_); } void D3D12CommandProcessor::WriteGammaRampSRV( @@ -6054,10 +5895,7 @@ void D3D12CommandProcessor::WriteGammaRampSRV( } #define COMMAND_PROCESSOR D3D12CommandProcessor -#define XE_GPU_OVERRIDES_EVENT_WRITE_ZPD - #include "../pm4_command_processor_implement.h" -#undef XE_GPU_OVERRIDES_EVENT_WRITE_ZPD #undef COMMAND_PROCESSOR } // namespace d3d12 } // namespace gpu diff --git a/src/xenia/gpu/d3d12/d3d12_command_processor.h b/src/xenia/gpu/d3d12/d3d12_command_processor.h index 85ccb25fc..c7b18d6c8 100644 --- a/src/xenia/gpu/d3d12/d3d12_command_processor.h +++ b/src/xenia/gpu/d3d12/d3d12_command_processor.h @@ -27,6 +27,7 @@ #include "xenia/gpu/d3d12/d3d12_render_target_cache.h" #include "xenia/gpu/d3d12/d3d12_shared_memory.h" #include "xenia/gpu/d3d12/d3d12_texture_cache.h" +#include "xenia/gpu/d3d12/d3d12_zpd_query_pool.h" #include "xenia/gpu/d3d12/deferred_command_list.h" #include "xenia/gpu/d3d12/pipeline_cache.h" #include "xenia/gpu/draw_util.h" @@ -56,6 +57,7 @@ struct MemExportRange { uint32_t base_address_dwords; uint32_t size_dwords; }; + class D3D12CommandProcessor final : public CommandProcessor { protected: #define OVERRIDING_BASE_CMDPROCESSOR @@ -80,13 +82,6 @@ class D3D12CommandProcessor final : public CommandProcessor { void RestoreEdramSnapshot(const void* snapshot) override; - void PrepareForWait() override; - void ReturnFromWait() override; - bool SupportsGuestOcclusionQueries() const override { - return occlusion_query_resources_available_ && - cvars::occlusion_query_enable; - } - ui::d3d12::D3D12Provider& GetD3D12Provider() const { return *static_cast( graphics_system_->provider()); @@ -509,22 +504,63 @@ class D3D12CommandProcessor final : public CommandProcessor { void WriteGammaRampSRV(bool is_pwl, D3D12_CPU_DESCRIPTOR_HANDLE handle) const; - bool InitializeOcclusionQueryResources(); - void ShutdownOcclusionQueryResources(); - bool BeginGuestOcclusionQuery(uint32_t sample_count_address); - bool EndGuestOcclusionQuery(uint32_t sample_count_address, - xenos::xe_gpu_depth_sample_counts* sample_counts); - void ProcessReadyOcclusionQueries(uint64_t completed_submission); - bool AcquireOcclusionQueryIndex(uint32_t& host_index_out); - void DisableHostOcclusionQueries(); - uint64_t NormalizeOcclusionSamples(uint64_t samples) const; - void WriteGuestOcclusionResult( - xenos::xe_gpu_depth_sample_counts* sample_counts, uint64_t samples); + // ZPD occlusion queries backend. + // BeginQuery/EndQuery must be in the same command list, segments split at + // EndSubmission, resume at BeginSubmission. Discarded queries still need + // EndQuery or the heap slot breaks on some drivers. RecordZPDResolveBatch + // emits coalesced ResolveQueryData at submit. First query at a fresh slot + // always produce 0 if the pool was exhausted when it was issued. + void EnsureZPDQueryResources() override; + void ShutdownZPDQueryResources() override { + zpd_host_query_pool_->Shutdown(); + } + + bool IsZPDQueryPoolReady() const override { + return zpd_host_query_pool_->is_initialized(); + } + bool CanOpenZPDQuery() const override; + + QueryOpenResult OpenZPDQuery(uint32_t& out_host_index, + uint32_t& out_host_generation, + bool can_close_submission) override; + bool CloseZPDQuery(uint32_t host_index, uint32_t host_generation, + uint64_t& out_submission) override; + bool DiscardZPDQuery(uint32_t host_index, uint32_t host_generation) override; + uint64_t GetZPDQueryResult(uint32_t host_index) override { + return zpd_host_query_pool_->GetQueryReadbackValue(host_index); + } + void ReleaseZPDQuery(uint32_t host_index, uint32_t host_generation) override { + zpd_host_query_pool_->ReleaseQueryIndex(host_index, host_generation); + } + bool IsZPDQueryResultValid(uint32_t host_index, + uint32_t host_generation) const override { + return zpd_host_query_pool_->GenerationMatches(host_index, host_generation); + } + + // Record the pending resolve batch on the current command list. + void RecordZPDResolveBatch(); + + CommandProcessor::ZPDSubmissionBridge* GetZPDSubmissionBridge() override; + + class ZPDSubmissionBridge final + : public CommandProcessor::ZPDSubmissionBridge { + public: + explicit ZPDSubmissionBridge(D3D12CommandProcessor& command_processor) + : command_processor_(command_processor) {} + CommandProcessor::ZPDSubmissionState GetState() const override; + bool EnsureProgress() override; + void AwaitSubmission(uint64_t submission) override; + + private: + D3D12CommandProcessor& command_processor_; + }; bool device_removed_ = false; bool cache_clear_requested_ = false; + ZPDSubmissionBridge zpd_submission_bridge_{*this}; + std::unique_ptr completion_timeline_; bool submission_open_ = false; @@ -567,6 +603,8 @@ class D3D12CommandProcessor final : public CommandProcessor { std::unique_ptr render_target_cache_; + std::unique_ptr zpd_host_query_pool_; + std::unique_ptr constant_buffer_pool_; static constexpr uint32_t kViewBindfulHeapSize = 32768; @@ -723,42 +761,6 @@ class D3D12CommandProcessor final : public CommandProcessor { Microsoft::WRL::ComPtr fxaa_source_texture_; uint64_t fxaa_source_texture_submission_ = 0; - // Occlusion query resources. - Microsoft::WRL::ComPtr occlusion_query_heap_; - Microsoft::WRL::ComPtr occlusion_query_readback_; - uint64_t* occlusion_query_readback_mapping_ = nullptr; // Persistent mapping - uint32_t occlusion_query_cursor_ = 0; - bool occlusion_query_resources_available_ = false; - struct ActiveOcclusionQuery { - uint32_t sample_count_address = 0; - uint32_t query_id = 0; // VIZ_QUERY ID (0-63) - uint32_t host_index = UINT32_MAX; - bool valid = false; - bool cache_serviced = false; // True if using cached result, no D3D12 query - } active_occlusion_query_; - - // Pending async queries (resolved when submission completes) - struct PendingOcclusionQuery { - uint32_t host_index; - uint64_t submission; - uint32_t sample_count_address; - xenos::xe_gpu_depth_sample_counts* - sample_counts; // Cached pointer (nullptr for cache-only updates) - uint32_t query_id; - }; - std::deque pending_occlusion_queries_; - - // Query statistics (logged every 100 frames) - struct OcclusionQueryStats { - uint64_t queries_begun = 0; - uint64_t queries_ended = 0; - uint64_t queries_failed = 0; - uint64_t queries_resolved_sync = 0; // Required GPU stall - uint64_t cursor_wraps = 0; - uint32_t max_cursor_value = 0; - uint64_t last_log_frame = 0; - } occlusion_query_stats_; - // Unsubmitted barrier batch. std::vector barriers_; diff --git a/src/xenia/gpu/d3d12/d3d12_zpd_query_pool.cc b/src/xenia/gpu/d3d12/d3d12_zpd_query_pool.cc new file mode 100644 index 000000000..5c8fe8eaf --- /dev/null +++ b/src/xenia/gpu/d3d12/d3d12_zpd_query_pool.cc @@ -0,0 +1,263 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/gpu/d3d12/d3d12_zpd_query_pool.h" + +#include + +#include "xenia/base/logging.h" +#include "xenia/gpu/d3d12/deferred_command_list.h" +#include "xenia/ui/d3d12/d3d12_provider.h" +#include "xenia/ui/d3d12/d3d12_util.h" + +namespace xe { +namespace gpu { +namespace d3d12 { +namespace { + +struct ResolveRange { + uint32_t start; + uint32_t count; +}; +} // namespace + +bool D3D12ZPDQueryPool::EnsureInitialized( + const ui::d3d12::D3D12Provider& provider, uint32_t requested_capacity, + bool can_recreate) { + if (is_initialized() && (capacity_ == requested_capacity || !can_recreate)) { + return true; + } + + // Can't recreate while resolves are in-flight, that would destroy the heap + // under a live ResolveQueryData call. + assert_true(!is_initialized() || !has_pending_resolve_batch()); + Shutdown(); + + ID3D12Device* device = provider.GetDevice(); + + D3D12_QUERY_HEAP_DESC heap_desc = {}; + heap_desc.Type = D3D12_QUERY_HEAP_TYPE_OCCLUSION; + heap_desc.Count = requested_capacity; + heap_desc.NodeMask = 0; + + if (FAILED(device->CreateQueryHeap(&heap_desc, IID_PPV_ARGS(&query_heap_)))) { + XELOGW( + "D3D12ZPDQueryPool: Failed to create the ZPD query " + "heap, falling back to fake sample counts."); + return false; + } + + D3D12_RESOURCE_DESC buffer_desc; + ui::d3d12::util::FillBufferResourceDesc(buffer_desc, + sizeof(uint64_t) * requested_capacity, + D3D12_RESOURCE_FLAG_NONE); + + if (FAILED(device->CreateCommittedResource( + &ui::d3d12::util::kHeapPropertiesReadback, + provider.GetHeapFlagCreateNotZeroed(), &buffer_desc, + D3D12_RESOURCE_STATE_COPY_DEST, nullptr, + IID_PPV_ARGS(&readback_buffer_)))) { + XELOGW( + "D3D12ZPDQueryPool: Failed to allocate the ZPD query " + "readback buffer, falling back to fake sample counts."); + Shutdown(); + return false; + } + + D3D12_RANGE read_range = {}; + read_range.Begin = 0; + read_range.End = sizeof(uint64_t) * requested_capacity; + + void* mapping = nullptr; + if (FAILED(readback_buffer_->Map(0, &read_range, &mapping))) { + XELOGW( + "D3D12ZPDQueryPool: Failed to map the ZPD query " + "readback buffer, falling back to fake sample counts."); + Shutdown(); + return false; + } + + readback_mapping_ = reinterpret_cast(mapping); + capacity_ = requested_capacity; + + resolve_batch_pending_.assign(requested_capacity, 0); + resolve_batch_index_count_ = 0; + + free_indices_.clear(); + free_indices_.reserve(requested_capacity); + for (uint32_t i = requested_capacity; i > 0; --i) { + free_indices_.push_back(i - 1); + } + index_generations_.assign(requested_capacity, 0); + + return true; +} + +void D3D12ZPDQueryPool::Shutdown() { + resolve_batch_pending_.clear(); + resolve_batch_index_count_ = 0; + free_indices_.clear(); + index_generations_.clear(); + + capacity_ = 0; + + if (readback_mapping_ && readback_buffer_) { + readback_buffer_->Unmap(0, nullptr); + } + readback_mapping_ = nullptr; + + readback_buffer_.Reset(); + query_heap_.Reset(); +} + +bool D3D12ZPDQueryPool::AcquireQueryIndex(uint32_t& query_index, + uint32_t& query_generation) { + if (free_indices_.empty()) { + query_index = UINT32_MAX; + query_generation = 0; + return false; + } + + query_index = free_indices_.back(); + free_indices_.pop_back(); + + assert_true(query_index < index_generations_.size()); + // Bump the generation. Any in-flight readbacks for the slot's previous + // occupants are ignored. + query_generation = ++index_generations_[query_index]; + return true; +} + +void D3D12ZPDQueryPool::ReleaseQueryIndex(uint32_t query_index, + uint32_t query_generation) { + if (query_index >= capacity_) { + return; + } + + if (!GenerationMatches(query_index, query_generation)) { + XELOGW("D3D12ZPDQueryPool: stale release index={} gen={}", query_index, + query_generation); + return; + } + + free_indices_.push_back(query_index); +} + +bool D3D12ZPDQueryPool::GenerationMatches(uint32_t query_index, + uint32_t query_generation) const { + return query_index < index_generations_.size() && + index_generations_[query_index] == query_generation; +} + +void D3D12ZPDQueryPool::BeginQuery(DeferredCommandList& deferred_command_list, + uint32_t query_index) const { + if (!query_heap_ || query_index >= capacity_) { + return; + } + + deferred_command_list.D3DBeginQuery(query_heap_.Get(), + D3D12_QUERY_TYPE_OCCLUSION, query_index); +} + +void D3D12ZPDQueryPool::EndQuery(DeferredCommandList& deferred_command_list, + uint32_t query_index) const { + if (!query_heap_ || query_index >= capacity_) { + return; + } + + deferred_command_list.D3DEndQuery(query_heap_.Get(), + D3D12_QUERY_TYPE_OCCLUSION, query_index); +} + +void D3D12ZPDQueryPool::QueueQueryResolve(uint32_t query_index) { + if (query_index >= capacity_) { + return; + } + + // Guard against duplicates. Split paths can touch the same index twice before + // the batch drains at EndSubmission. + if (!resolve_batch_pending_[query_index]) { + resolve_batch_pending_[query_index] = 1; + ++resolve_batch_index_count_; + } +} + +void D3D12ZPDQueryPool::FlushResolveBatch( + DeferredCommandList& deferred_command_list, bool submission_open) { + if (!submission_open) { + return; + } + + if (!resolve_batch_index_count_) { + return; + } + + if (!is_initialized()) { + std::fill(resolve_batch_pending_.begin(), resolve_batch_pending_.end(), 0); + resolve_batch_index_count_ = 0; + return; + } + + std::vector ranges; + + // Coalesce into contiguous ranges to cut down on ResolveQueryData calls, + // which have considerable overhead. + uint32_t range_start = 0; + uint32_t range_count = 0; + for (uint32_t index = 0; index < capacity_; ++index) { + if (!resolve_batch_pending_[index]) { + continue; + } + + if (range_count == 0) { + range_start = index; + range_count = 1; + continue; + } + + if (index == range_start + range_count) { + ++range_count; + continue; + } + + ranges.push_back({range_start, range_count}); + range_start = index; + range_count = 1; + } + + if (range_count != 0) { + ranges.push_back({range_start, range_count}); + } + + // Reset the batch. ENDs from later in this submission belong to the next. + std::fill(resolve_batch_pending_.begin(), resolve_batch_pending_.end(), 0); + resolve_batch_index_count_ = 0; + + if (ranges.empty()) { + return; + } + + for (const ResolveRange& range : ranges) { + deferred_command_list.D3DResolveQueryData( + query_heap_.Get(), D3D12_QUERY_TYPE_OCCLUSION, range.start, range.count, + readback_buffer_.Get(), range.start * sizeof(uint64_t)); + } +} + +uint64_t D3D12ZPDQueryPool::GetQueryReadbackValue(uint32_t query_index) const { + if (!readback_mapping_ || query_index >= capacity_) { + return 0; + } + + return readback_mapping_[query_index]; +} + +} // namespace d3d12 +} // namespace gpu +} // namespace xe diff --git a/src/xenia/gpu/d3d12/d3d12_zpd_query_pool.h b/src/xenia/gpu/d3d12/d3d12_zpd_query_pool.h new file mode 100644 index 000000000..63e60dab8 --- /dev/null +++ b/src/xenia/gpu/d3d12/d3d12_zpd_query_pool.h @@ -0,0 +1,98 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_GPU_D3D12_D3D12_ZPD_QUERY_POOL_H_ +#define XENIA_GPU_D3D12_D3D12_ZPD_QUERY_POOL_H_ + +#include +#include + +#include "xenia/ui/d3d12/d3d12_api.h" + +namespace xe { +namespace ui { +namespace d3d12 { +class D3D12Provider; +} +} // namespace ui + +namespace gpu { +namespace d3d12 { + +class DeferredCommandList; + +// D3D12 occlusion query pool for ZPD reports. Queries live in ID3D12QueryHeap, +// results are copied to a persistent readback buffer via ResolveQueryData. +// +// D3D12 requires BeginQuery and EndQuery to be recorded in the same command +// list, so segments split at EndSubmission. Discarded queries still need a +// paired EndQuery or the heap slot may become undefined on some drivers. +// +// FlushResolveBatch coalesces pending indices into contiguous ranges to cut +// down on ResolveQueryData call count. +class D3D12ZPDQueryPool { + public: + D3D12ZPDQueryPool() = default; + D3D12ZPDQueryPool(const D3D12ZPDQueryPool&) = delete; + D3D12ZPDQueryPool& operator=(const D3D12ZPDQueryPool&) = delete; + ~D3D12ZPDQueryPool() { Shutdown(); } + + bool EnsureInitialized(const ui::d3d12::D3D12Provider& provider, + uint32_t requested_capacity, bool can_recreate); + void Shutdown(); + + bool is_initialized() const { + return query_heap_ && readback_buffer_ && readback_mapping_ != nullptr && + capacity_ != 0; + } + + uint32_t capacity() const { return capacity_; } + + bool has_pending_resolve_batch() const { + return resolve_batch_index_count_ != 0; + } + + bool has_free_indices() const { return !free_indices_.empty(); } + + bool AcquireQueryIndex(uint32_t& query_index, uint32_t& query_generation); + void ReleaseQueryIndex(uint32_t query_index, uint32_t query_generation); + bool GenerationMatches(uint32_t query_index, uint32_t query_generation) const; + + void BeginQuery(DeferredCommandList& deferred_command_list, + uint32_t query_index) const; + void EndQuery(DeferredCommandList& deferred_command_list, + uint32_t query_index) const; + void QueueQueryResolve(uint32_t query_index); + void FlushResolveBatch(DeferredCommandList& deferred_command_list, + bool submission_open); + + uint64_t GetQueryReadbackValue(uint32_t query_index) const; + + private: + Microsoft::WRL::ComPtr query_heap_; + + // Persistently mapped. Results readable once the fence signals. + Microsoft::WRL::ComPtr readback_buffer_; + uint64_t* readback_mapping_ = nullptr; + + uint32_t capacity_ = 0; + std::vector free_indices_; + + // Bumped on each acquire so stale readbacks from a recycled slot get dropped. + std::vector index_generations_; + + std::vector resolve_batch_pending_; + uint32_t resolve_batch_index_count_ = 0; +}; + +} // namespace d3d12 +} // namespace gpu +} // namespace xe + +#endif // XENIA_GPU_D3D12_D3D12_ZPD_QUERY_POOL_H_ diff --git a/src/xenia/gpu/d3d12/pipeline_cache.cc b/src/xenia/gpu/d3d12/pipeline_cache.cc index d82658c98..384150f06 100644 --- a/src/xenia/gpu/d3d12/pipeline_cache.cc +++ b/src/xenia/gpu/d3d12/pipeline_cache.cc @@ -556,6 +556,37 @@ bool PipelineCache::IsCreatingPipelines() { return !creation_queue_.empty() || creation_threads_busy_ != 0; } +void PipelineCache::AwaitPipelineCompletion() { + if (creation_threads_.empty()) { + return; + } + + bool await_creation_completion_event; + { + std::lock_guard lock(creation_request_lock_); + await_creation_completion_event = + !creation_queue_.empty() || creation_threads_busy_ != 0; + if (await_creation_completion_event) { + creation_completion_event_->Reset(); + creation_completion_set_event_ = true; + } + } + + if (await_creation_completion_event) { + creation_request_cond_.notify_one(); + xe::threading::Wait(creation_completion_event_.get(), false); + } +} + +ID3D12PipelineState* PipelineCache::AwaitD3D12PipelineByHandle(void* handle) { + ID3D12PipelineState* pipeline = GetD3D12PipelineByHandle(handle); + if (pipeline != nullptr) { + return pipeline; + } + AwaitPipelineCompletion(); + return GetD3D12PipelineByHandle(handle); +} + D3D12Shader* PipelineCache::LoadShader(xenos::ShaderType shader_type, const uint32_t* host_address, uint32_t dword_count) { diff --git a/src/xenia/gpu/d3d12/pipeline_cache.h b/src/xenia/gpu/d3d12/pipeline_cache.h index 6771fb771..172a1ee29 100644 --- a/src/xenia/gpu/d3d12/pipeline_cache.h +++ b/src/xenia/gpu/d3d12/pipeline_cache.h @@ -71,6 +71,10 @@ class PipelineCache { void EndSubmission(); bool IsCreatingPipelines(); + // Waits for any pipeline creation needed by the current draw path to finish + // before state is consumed. This was added so strict ZPD query paths stop + // racing pipeline compilation and then blocking work on incomplete state. + void AwaitPipelineCompletion(); D3D12Shader* LoadShader(xenos::ShaderType shader_type, const uint32_t* host_address, uint32_t dword_count); @@ -109,6 +113,7 @@ class PipelineCache { return reinterpret_cast(handle)->state.load( std::memory_order_acquire); } + ID3D12PipelineState* AwaitD3D12PipelineByHandle(void* handle); ID3D12RootSignature* GetRootSignatureByHandle(void* handle) const { return reinterpret_cast(handle) diff --git a/src/xenia/gpu/gpu_flags.cc b/src/xenia/gpu/gpu_flags.cc index 93c9019ca..371a2c39b 100644 --- a/src/xenia/gpu/gpu_flags.cc +++ b/src/xenia/gpu/gpu_flags.cc @@ -80,26 +80,20 @@ DEFINE_bool( "when MSAA is used with fullscreen passes.", "GPU"); -DEFINE_int32(query_occlusion_sample_lower_threshold, 80, - "If set to -1 no sample counts are written, games may hang. Else, " - "the sample count of every tile will be incremented on every " - "EVENT_WRITE_ZPD by this number. Setting this to 0 means " - "everything is reported as occluded.", +DEFINE_int32(occlusion_query_fake_lower_threshold, 80, + "Lower end of the fake sample count value written on " + "EVENT_WRITE_ZPD when real occlusion queries are disabled.\n" + "-1 writes nothing, resulting in some games that sit and hang.\n" + "0 means the fake result stays fully occluded.", "GPU"); -DEFINE_int32( - query_occlusion_sample_upper_threshold, 100, - "Set to higher number than query_occlusion_sample_lower_threshold. This " - "value is ignored if query_occlusion_sample_lower_threshold is set to -1.", - "GPU"); - -DEFINE_bool(occlusion_query_enable, false, - "Use hardware occlusion queries instead of fake results. More " - "accurate but causes GPU stalls and performance issues.", - "GPU"); - -void SetOcclusionQueryEnable(bool value) { - OVERRIDE_bool(occlusion_query_enable, value); -} +DEFINE_int32(occlusion_query_fake_upper_threshold, 100, + "Upper end of the fake sample count value written on " + "EVENT_WRITE_ZPD when real occlusion queries are disabled.\n" + "Keep this higher than occlusion_query_fake_lower_threshold.\n" + "Ignored if occlusion_query_fake_lower_threshold is -1.", + "GPU"); +DEFINE_bool(occlusion_query_log, false, + "Log occlusion query lifetime and summary stats.", "GPU"); uint32_t GetGuestVblankRateHz() { return cvars::use_50Hz_mode ? 50 : 60; } diff --git a/src/xenia/gpu/gpu_flags.h b/src/xenia/gpu/gpu_flags.h index 4067b8334..cb48b1c04 100644 --- a/src/xenia/gpu/gpu_flags.h +++ b/src/xenia/gpu/gpu_flags.h @@ -29,13 +29,13 @@ DECLARE_bool(non_seamless_cube_map); DECLARE_bool(half_pixel_offset); -DECLARE_int32(query_occlusion_sample_lower_threshold); +DECLARE_string(occlusion_query); -DECLARE_int32(query_occlusion_sample_upper_threshold); +DECLARE_int32(occlusion_query_fake_lower_threshold); -DECLARE_bool(occlusion_query_enable); +DECLARE_int32(occlusion_query_fake_upper_threshold); -void SetOcclusionQueryEnable(bool value); +DECLARE_bool(occlusion_query_log); // Returns the guest vblank rate in Hz (50 for PAL, 60 for NTSC). // Based on use_50Hz_mode cvar. diff --git a/src/xenia/gpu/pm4_command_processor_implement.h b/src/xenia/gpu/pm4_command_processor_implement.h index d6ce5c5bb..af70c4121 100644 --- a/src/xenia/gpu/pm4_command_processor_implement.h +++ b/src/xenia/gpu/pm4_command_processor_implement.h @@ -1146,14 +1146,14 @@ bool COMMAND_PROCESSOR::ExecutePacketType3_EVENT_WRITE_EXT( return true; } -static uint32_t samples = cvars::query_occlusion_sample_upper_threshold; - -#if !defined(XE_GPU_OVERRIDES_EVENT_WRITE_ZPD) XE_NOINLINE +// This is not a simple BEGIN/END around a host occlusion query. One slot can +// span multiple submissions, be force closed by a colliding BEGIN, or get an +// orphaned END with no matching BEGIN. Hardware only uses a single register +// (RB_SAMPLE_COUNT_ADDR) for the target address, no explicit handle passing. +// Debugging RB_SAMPLE_COUNT_CTL has not yet revealed any helpful bits. bool COMMAND_PROCESSOR::ExecutePacketType3_EVENT_WRITE_ZPD( uint32_t packet, uint32_t count) XE_RESTRICT { - // Set by D3D as BE but struct ABI is LE - const uint32_t kQueryFinished = xe::byte_swap(0xFFFFFEED); assert_true(count == 1); uint32_t initiator = reader_.ReadAndSwap(); uint32_t event_type = initiator & 0x3F; @@ -1166,36 +1166,158 @@ bool COMMAND_PROCESSOR::ExecutePacketType3_EVENT_WRITE_ZPD( // Writeback initiator. COMMAND_PROCESSOR::WriteEventInitiator(event_type); - if (cvars::query_occlusion_sample_lower_threshold < 0) { + uint32_t report_address = + register_file_->values[XE_GPU_REG_RB_SAMPLE_COUNT_ADDR]; + uint32_t report_record_base = XenosZPDReport::GetRecordBase(report_address); + bool is_begin_record = XenosZPDReport::IsBeginRecord(report_address); + bool is_end_record = XenosZPDReport::IsEndRecord(report_address); + + xe_gpu_depth_sample_counts* report = + report_record_base + ? memory_->TranslatePhysical( + report_record_base) + : nullptr; + + // True if the record has the pending D3D sentinel. + // Useful as a hint, but not authoritative for report boundaries. + // QueryBatch titles can have multiple pending sentinels in a row and don't + // necessarily update in an order we currently observe. + bool guest_marks_end = report && XenosZPDReport::HasPendingSentinel(report); + uint32_t slot_base = XenosZPDReport::GetSlotBase(report_address); + bool logical_active = zpd_active_segment_.logical_active; + + auto write_batch_fake = [&]() { + if (!report_record_base) { + return; + } + + if (cvars::occlusion_query_fake_lower_threshold >= 0) { + fake_zpd_sample_count_ = + (fake_zpd_sample_count_ <= + static_cast(cvars::occlusion_query_fake_lower_threshold)) + ? static_cast( + cvars::occlusion_query_fake_upper_threshold) + : fake_zpd_sample_count_ - 1; + } else if (fake_zpd_sample_count_ == 0) { + fake_zpd_sample_count_ = 1; + } + + uint32_t step = std::max(uint32_t{1}, fake_zpd_sample_count_); + zpd_batch_fake_count_ = + XenosZPDReport::AddSamples(zpd_batch_fake_count_, step); + XenosZPDReport::WriteSampleCount(report, zpd_batch_fake_count_); + }; + + if (cvars::occlusion_query_log && report) { + XELOGI( + "ZPD: EVENT_WRITE_ZPD fields event={} report_address=0x{:08X} " + "record=0x{:08X} Total=({:08X},{:08X}) ZFail=({:08X},{:08X}) " + "ZPass=({:08X},{:08X}) Stencil=({:08X},{:08X}) pending={}", + GetEventName(event_type), report_address, report_record_base, + uint32_t(report->Total_A), uint32_t(report->Total_B), + uint32_t(report->ZFail_A), uint32_t(report->ZFail_B), + uint32_t(report->ZPass_A), uint32_t(report->ZPass_B), + uint32_t(report->StencilFail_A), uint32_t(report->StencilFail_B), + guest_marks_end); + } + + // Sticky fallback. Stop using the normal query path and feed cumulative fake + // samples for the rest of the session. + if (zpd_batch_fake_) { + write_batch_fake(); return true; } - // Occlusion queries: - // This command is send on query begin and end. - // As a workaround report some fixed amount of passed samples. - auto* pSampleCounts = memory_->TranslatePhysical( - register_file_->values[XE_GPU_REG_RB_SAMPLE_COUNT_ADDR]); - // 0xFFFFFEED is written to this two locations by D3D only on D3DISSUE_END - // and used to detect a finished query. - bool is_end_via_z_pass = pSampleCounts->ZPass_A == kQueryFinished && - pSampleCounts->ZPass_B == kQueryFinished; - // Older versions of D3D also checks for ZFail (4D5307D5). - bool is_end_via_z_fail = pSampleCounts->ZFail_A == kQueryFinished && - pSampleCounts->ZFail_B == kQueryFinished; - std::memset(pSampleCounts, 0, sizeof(xe_gpu_depth_sample_counts)); - if (is_end_via_z_pass || is_end_via_z_fail) { - pSampleCounts->ZPass_A = samples; - pSampleCounts->Total_A = samples; + + // QueryBatch titles advance through pending records in steps within one page. + // Detect a short consecutive run here and pull the fake ripcord before the + // guest starts waiting on sentinels that won't clear. + uint32_t batch_page_base = XenosZPDReport::GetBatchPageBase(report_address); + // 5451082C is another batched title, but it doesn't advance through + // records. Instead, it has a fixed record orphan END that it repeatedly hits. + // Detect this pattern as well. + bool repeated_orphan_end = report_record_base && guest_marks_end && + is_end_record && !logical_active && + batch_page_base != 0 && + zpd_batch_page_ == batch_page_base && + report_record_base == zpd_batch_last_record_; + if (report_record_base && guest_marks_end && batch_page_base != 0) { + if ((zpd_batch_page_ == batch_page_base && + XenosZPDReport::IsBatchStep(zpd_batch_last_record_, + report_record_base)) || + repeated_orphan_end) { + ++zpd_batch_run_; + } else { + zpd_batch_page_ = batch_page_base; + zpd_batch_run_ = 1; + } + zpd_batch_last_record_ = report_record_base; + + if (zpd_batch_run_ >= (repeated_orphan_end ? 16u : 4u)) { + // Don't try to mix real and fake results. + zpd_batch_fake_ = true; + zpd_batch_fake_count_ = 0; + zpd_pending_retire_handle_ = XenosReportController::kInvalidReportHandle; + zpd_pending_retire_stalls_ = 0; + XELOGI( + "ZPD: Batched occlusion query pattern detected, falling back to " + "fake sample counts."); + write_batch_fake(); + return true; + } + } else { + zpd_batch_page_ = 0; + zpd_batch_last_record_ = 0; + zpd_batch_run_ = 0; } - samples = - samples <= static_cast( - cvars::query_occlusion_sample_lower_threshold) - ? static_cast(cvars::query_occlusion_sample_upper_threshold) - : samples - 1; + if (COMMAND_PROCESSOR::GetZPDMode() != ZPDMode::kFake && + zpd_report_controller_) { + if (logical_active && is_end_record) { + if (slot_base == zpd_active_segment_.slot_base) { + COMMAND_PROCESSOR::EndZPDReport(report_address, false); + } + return true; + } + if (is_begin_record) { + COMMAND_PROCESSOR::BeginZPDReport(report_address); + return true; + } + if (!logical_active && is_end_record) { + // No logical report is active for this slot, so this is likely an + // orphaned END. In fast mode, replay the last cached delta so polling + // code does not sit on the sentinel forever. + if (COMMAND_PROCESSOR::GetZPDMode() == ZPDMode::kFast) { + uint32_t cached_delta = 1; + auto cache_it = fast_zpd_report_cached_values_.find(report_record_base); + if (cache_it != fast_zpd_report_cached_values_.end()) { + cached_delta = cache_it->second; + } + COMMAND_PROCESSOR::WriteZPDReport(0, report_record_base, 0, + cached_delta, false); + } else { + // In strict mode, just pump in case a previous report has resolved. + zpd_report_controller_->RetireReports(); + } + return true; + } + return true; + } + // Conventional fake fallback, which only touches records marked as pending. + if (cvars::occlusion_query_fake_lower_threshold < 0 || !report_record_base || + !guest_marks_end) { + return true; + } + + fake_zpd_sample_count_ = + (fake_zpd_sample_count_ <= + static_cast(cvars::occlusion_query_fake_lower_threshold)) + ? static_cast(cvars::occlusion_query_fake_upper_threshold) + : fake_zpd_sample_count_ - 1; + + XenosZPDReport::WriteSampleCount(report, fake_zpd_sample_count_); return true; } -#endif // !defined(XE_GPU_OVERRIDES_EVENT_WRITE_ZPD) bool COMMAND_PROCESSOR::ExecutePacketType3Draw( uint32_t packet, const char* opcode_name, uint32_t viz_query_condition, @@ -1287,9 +1409,7 @@ bool COMMAND_PROCESSOR::ExecutePacketType3Draw( if (draw_succeeded) { auto viz_query = register_file_->Get(); - bool viz_query_active = - viz_query.viz_query_ena && viz_query.kill_pix_post_hi_z; - if (!viz_query_active || SupportsGuestOcclusionQueries()) { + if (!(viz_query.viz_query_ena && viz_query.kill_pix_post_hi_z)) { // TODO(Triang3l): Don't drop the draw call completely if the vertex // shader has memexport. // TODO(Triang3l || JoelLinn): Handle this properly in the render diff --git a/src/xenia/gpu/vulkan/vulkan_command_processor.cc b/src/xenia/gpu/vulkan/vulkan_command_processor.cc index 7dd3f971d..f5689d913 100644 --- a/src/xenia/gpu/vulkan/vulkan_command_processor.cc +++ b/src/xenia/gpu/vulkan/vulkan_command_processor.cc @@ -30,7 +30,9 @@ #include "xenia/gpu/vulkan/vulkan_render_target_cache.h" #include "xenia/gpu/vulkan/vulkan_shader.h" #include "xenia/gpu/vulkan/vulkan_shared_memory.h" +#include "xenia/gpu/vulkan/vulkan_zpd_query_pool.h" #include "xenia/gpu/xenos.h" +#include "xenia/gpu/xenos_report_controller.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/user_module.h" #include "xenia/ui/vulkan/vulkan_presenter.h" @@ -38,7 +40,6 @@ DECLARE_bool(clear_memory_page_state); DECLARE_bool(gpu_debug_markers); -DECLARE_bool(occlusion_query_enable); DECLARE_bool(readback_memexport_fast); DECLARE_bool(submit_on_primary_buffer_end); @@ -178,64 +179,6 @@ void VulkanCommandProcessor::InitializeShaderStorage( void VulkanCommandProcessor::RestoreEdramSnapshot(const void* snapshot) {} -void VulkanCommandProcessor::PrepareForWait() { - CheckSubmissionCompletionAndDeviceLoss(GetCompletedSubmission()); - CommandProcessor::PrepareForWait(); -} - -void VulkanCommandProcessor::ReturnFromWait() { - CheckSubmissionCompletionAndDeviceLoss(GetCompletedSubmission()); - CommandProcessor::ReturnFromWait(); -} - -bool VulkanCommandProcessor::ExecutePacketType3_EVENT_WRITE_ZPD( - uint32_t packet, uint32_t count) { - if (!cvars::occlusion_query_enable || !occlusion_query_resources_available_) { - return CommandProcessor::ExecutePacketType3_EVENT_WRITE_ZPD(packet, count); - } - - const uint32_t kQueryFinished = xe::byte_swap(0xFFFFFEED); - assert_true(count == 1); - uint32_t initiator = reader_.ReadAndSwap(); - VulkanCommandProcessor::WriteEventInitiator(initiator & 0x3F); - - uint32_t sample_count_addr = - register_file_->values[XE_GPU_REG_RB_SAMPLE_COUNT_ADDR]; - auto* sample_counts = - memory_->TranslatePhysical( - sample_count_addr); - if (!sample_counts) { - DisableHostOcclusionQueries(); - return CommandProcessor::ExecutePacketType3_EVENT_WRITE_ZPD(packet, count); - } - - bool is_end_via_z_pass = sample_counts->ZPass_A == kQueryFinished && - sample_counts->ZPass_B == kQueryFinished; - bool is_end_via_z_fail = sample_counts->ZFail_A == kQueryFinished && - sample_counts->ZFail_B == kQueryFinished; - bool is_end = is_end_via_z_pass || is_end_via_z_fail; - - if (!is_end) { - if (!BeginGuestOcclusionQuery(sample_count_addr)) { - DisableHostOcclusionQueries(); - return CommandProcessor::ExecutePacketType3_EVENT_WRITE_ZPD(packet, - count); - } - // Don't clear sample_counts here - the query is async and games may poll it - return true; - } - - // Clear before writing end results - std::memset(sample_counts, 0, sizeof(xenos::xe_gpu_depth_sample_counts)); - - if (!EndGuestOcclusionQuery(sample_count_addr)) { - DisableHostOcclusionQueries(); - return CommandProcessor::ExecutePacketType3_EVENT_WRITE_ZPD(packet, count); - } - - return true; -} - std::string VulkanCommandProcessor::GetWindowTitleText() const { std::ostringstream title; title << "Vulkan"; @@ -522,6 +465,10 @@ bool VulkanCommandProcessor::SetupContext() { return false; } + // Needed by NormalizeSampleCount. + zpd_draw_resolution_scale_x_ = draw_resolution_scale_x; + zpd_draw_resolution_scale_y_ = draw_resolution_scale_y; + // Shared memory and EDRAM common bindings. VkDescriptorPoolSize descriptor_pool_sizes[1]; descriptor_pool_sizes[0].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; @@ -1333,7 +1280,9 @@ bool VulkanCommandProcessor::SetupContext() { resolve_downscale_descriptor_set_layout_); } - occlusion_query_resources_available_ = InitializeOcclusionQueryResources(); + // Initialize the ZPD occlusion query pool and resources. + zpd_host_query_pool_ = std::make_unique(); + EnsureZPDQueryResources(); // Just not to expose uninitialized memory. std::memset(&system_constants_, 0, sizeof(system_constants_)); @@ -1344,7 +1293,8 @@ bool VulkanCommandProcessor::SetupContext() { void VulkanCommandProcessor::ShutdownContext() { AwaitAllQueueOperationsCompletion(); - ShutdownOcclusionQueryResources(); + ShutdownZPDQueryResources(); + zpd_host_query_pool_.reset(); const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice(); const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); @@ -2245,9 +2195,12 @@ void VulkanCommandProcessor::IssueSwap(uint32_t frontbuffer_ptr, } void VulkanCommandProcessor::OnPrimaryBufferEnd() { + // Pump any completed resolves now since the guest is likely about to poll. + PumpQueryResolves(); + PumpPendingRetire(); + if (cvars::submit_on_primary_buffer_end && submission_open_ && - !scratch_buffer_used_ && !active_occlusion_query_.valid && - CanEndSubmissionImmediately()) { + !scratch_buffer_used_ && CanEndSubmissionImmediately()) { EndSubmission(false); } } @@ -2519,6 +2472,9 @@ void VulkanCommandProcessor::SubmitBarriersAndEnterRenderTargetCacheRenderPass( VK_SUBPASS_CONTENTS_INLINE); } in_render_pass_ = true; + + // Resume any pending ZPD segment now that the pass is open. + OpenQuerySegment(false); } void VulkanCommandProcessor::SubmitBarriersAndEnterRenderTargetCacheRenderPass( @@ -2623,6 +2579,8 @@ void VulkanCommandProcessor::SubmitBarriersAndEnterRenderTargetCacheRenderPass( VK_SUBPASS_CONTENTS_INLINE); } in_render_pass_ = true; + + OpenQuerySegment(false); } void VulkanCommandProcessor::EndRenderPass() { @@ -2630,6 +2588,14 @@ void VulkanCommandProcessor::EndRenderPass() { if (!in_render_pass_) { return; } + // Close any active segment before ending the pass. vkCmdBeginQuery is only + // valid inside a render pass. segment_pending_begin reopens at the next one. + if (GetZPDMode() != ZPDMode::kFake && zpd_active_segment_.segment_active) { + CloseQuerySegment(); + if (zpd_active_segment_.logical_active) { + zpd_active_segment_.segment_pending_begin = true; + } + } // Use current_render_pass_ to determine which end command to use. // VK_NULL_HANDLE means we used dynamic rendering, otherwise traditional. if (current_render_pass_ == VK_NULL_HANDLE) { @@ -4708,297 +4674,193 @@ VkBuffer VulkanCommandProcessor::RequestReadbackBuffer(uint32_t size) { return memexport_readback_buffer_; } -bool VulkanCommandProcessor::InitializeOcclusionQueryResources() { - ShutdownOcclusionQueryResources(); - - const ui::vulkan::VulkanDevice* const vulkan_device = GetVulkanDevice(); - if (!vulkan_device) { - return false; - } - const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); - const VkDevice device = vulkan_device->device(); - - VkQueryPoolCreateInfo pool_info; - pool_info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; - pool_info.pNext = nullptr; - pool_info.flags = 0; - pool_info.queryType = VK_QUERY_TYPE_OCCLUSION; - pool_info.queryCount = kMaxOcclusionQueries; - pool_info.pipelineStatistics = 0; - if (dfn.vkCreateQueryPool(device, &pool_info, nullptr, - &occlusion_query_pool_) != VK_SUCCESS) { - XELOGW( - "VulkanCommandProcessor: Failed to create the occlusion query pool, " - "falling back to fake sample counts."); - return false; +void VulkanCommandProcessor::EnsureZPDQueryResources() { + if (GetZPDMode() == ZPDMode::kFake) { + return; } - VkBufferCreateInfo buffer_info; - buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; - buffer_info.pNext = nullptr; - buffer_info.flags = 0; - buffer_info.size = sizeof(uint64_t) * kMaxOcclusionQueries; - buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; - buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - buffer_info.queueFamilyIndexCount = 0; - buffer_info.pQueueFamilyIndices = nullptr; - if (dfn.vkCreateBuffer(device, &buffer_info, nullptr, - &occlusion_query_readback_buffer_) != VK_SUCCESS) { - XELOGW( - "VulkanCommandProcessor: Failed to create the occlusion query " - "readback buffer, falling back to fake sample counts."); - ShutdownOcclusionQueryResources(); - return false; - } - - VkMemoryRequirements memory_requirements; - dfn.vkGetBufferMemoryRequirements(device, occlusion_query_readback_buffer_, - &memory_requirements); - uint32_t memory_type = ui::vulkan::util::ChooseMemoryType( - vulkan_device->memory_types(), memory_requirements.memoryTypeBits, - ui::vulkan::util::MemoryPurpose::kReadback); - if (memory_type == UINT32_MAX) { - XELOGW( - "VulkanCommandProcessor: Failed to find a memory type for occlusion " - "query readback, falling back to fake sample counts."); - ShutdownOcclusionQueryResources(); - return false; - } - - VkMemoryAllocateInfo allocate_info; - allocate_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - allocate_info.pNext = nullptr; - allocate_info.allocationSize = memory_requirements.size; - allocate_info.memoryTypeIndex = memory_type; - if (dfn.vkAllocateMemory(device, &allocate_info, nullptr, - &occlusion_query_readback_memory_) != VK_SUCCESS) { - XELOGW( - "VulkanCommandProcessor: Failed to allocate occlusion query readback " - "memory, falling back to fake sample counts."); - ShutdownOcclusionQueryResources(); - return false; - } - - if (dfn.vkBindBufferMemory(device, occlusion_query_readback_buffer_, - occlusion_query_readback_memory_, - 0) != VK_SUCCESS) { - XELOGW( - "VulkanCommandProcessor: Failed to bind occlusion query readback " - "memory."); - ShutdownOcclusionQueryResources(); - return false; - } - - if (dfn.vkMapMemory( - device, occlusion_query_readback_memory_, 0, VK_WHOLE_SIZE, 0, - reinterpret_cast(&occlusion_query_readback_mapping_)) != - VK_SUCCESS) { - XELOGW( - "VulkanCommandProcessor: Failed to map occlusion query readback " - "memory."); - ShutdownOcclusionQueryResources(); - return false; - } - - occlusion_query_cursor_ = 0; - pending_occlusion_queries_.clear(); - active_occlusion_query_ = {}; - occlusion_query_resources_available_ = true; - return true; + bool can_recreate = !zpd_active_segment_.logical_active && + !zpd_active_segment_.segment_active && + !zpd_host_query_pool_->has_pending_resolve_batch() && + zpd_resolves_in_flight_.empty(); + zpd_host_query_pool_->EnsureInitialized(GetVulkanDevice(), + kZPDQueryPoolCapacity, can_recreate); } -void VulkanCommandProcessor::ShutdownOcclusionQueryResources() { - // Safely disable queries (ends any active query) - DisableHostOcclusionQueries(); +bool VulkanCommandProcessor::CanOpenZPDQuery() const { return in_render_pass_; } - const ui::vulkan::VulkanDevice* vulkan_device = GetVulkanDevice(); - if (occlusion_query_readback_mapping_ && - occlusion_query_readback_memory_ != VK_NULL_HANDLE && vulkan_device) { - vulkan_device->functions().vkUnmapMemory(vulkan_device->device(), - occlusion_query_readback_memory_); +CommandProcessor::QueryOpenResult VulkanCommandProcessor::OpenZPDQuery( + uint32_t& out_host_index, uint32_t& out_host_generation, + bool can_close_submission) { + if (!BeginSubmission(true)) { + return QueryOpenResult::kFailed; } - occlusion_query_readback_mapping_ = nullptr; - if (occlusion_query_readback_buffer_ != VK_NULL_HANDLE && vulkan_device) { - vulkan_device->functions().vkDestroyBuffer( - vulkan_device->device(), occlusion_query_readback_buffer_, nullptr); - } - occlusion_query_readback_buffer_ = VK_NULL_HANDLE; - if (occlusion_query_readback_memory_ != VK_NULL_HANDLE && vulkan_device) { - vulkan_device->functions().vkFreeMemory( - vulkan_device->device(), occlusion_query_readback_memory_, nullptr); - } - occlusion_query_readback_memory_ = VK_NULL_HANDLE; - if (occlusion_query_pool_ != VK_NULL_HANDLE && vulkan_device) { - vulkan_device->functions().vkDestroyQueryPool( - vulkan_device->device(), occlusion_query_pool_, nullptr); - } - occlusion_query_pool_ = VK_NULL_HANDLE; -} -void VulkanCommandProcessor::DisableHostOcclusionQueries() { - // End any active query first to avoid Vulkan validation errors - if (active_occlusion_query_.valid && - occlusion_query_pool_ != VK_NULL_HANDLE) { - if (BeginSubmission(true)) { - DeferredCommandBuffer& command_buffer = deferred_command_buffer(); - command_buffer.CmdVkEndQuery(occlusion_query_pool_, - active_occlusion_query_.host_index); - // Don't copy results - we're abandoning the result - EndSubmission(false); + if (!in_render_pass_) { + // BeginSubmission tends to reset the render pass state, so defer + // until the normal draw path catches up. + return QueryOpenResult::kDeferred; + } + + bool retried_after_submission_flip = false; + while (true) { + bool is_pool_exhausted = !zpd_host_query_pool_->has_free_indices(); + + if (is_pool_exhausted) { + DrainQueryResolves(GetCompletedSubmission()); + is_pool_exhausted = !zpd_host_query_pool_->has_free_indices(); } + + if (is_pool_exhausted && GetZPDMode() == ZPDMode::kFast) { + return QueryOpenResult::kPoolExhausted; + } + + uint64_t wait_for = 0; + + if (is_pool_exhausted) { + if (!zpd_resolves_in_flight_.empty()) { + wait_for = zpd_resolves_in_flight_.front().submission; + } + } + + if (wait_for == 0) { + break; + } + + // Only flip the submission once. If the pool is still exhausted after + // draining, defer and let the caller try again next draw. + if (submission_open_ && wait_for == GetCurrentSubmission()) { + if (retried_after_submission_flip || !can_close_submission || + !CanEndSubmissionImmediately()) { + return QueryOpenResult::kDeferred; + } + + VkRenderPass saved_render_pass = current_render_pass_; + const VulkanRenderTargetCache::Framebuffer* saved_framebuffer = + current_framebuffer_; + EndRenderPass(); + if (!EndSubmission(false)) { + return QueryOpenResult::kFailed; + } + if (!BeginSubmission(true)) { + return QueryOpenResult::kFailed; + } + + SubmitBarriersAndEnterRenderTargetCacheRenderPass(saved_render_pass, + saved_framebuffer); + if (!in_render_pass_) { + return QueryOpenResult::kDeferred; + } + + retried_after_submission_flip = true; + continue; + } + + uint64_t completed_submission = GetCompletedSubmission(); + + if (wait_for > completed_submission) { + if (cvars::occlusion_query_log) { + XELOGI("ZPD: Stall awaiting submission={} completed_before={}", + wait_for, completed_submission); + } + + completion_timeline_.AwaitSubmissionAndUpdateCompleted(wait_for); + DrainQueryResolves(GetCompletedSubmission()); + } + + break; } - occlusion_query_resources_available_ = false; - active_occlusion_query_ = {}; - pending_occlusion_queries_.clear(); - occlusion_query_cursor_ = 0; + + if (!in_render_pass_) { + return QueryOpenResult::kDeferred; + } + + if (!zpd_host_query_pool_->AcquireQueryIndex(out_host_index, + out_host_generation)) { + return QueryOpenResult::kFailed; + } + + zpd_host_query_pool_->BeginQuery(deferred_command_buffer_, out_host_index); + return QueryOpenResult::kOpened; } -bool VulkanCommandProcessor::AcquireOcclusionQueryIndex( - uint32_t& host_index_out) { - if (occlusion_query_cursor_ >= kMaxOcclusionQueries) { - // Reset cursor - all queries complete synchronously now - occlusion_query_cursor_ = 0; +bool VulkanCommandProcessor::CloseZPDQuery(uint32_t host_index, + uint32_t /*unused*/, + uint64_t& out_submission) { + if (!in_render_pass_) { + XELOGW("ZPD: Split segment requested outside render pass"); + return false; } - host_index_out = occlusion_query_cursor_++; + + zpd_host_query_pool_->EndQuery(deferred_command_buffer_, host_index); + zpd_host_query_pool_->QueueQueryResolve(host_index); + out_submission = GetCurrentSubmission(); return true; } -bool VulkanCommandProcessor::BeginGuestOcclusionQuery( - uint32_t sample_count_address) { - if (!cvars::occlusion_query_enable || !occlusion_query_resources_available_ || - occlusion_query_pool_ == VK_NULL_HANDLE || - occlusion_query_readback_mapping_ == nullptr) { - return false; +bool VulkanCommandProcessor::DiscardZPDQuery(uint32_t host_index, + uint32_t host_generation) { + // vkCmdEndQuery is invalid outside a render pass. + // CPU reset via vkResetQueryPool is enough. + if (!in_render_pass_) { + XELOGW("ZPD: Discard segment requested outside render pass"); + zpd_host_query_pool_->ReleaseQueryIndex(host_index, host_generation); + return true; } - if (active_occlusion_query_.valid) { - XELOGW( - "VulkanCommandProcessor: Occlusion query begin issued while another " - "query is active, disabling hardware queries"); - DisableHostOcclusionQueries(); - return false; - } - uint32_t host_index = 0; - if (!AcquireOcclusionQueryIndex(host_index)) { - return false; - } - if (!BeginSubmission(true)) { - return false; - } - DeferredCommandBuffer& command_buffer = deferred_command_buffer(); - command_buffer.CmdVkResetQueryPool(occlusion_query_pool_, host_index, 1); - command_buffer.CmdVkBeginQuery(occlusion_query_pool_, host_index, 0); - active_occlusion_query_.sample_count_address = sample_count_address; - active_occlusion_query_.host_index = host_index; - active_occlusion_query_.valid = true; + + // Inside a render pass, EndQuery must be issued before releasing the slot. + zpd_host_query_pool_->EndQuery(deferred_command_buffer_, host_index); + zpd_host_query_pool_->ReleaseQueryIndex(host_index, host_generation); return true; } -bool VulkanCommandProcessor::EndGuestOcclusionQuery( - uint32_t sample_count_address) { - if (!cvars::occlusion_query_enable || !occlusion_query_resources_available_ || - !active_occlusion_query_.valid || - occlusion_query_pool_ == VK_NULL_HANDLE || - occlusion_query_readback_mapping_ == nullptr) { - return false; - } - - const uint32_t host_index = active_occlusion_query_.host_index; - - // Mark as invalid BEFORE ending to prevent restart in BeginSubmission - active_occlusion_query_.valid = false; - - if (!BeginSubmission(true)) { - return false; - } - - DeferredCommandBuffer& command_buffer = deferred_command_buffer(); - command_buffer.CmdVkEndQuery(occlusion_query_pool_, host_index); - InsertDebugMarker("Occlusion Query Readback: index %u", host_index); - command_buffer.CmdVkCopyQueryPoolResults( - occlusion_query_pool_, host_index, 1, occlusion_query_readback_buffer_, - sizeof(uint64_t) * host_index, sizeof(uint64_t), - VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); - - // Force submission and wait for GPU to complete the query synchronously - if (!EndSubmission(false)) { - return false; - } - - // Wait for the GPU to complete this query - if (!AwaitAllQueueOperationsCompletion()) { - return false; - } - - // Read the result immediately from persistently mapped memory - const uint64_t* results = - reinterpret_cast(occlusion_query_readback_mapping_); - uint64_t samples = results[host_index]; - - samples = NormalizeOcclusionSamples(samples); - WriteGuestOcclusionResult(sample_count_address, samples); - - return true; +CommandProcessor::ZPDSubmissionBridge* +VulkanCommandProcessor::GetZPDSubmissionBridge() { + return &zpd_submission_bridge_; } -uint64_t VulkanCommandProcessor::NormalizeOcclusionSamples( - uint64_t samples) const { - if (samples == 0 || !texture_cache_) { - return samples; - } - uint64_t scale_x = texture_cache_->draw_resolution_scale_x(); - uint64_t scale_y = texture_cache_->draw_resolution_scale_y(); - uint64_t scale = scale_x * scale_y; - if (scale <= 1) { - return samples; - } - return (samples + (scale >> 1)) / scale; +CommandProcessor::ZPDSubmissionState +VulkanCommandProcessor::ZPDSubmissionBridge::GetState() const { + return {command_processor_.GetCurrentSubmission(), + command_processor_.GetCompletedSubmission()}; } -void VulkanCommandProcessor::WriteGuestOcclusionResult( - uint32_t sample_count_address, uint64_t samples) { - auto* sample_counts = - memory_->TranslatePhysical( - sample_count_address); - if (!sample_counts) { - return; +void VulkanCommandProcessor::ZPDSubmissionBridge::PrepareReadback( + uint64_t completed_submission) { + // Invalidate CPU cache before reading results on non-coherent memory. + if (!command_processor_.zpd_resolves_in_flight_.empty() && + command_processor_.zpd_resolves_in_flight_.front().submission <= + completed_submission) { + command_processor_.zpd_host_query_pool_->InvalidateReadback(); } - uint32_t clamped = - samples > uint64_t(UINT32_MAX) ? UINT32_MAX : uint32_t(samples); - sample_counts->Total_A = clamped; - sample_counts->Total_B = 0; - sample_counts->ZPass_A = clamped; - sample_counts->ZPass_B = 0; - sample_counts->ZFail_A = 0; - sample_counts->ZFail_B = 0; - sample_counts->StencilFail_A = 0; - sample_counts->StencilFail_B = 0; } -void VulkanCommandProcessor::ProcessReadyOcclusionQueries( - uint64_t completed_submission_hint) { - if (!occlusion_query_resources_available_ || - pending_occlusion_queries_.empty() || - occlusion_query_readback_mapping_ == nullptr) { - return; +bool VulkanCommandProcessor::ZPDSubmissionBridge::EnsureProgress() { + if (!command_processor_.submission_open_) { + return false; } - uint64_t completed_submission = completed_submission_hint; - if (completed_submission == UINT64_MAX) { - completed_submission = GetCompletedSubmission(); + + if (!command_processor_.CanEndSubmissionImmediately() && + !command_processor_.pipeline_cache_->IsCreatingPipelines()) { + return false; } - if (pending_occlusion_queries_.front().submission > completed_submission) { - return; - } - const uint64_t* results = - reinterpret_cast(occlusion_query_readback_mapping_); - while (!pending_occlusion_queries_.empty() && - pending_occlusion_queries_.front().submission <= - completed_submission) { - PendingOcclusionQuery query = pending_occlusion_queries_.front(); - pending_occlusion_queries_.pop_front(); - uint64_t samples = results[query.host_index]; - samples = NormalizeOcclusionSamples(samples); - WriteGuestOcclusionResult(query.sample_count_address, samples); + if (!command_processor_.CanEndSubmissionImmediately()) { + if (cvars::occlusion_query_log) { + XELOGI( + "ZPD/Async: Draining Vulkan async pipeline creation for strict " + "retirement"); + } + command_processor_.pipeline_cache_->AwaitPipelineCompletion(); } + + command_processor_.EndRenderPass(); + return command_processor_.EndSubmission(false); +} + +void VulkanCommandProcessor::ZPDSubmissionBridge::AwaitSubmission( + uint64_t submission) { + command_processor_.completion_timeline_.AwaitSubmissionAndUpdateCompleted( + submission); } void VulkanCommandProcessor::InitializeTrace() { @@ -5081,7 +4943,7 @@ void VulkanCommandProcessor::CheckSubmissionCompletionAndDeviceLoss( resolve_downscale_descriptor_pool_chain_->Reclaim(completed_submission); } - ProcessReadyOcclusionQueries(completed_submission); + DrainQueryResolves(completed_submission); // Destroy objects scheduled for destruction. const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device->functions(); @@ -5208,6 +5070,26 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) { if (is_opening_frame) { frame_open_ = true; + // Log guest ZPD report stats every 100 frames. + if (GetZPDMode() != ZPDMode::kFake && cvars::occlusion_query_log && + zpd_host_query_pool_ && zpd_host_query_pool_->capacity() && + zpd_report_controller_ && + frame_current_ - zpd_stats_.last_log_frame >= 100) { + XenosReportController::Stats report_stats = + zpd_report_controller_->stats(); + XELOGI( + "Occlusion Query Stats (last 100 frames): " + "LogicalBegun={}, LogicalEnded={}, SegBegun={}, SegEnded={}, " + "WritesRetired={}, PoolExhausted={}, Failed={}", + zpd_stats_.logical_begun, zpd_stats_.logical_ended, + zpd_stats_.segments_begun, zpd_stats_.segments_ended, + report_stats.writes_retired, zpd_stats_.pool_exhausted, + zpd_stats_.failed); + + zpd_report_controller_->ResetStats(); + zpd_stats_.Reset(frame_current_); + } + // Reset bindings that depend on transient data. std::memset(current_float_constant_map_vertex_, 0, sizeof(current_float_constant_map_vertex_)); @@ -5278,7 +5160,7 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) { return true; } -bool VulkanCommandProcessor::CanEndSubmissionImmediately() { +bool VulkanCommandProcessor::CanEndSubmissionImmediately() const { return !submission_open_ || !pipeline_cache_ || !pipeline_cache_->IsCreatingPipelines(); } @@ -5405,16 +5287,8 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) { sparse_buffer_binds_.clear(); sparse_memory_binds_.clear(); } - - // End any active occlusion query before closing the command buffer - // Vulkan requires BeginQuery/EndQuery to be within the same command buffer - // This should never happen in synchronous mode - log a warning - if (active_occlusion_query_.valid && occlusion_query_resources_available_) { - XELOGW( - "VulkanCommandProcessor: EndSubmission called with active occlusion " - "query - disabling hardware queries"); - DisableHostOcclusionQueries(); - } + // Can't cross command buffer boundaries. Close the active segment first. + CloseQuerySegment(); SubmitBarriers(true); @@ -5437,6 +5311,11 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) { return false; } deferred_command_buffer_.Execute(command_buffer.buffer); + + // Record ZPD resolves before submitting. + if (zpd_host_query_pool_) { + zpd_host_query_pool_->RecordResolveBatch(command_buffer.buffer); + } if (dfn.vkEndCommandBuffer(command_buffer.buffer) != VK_SUCCESS) { XELOGE("Failed to end a Vulkan command buffer"); return false; @@ -5494,6 +5373,11 @@ bool VulkanCommandProcessor::EndSubmission(bool is_swap) { } submission_open_ = false; + + // Process any ZPD resolves that completed with this submission. + // Block if strict mode has a pending result waiting on the guest sentinel. + PumpQueryResolves(); + PumpPendingRetire(); } if (is_closing_frame) { @@ -6927,9 +6811,7 @@ uint32_t VulkanCommandProcessor::WriteTransientTextureBindings( } #define COMMAND_PROCESSOR VulkanCommandProcessor -#define XE_GPU_OVERRIDES_EVENT_WRITE_ZPD #include "../pm4_command_processor_implement.h" -#undef XE_GPU_OVERRIDES_EVENT_WRITE_ZPD #undef COMMAND_PROCESSOR } // namespace vulkan } // namespace gpu diff --git a/src/xenia/gpu/vulkan/vulkan_command_processor.h b/src/xenia/gpu/vulkan/vulkan_command_processor.h index 427118e1d..d7b086707 100644 --- a/src/xenia/gpu/vulkan/vulkan_command_processor.h +++ b/src/xenia/gpu/vulkan/vulkan_command_processor.h @@ -25,6 +25,7 @@ #include "xenia/base/hash.h" #include "xenia/gpu/command_processor.h" #include "xenia/gpu/draw_util.h" +#include "xenia/gpu/gpu_flags.h" #include "xenia/gpu/registers.h" #include "xenia/gpu/spirv_shader_translator.h" #include "xenia/gpu/vulkan/deferred_command_buffer.h" @@ -35,6 +36,7 @@ #include "xenia/gpu/vulkan/vulkan_shader.h" #include "xenia/gpu/vulkan/vulkan_shared_memory.h" #include "xenia/gpu/vulkan/vulkan_texture_cache.h" +#include "xenia/gpu/vulkan/vulkan_zpd_query_pool.h" #include "xenia/gpu/xenos.h" #include "xenia/kernel/kernel_state.h" #include "xenia/ui/vulkan/linked_type_descriptor_set_allocator.h" @@ -154,12 +156,6 @@ class VulkanCommandProcessor final : public CommandProcessor { void RestoreEdramSnapshot(const void* snapshot) override; - void PrepareForWait() override; - void ReturnFromWait() override; - bool SupportsGuestOcclusionQueries() const override { - return occlusion_query_resources_available_; - } - ui::vulkan::VulkanDevice* GetVulkanDevice() const { return static_cast( graphics_system_->provider()) @@ -448,7 +444,7 @@ class VulkanCommandProcessor final : public CommandProcessor { // Checks if ending a submission right now would not cause potentially more // delay than it would reduce - such as when there are unfinished graphics // pipeline creation requests. - bool CanEndSubmissionImmediately(); + bool CanEndSubmissionImmediately() const; bool AwaitAllQueueOperationsCompletion() { CheckSubmissionCompletionAndDeviceLoss(GetCurrentSubmission()); return !submission_open_ && @@ -464,17 +460,56 @@ class VulkanCommandProcessor final : public CommandProcessor { void DestroyScratchBuffer(); - void ProcessReadyOcclusionQueries( - uint64_t completed_submission_hint = UINT64_MAX); - bool InitializeOcclusionQueryResources(); - void ShutdownOcclusionQueryResources(); - bool BeginGuestOcclusionQuery(uint32_t sample_count_address); - bool EndGuestOcclusionQuery(uint32_t sample_count_address); - bool AcquireOcclusionQueryIndex(uint32_t& host_index_out); - void DisableHostOcclusionQueries(); - uint64_t NormalizeOcclusionSamples(uint64_t samples) const; - void WriteGuestOcclusionResult(uint32_t sample_count_address, - uint64_t samples); + // ZPD occlusion queries backend. + // vkCmdBeginQuery is only valid inside a render pass, so segments split at + // pass end and resume at the next pass begin. If BEGIN fires outside a pass, + // segment_pending_begin waits for the next. Outside a render pass, + // DiscardZPDQuery can skip EndQuery and just reset the slot via + // vkResetQueryPool. Inside a pass, EndQuery is required. + // Strict ZPD retirement may need to end the pass before blocking. + void EnsureZPDQueryResources() override; + void ShutdownZPDQueryResources() override { + zpd_host_query_pool_->Shutdown(); + } + + bool IsZPDQueryPoolReady() const override { + return zpd_host_query_pool_->is_initialized(); + } + bool CanOpenZPDQuery() const override; + + QueryOpenResult OpenZPDQuery(uint32_t& out_host_index, + uint32_t& out_host_generation, + bool can_close_submission) override; + bool CloseZPDQuery(uint32_t host_index, uint32_t host_generation, + uint64_t& out_submission) override; + bool DiscardZPDQuery(uint32_t host_index, uint32_t host_generation) override; + uint64_t GetZPDQueryResult(uint32_t host_index) override { + return zpd_host_query_pool_->GetQueryReadbackValue(host_index); + } + void ReleaseZPDQuery(uint32_t host_index, uint32_t host_generation) override { + zpd_host_query_pool_->ReleaseQueryIndex(host_index, host_generation); + } + bool IsZPDQueryResultValid(uint32_t host_index, + uint32_t host_generation) const override { + return zpd_host_query_pool_->GenerationMatches(host_index, host_generation); + } + + CommandProcessor::ZPDSubmissionBridge* GetZPDSubmissionBridge() override; + + class ZPDSubmissionBridge final + : public CommandProcessor::ZPDSubmissionBridge { + public: + explicit ZPDSubmissionBridge(VulkanCommandProcessor& command_processor) + : command_processor_(command_processor) {} + + CommandProcessor::ZPDSubmissionState GetState() const override; + void PrepareReadback(uint64_t completed_submission) override; + bool EnsureProgress() override; + void AwaitSubmission(uint64_t submission) override; + + private: + VulkanCommandProcessor& command_processor_; + }; void UpdateDynamicState(const draw_util::ViewportInfo& viewport_info, bool primitive_polygonal, @@ -516,6 +551,8 @@ class VulkanCommandProcessor final : public CommandProcessor { std::vector semaphores_free_; + ZPDSubmissionBridge zpd_submission_bridge_{*this}; + ui::vulkan::VulkanGPUCompletionTimeline completion_timeline_; bool submission_open_ = false; // In case vkQueueSubmit fails after something like a successful @@ -613,6 +650,8 @@ class VulkanCommandProcessor final : public CommandProcessor { std::unique_ptr render_target_cache_; + std::unique_ptr zpd_host_query_pool_; + std::unique_ptr pipeline_cache_; std::unique_ptr texture_cache_; @@ -887,25 +926,6 @@ class VulkanCommandProcessor final : public CommandProcessor { // Per-memexport double-buffered readback for fast mode (delayed sync) std::unordered_map memexport_readback_buffers_; - // Occlusion query support. - VkQueryPool occlusion_query_pool_ = VK_NULL_HANDLE; - VkBuffer occlusion_query_readback_buffer_ = VK_NULL_HANDLE; - VkDeviceMemory occlusion_query_readback_memory_ = VK_NULL_HANDLE; - uint8_t* occlusion_query_readback_mapping_ = nullptr; - uint32_t occlusion_query_cursor_ = 0; - bool occlusion_query_resources_available_ = false; - struct ActiveOcclusionQuery { - uint32_t sample_count_address = 0; - uint32_t host_index = UINT32_MAX; - bool valid = false; - } active_occlusion_query_; - struct PendingOcclusionQuery { - uint32_t host_index; - uint64_t submission; - uint32_t sample_count_address; - }; - std::deque pending_occlusion_queries_; - // Debug marker support for RenderDoc/debug tools. bool debug_markers_enabled_ = false; void UpdateDebugMarkersEnabled(); diff --git a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc index fbf30c214..00d974201 100644 --- a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc +++ b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.cc @@ -803,6 +803,28 @@ bool VulkanPipelineCache::IsCreatingPipelines() { return !creation_queue_.empty() || creation_threads_busy_ != 0; } +void VulkanPipelineCache::AwaitPipelineCompletion() { + if (creation_threads_.empty()) { + return; + } + + bool await_creation_completion_event; + { + std::lock_guard lock(creation_request_lock_); + await_creation_completion_event = + !creation_queue_.empty() || creation_threads_busy_ != 0; + if (await_creation_completion_event) { + creation_completion_event_->Reset(); + creation_completion_set_event_.store(true, std::memory_order_release); + } + } + + if (await_creation_completion_event) { + creation_request_cond_.notify_one(); + xe::threading::Wait(creation_completion_event_.get(), false); + } +} + void VulkanPipelineCache::CreationThread() { for (;;) { PipelineCreationArguments creation_arguments; diff --git a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.h b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.h index 1366296c5..aa18f4483 100644 --- a/src/xenia/gpu/vulkan/vulkan_pipeline_cache.h +++ b/src/xenia/gpu/vulkan/vulkan_pipeline_cache.h @@ -118,6 +118,10 @@ class VulkanPipelineCache { void EndSubmission(); bool IsCreatingPipelines(); + // Waits for any pipeline creation needed by the current draw path to finish + // before state is consumed. This was added so strict ZPD query paths stop + // racing pipeline compilation and then blocking work on incomplete state. + void AwaitPipelineCompletion(); VulkanShader* LoadShader(xenos::ShaderType shader_type, const uint32_t* host_address, uint32_t dword_count); diff --git a/src/xenia/gpu/vulkan/vulkan_zpd_query_pool.cc b/src/xenia/gpu/vulkan/vulkan_zpd_query_pool.cc new file mode 100644 index 000000000..d48841de4 --- /dev/null +++ b/src/xenia/gpu/vulkan/vulkan_zpd_query_pool.cc @@ -0,0 +1,397 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/gpu/vulkan/vulkan_zpd_query_pool.h" + +#include + +#include "xenia/base/logging.h" +#include "xenia/gpu/vulkan/deferred_command_buffer.h" +#include "xenia/ui/vulkan/vulkan_device.h" +#include "xenia/ui/vulkan/vulkan_util.h" + +namespace xe { +namespace gpu { +namespace vulkan { +namespace { + +struct ResolveRange { + uint32_t start; + uint32_t count; +}; +} // namespace + +bool VulkanZPDQueryPool::EnsureInitialized( + const ui::vulkan::VulkanDevice* vulkan_device, uint32_t requested_capacity, + bool can_recreate) { + vulkan_device_ = vulkan_device; + if (!vulkan_device_) { + return false; + } + + if (is_initialized() && (capacity_ == requested_capacity || !can_recreate)) { + return true; + } + + Shutdown(); + + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions(); + const VkDevice device = vulkan_device_->device(); + + // Need VK_EXT_host_query_reset (1.2 core) to reset slots on the CPU without + // a paired vkCmdEndQuery. May be unavailable on older drivers or devices. + if (!vulkan_device_->properties().hostQueryReset || + dfn.vkResetQueryPool == nullptr) { + return false; + } + + VkQueryPoolCreateInfo pool_info; + pool_info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO; + pool_info.pNext = nullptr; + pool_info.flags = 0; + pool_info.queryType = VK_QUERY_TYPE_OCCLUSION; + pool_info.queryCount = requested_capacity; + pool_info.pipelineStatistics = 0; + if (dfn.vkCreateQueryPool(device, &pool_info, nullptr, &query_pool_) != + VK_SUCCESS) { + XELOGW( + "VulkanZPDQueryPool: Failed to create the ZPD query " + "pool, falling back to fake sample counts."); + Shutdown(); + return false; + } + + VkBufferCreateInfo readback_buffer_info; + readback_buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + readback_buffer_info.pNext = nullptr; + readback_buffer_info.flags = 0; + readback_buffer_info.size = sizeof(uint64_t) * requested_capacity; + readback_buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; + readback_buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + readback_buffer_info.queueFamilyIndexCount = 0; + readback_buffer_info.pQueueFamilyIndices = nullptr; + if (dfn.vkCreateBuffer(device, &readback_buffer_info, nullptr, + &readback_buffer_) != VK_SUCCESS) { + XELOGW( + "VulkanZPDQueryPool: Failed to create the ZPD query " + "readback buffer, falling back to fake sample counts."); + Shutdown(); + return false; + } + + VkMemoryRequirements readback_mem_reqs; + dfn.vkGetBufferMemoryRequirements(device, readback_buffer_, + &readback_mem_reqs); + + VkMemoryAllocateInfo readback_alloc_info; + readback_alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + readback_alloc_info.pNext = nullptr; + readback_alloc_info.allocationSize = readback_mem_reqs.size; + readback_alloc_info.memoryTypeIndex = ui::vulkan::util::ChooseMemoryType( + vulkan_device_->memory_types(), readback_mem_reqs.memoryTypeBits, + ui::vulkan::util::MemoryPurpose::kReadback); + if (readback_alloc_info.memoryTypeIndex == UINT32_MAX || + dfn.vkAllocateMemory(device, &readback_alloc_info, nullptr, + &readback_memory_) != VK_SUCCESS) { + XELOGW( + "VulkanZPDQueryPool: Failed to allocate ZPD query " + "readback memory, falling back to fake sample counts."); + Shutdown(); + return false; + } + + readback_is_coherent_ = (vulkan_device_->memory_types().host_coherent & + (1u << readback_alloc_info.memoryTypeIndex)) != 0; + + if (dfn.vkBindBufferMemory(device, readback_buffer_, readback_memory_, 0) != + VK_SUCCESS) { + XELOGW( + "VulkanZPDQueryPool: Failed to bind ZPD query readback " + "buffer memory, falling back to fake sample counts."); + Shutdown(); + return false; + } + + void* mapping = nullptr; + if (dfn.vkMapMemory(device, readback_memory_, 0, VK_WHOLE_SIZE, 0, + &mapping) != VK_SUCCESS) { + XELOGW( + "VulkanZPDQueryPool: Failed to map ZPD query readback " + "memory, falling back to fake sample counts."); + Shutdown(); + return false; + } + + readback_mapping_ = reinterpret_cast(mapping); + capacity_ = requested_capacity; + + dfn.vkResetQueryPool(device, query_pool_, 0, requested_capacity); + + free_indices_.clear(); + free_indices_.reserve(requested_capacity); + for (uint32_t i = 0; i < requested_capacity; ++i) { + free_indices_.push_back(requested_capacity - 1 - i); + } + index_generations_.assign(requested_capacity, 0); + + resolve_batch_pending_.assign(requested_capacity, 0); + resolve_batch_index_count_ = 0; + + return true; +} + +void VulkanZPDQueryPool::Shutdown() { + if (!vulkan_device_) { + query_pool_ = VK_NULL_HANDLE; + readback_buffer_ = VK_NULL_HANDLE; + readback_memory_ = VK_NULL_HANDLE; + readback_mapping_ = nullptr; + readback_is_coherent_ = true; + capacity_ = 0; + free_indices_.clear(); + index_generations_.clear(); + resolve_batch_pending_.clear(); + resolve_batch_index_count_ = 0; + return; + } + + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions(); + const VkDevice device = vulkan_device_->device(); + + free_indices_.clear(); + index_generations_.clear(); + resolve_batch_pending_.clear(); + resolve_batch_index_count_ = 0; + + capacity_ = 0; + readback_is_coherent_ = true; + + if (readback_mapping_ && readback_memory_ != VK_NULL_HANDLE) { + dfn.vkUnmapMemory(device, readback_memory_); + } + readback_mapping_ = nullptr; + + if (readback_buffer_ != VK_NULL_HANDLE) { + dfn.vkDestroyBuffer(device, readback_buffer_, nullptr); + } + readback_buffer_ = VK_NULL_HANDLE; + + if (readback_memory_ != VK_NULL_HANDLE) { + dfn.vkFreeMemory(device, readback_memory_, nullptr); + } + readback_memory_ = VK_NULL_HANDLE; + + if (query_pool_ != VK_NULL_HANDLE) { + dfn.vkDestroyQueryPool(device, query_pool_, nullptr); + } + query_pool_ = VK_NULL_HANDLE; +} + +bool VulkanZPDQueryPool::AcquireQueryIndex(uint32_t& query_index, + uint32_t& query_generation) { + if (free_indices_.empty()) { + query_index = UINT32_MAX; + query_generation = 0; + return false; + } + + query_index = free_indices_.back(); + free_indices_.pop_back(); + + assert_true(query_index < index_generations_.size()); + // Bump before returning - invalidates in-flight copies from prior occupant. + query_generation = ++index_generations_[query_index]; + return true; +} + +void VulkanZPDQueryPool::ReleaseQueryIndex(uint32_t query_index, + uint32_t query_generation) { + if (!vulkan_device_ || query_index >= capacity_) { + return; + } + + if (!GenerationMatches(query_index, query_generation)) { + XELOGW("VulkanZPDQueryPool: stale release index={} gen={}", query_index, + query_generation); + return; + } + + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions(); + const VkDevice device = vulkan_device_->device(); + + // Immediately reset the slot on the CPU so it's ready for the next + // AcquireQueryIndex without requiring a paired EndQuery. + dfn.vkResetQueryPool(device, query_pool_, query_index, 1); + free_indices_.push_back(query_index); +} + +bool VulkanZPDQueryPool::GenerationMatches(uint32_t query_index, + uint32_t query_generation) const { + return query_index < index_generations_.size() && + index_generations_[query_index] == query_generation; +} + +void VulkanZPDQueryPool::BeginQuery( + DeferredCommandBuffer& deferred_command_buffer, + uint32_t query_index) const { + if (query_pool_ == VK_NULL_HANDLE || query_index >= capacity_) { + return; + } + + // Precise bit is crucial. Most titles tested actually care about the sample + // counts, not just 0 vs non-zero. + deferred_command_buffer.CmdVkBeginQuery(query_pool_, query_index, + VK_QUERY_CONTROL_PRECISE_BIT); +} + +void VulkanZPDQueryPool::EndQuery( + DeferredCommandBuffer& deferred_command_buffer, + uint32_t query_index) const { + if (query_pool_ == VK_NULL_HANDLE || query_index >= capacity_) { + return; + } + + deferred_command_buffer.CmdVkEndQuery(query_pool_, query_index); +} + +void VulkanZPDQueryPool::QueueQueryResolve(uint32_t query_index) { + if (query_index >= capacity_) { + return; + } + + // Guard against duplicates within the same submission. + if (!resolve_batch_pending_[query_index]) { + resolve_batch_pending_[query_index] = 1; + ++resolve_batch_index_count_; + } +} + +void VulkanZPDQueryPool::RecordResolveBatch(VkCommandBuffer command_buffer) { + if (!resolve_batch_index_count_) { + return; + } + + if (!is_initialized()) { + std::fill(resolve_batch_pending_.begin(), resolve_batch_pending_.end(), 0); + resolve_batch_index_count_ = 0; + return; + } + + std::vector ranges; + + // Coalesce into contiguous ranges - minimize vkCmdCopyQueryPoolResults calls. + uint32_t range_start = 0; + uint32_t range_count = 0; + for (uint32_t index = 0; index < capacity_; ++index) { + if (!resolve_batch_pending_[index]) { + continue; + } + + if (range_count == 0) { + range_start = index; + range_count = 1; + continue; + } + + if (index == range_start + range_count) { + ++range_count; + continue; + } + + ranges.push_back({range_start, range_count}); + range_start = index; + range_count = 1; + } + + if (range_count != 0) { + ranges.push_back({range_start, range_count}); + } + + // Reset the batch. ENDs from later in this submission belong to the next. + std::fill(resolve_batch_pending_.begin(), resolve_batch_pending_.end(), 0); + resolve_batch_index_count_ = 0; + + if (ranges.empty()) { + return; + } + + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions(); + + VkDeviceSize barrier_offset = VK_WHOLE_SIZE; + VkDeviceSize barrier_end = 0; + for (const ResolveRange& range : ranges) { + if (range.start >= capacity_) { + continue; + } + + uint32_t count = std::min(range.count, capacity_ - range.start); + VkDeviceSize offset = + static_cast(range.start) * sizeof(uint64_t); + VkDeviceSize size = static_cast(count) * sizeof(uint64_t); + + // WAIT_BIT blocks until available. No separate availability check needed. + dfn.vkCmdCopyQueryPoolResults( + command_buffer, query_pool_, range.start, count, readback_buffer_, + offset, sizeof(uint64_t), + VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT); + + barrier_offset = std::min(barrier_offset, offset); + barrier_end = std::max(barrier_end, offset + size); + } + + if (barrier_offset == VK_WHOLE_SIZE || barrier_end <= barrier_offset) { + return; + } + + VkBufferMemoryBarrier readback_barrier; + readback_barrier.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER; + readback_barrier.pNext = nullptr; + readback_barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + readback_barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT; + readback_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + readback_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + readback_barrier.buffer = readback_buffer_; + readback_barrier.offset = barrier_offset; + readback_barrier.size = barrier_end - barrier_offset; + dfn.vkCmdPipelineBarrier(command_buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_HOST_BIT, 0, 0, nullptr, 1, + &readback_barrier, 0, nullptr); +} + +void VulkanZPDQueryPool::InvalidateReadback() { + if (readback_is_coherent_ || !vulkan_device_ || + readback_memory_ == VK_NULL_HANDLE || !readback_mapping_) { + return; + } + + const ui::vulkan::VulkanDevice::Functions& dfn = vulkan_device_->functions(); + const VkDevice device = vulkan_device_->device(); + + // Flushes the CPU-side cache so the persistent mapping reflects the GPU + // writes made by vkCmdCopyQueryPoolResults. Not needed on HOST_COHERENT. + VkMappedMemoryRange range; + range.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; + range.pNext = nullptr; + range.memory = readback_memory_; + range.offset = 0; + range.size = VK_WHOLE_SIZE; + dfn.vkInvalidateMappedMemoryRanges(device, 1, &range); +} + +uint64_t VulkanZPDQueryPool::GetQueryReadbackValue(uint32_t query_index) const { + if (!readback_mapping_ || query_index >= capacity_) { + return 0; + } + + return readback_mapping_[query_index]; +} + +} // namespace vulkan +} // namespace gpu +} // namespace xe diff --git a/src/xenia/gpu/vulkan/vulkan_zpd_query_pool.h b/src/xenia/gpu/vulkan/vulkan_zpd_query_pool.h new file mode 100644 index 000000000..dd2cfeec4 --- /dev/null +++ b/src/xenia/gpu/vulkan/vulkan_zpd_query_pool.h @@ -0,0 +1,108 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_GPU_VULKAN_VULKAN_ZPD_QUERY_POOL_H_ +#define XENIA_GPU_VULKAN_VULKAN_ZPD_QUERY_POOL_H_ + +#include +#include + +#include "xenia/ui/vulkan/vulkan_api.h" + +namespace xe { +namespace ui { +namespace vulkan { +class VulkanDevice; +} +} // namespace ui + +namespace gpu { +namespace vulkan { + +class DeferredCommandBuffer; + +// Vulkan occlusion query pool for ZPD reports. Queries live in VkQueryPool, +// results are copied to a persistent buffer via vkCmdCopyQueryPoolResults. +// vkCmdBeginQuery is only valid inside a render pass, queries get deferred +// when no pass is open and segments split at pass boundaries. +// Requires VK_EXT_host_query_reset (1.2 core) so slots can be reset on the +// CPU at release time, no paired vkCmdEndQuery needed, and also allows +// DiscardHostZPDQuery work outside a pass. +// +// VK_QUERY_RESULT_WAIT_BIT in the copy removes the need for a separate +// availability check. Transfer barrier before InvalidateReadback covers non- +// coherent memory. +// +// Per-slot generation counter has same purpose as D3D12 pool. +class VulkanZPDQueryPool { + public: + VulkanZPDQueryPool() = default; + VulkanZPDQueryPool(const VulkanZPDQueryPool&) = delete; + VulkanZPDQueryPool& operator=(const VulkanZPDQueryPool&) = delete; + ~VulkanZPDQueryPool() { Shutdown(); } + + bool EnsureInitialized(const ui::vulkan::VulkanDevice* vulkan_device, + uint32_t requested_capacity, bool can_recreate); + void Shutdown(); + + bool is_initialized() const { + return query_pool_ != VK_NULL_HANDLE && + readback_buffer_ != VK_NULL_HANDLE && readback_mapping_ != nullptr && + capacity_ != 0; + } + + uint32_t capacity() const { return capacity_; } + + bool has_pending_resolve_batch() const { + return resolve_batch_index_count_ != 0; + } + + bool has_free_indices() const { return !free_indices_.empty(); } + + bool AcquireQueryIndex(uint32_t& query_index, uint32_t& query_generation); + void ReleaseQueryIndex(uint32_t query_index, uint32_t query_generation); + bool GenerationMatches(uint32_t query_index, uint32_t query_generation) const; + + void BeginQuery(DeferredCommandBuffer& deferred_command_buffer, + uint32_t query_index) const; + void EndQuery(DeferredCommandBuffer& deferred_command_buffer, + uint32_t query_index) const; + void QueueQueryResolve(uint32_t query_index); + void RecordResolveBatch(VkCommandBuffer command_buffer); + + void InvalidateReadback(); + + uint64_t GetQueryReadbackValue(uint32_t query_index) const; + + private: + const ui::vulkan::VulkanDevice* vulkan_device_ = nullptr; + + VkQueryPool query_pool_ = VK_NULL_HANDLE; + + VkBuffer readback_buffer_ = VK_NULL_HANDLE; + VkDeviceMemory readback_memory_ = VK_NULL_HANDLE; + uint64_t* readback_mapping_ = nullptr; + // If not HOST_COHERENT, call InvalidateReadback before reading. + bool readback_is_coherent_ = true; + + uint32_t capacity_ = 0; + std::vector free_indices_; + + // Bumped on each acquire so stale copies from recycled slots get dropped. + std::vector index_generations_; + + std::vector resolve_batch_pending_; + uint32_t resolve_batch_index_count_ = 0; +}; + +} // namespace vulkan +} // namespace gpu +} // namespace xe + +#endif // XENIA_GPU_VULKAN_VULKAN_ZPD_QUERY_POOL_H_ diff --git a/src/xenia/gpu/xenos_report_controller.cc b/src/xenia/gpu/xenos_report_controller.cc new file mode 100644 index 000000000..f67c86830 --- /dev/null +++ b/src/xenia/gpu/xenos_report_controller.cc @@ -0,0 +1,213 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/gpu/xenos_report_controller.h" + +#include + +#include "xenia/base/logging.h" +#include "xenia/gpu/gpu_flags.h" +#include "xenia/gpu/xenos_zpd_report.h" + +namespace xe { +namespace gpu { + +XenosReportController::BeginReportResult XenosReportController::BeginReport( + uint32_t report_address) { + std::lock_guard lock(mutex_); + + uint32_t slot_base = XenosZPDReport::GetSlotBase(report_address); + if (!slot_base) { + return {}; + } + + // Bumping before state creation invalidates pending writes from old lifetime. + uint64_t slot_sequence_id = ++slot_sequences_[slot_base]; + + ReportHandle report_handle = next_report_handle_++; + if (report_handle == kInvalidReportHandle) { + // 0 is reserved as the invalid handle. Skip over it if the counter wraps. + report_handle = next_report_handle_++; + } + + LogicalReportState& report_state = logical_reports_[report_handle]; + + report_state.slot_base = slot_base; + report_state.slot_sequence_id = slot_sequence_id; + report_state.begin_value = slot_values_[slot_base]; + report_state.resolved = false; + report_state.delta_value = 0; + + return {report_handle, report_state.begin_value}; +} + +void XenosReportController::QueueReportWrite(uint32_t report_address, + ReportHandle report_handle) { + std::lock_guard lock(mutex_); + + uint32_t slot_base = XenosZPDReport::GetSlotBase(report_address); + if (!slot_base) { + return; + } + + auto existing_report = logical_reports_.find(report_handle); + if (existing_report == logical_reports_.end()) { + return; + } + + QueuedReportWrite queued_write; + queued_write.report_handle = report_handle; + queued_write.slot_base = slot_base; + + // Appended to back, preserves FIFO so a later write can't jump an earlier one + queued_report_writes_.push_back(queued_write); + ++queued_report_write_slot_counts_[queued_write.slot_base]; +} + +void XenosReportController::SetReportResolved(ReportHandle report_handle, + uint32_t delta_value) { + std::lock_guard lock(mutex_); + + auto existing_report = logical_reports_.find(report_handle); + if (existing_report == logical_reports_.end()) { + return; + } + + existing_report->second.resolved = true; + + existing_report->second.delta_value = delta_value; +} + +uint32_t XenosReportController::RetireReports() { + std::vector pending_guest_commits; + { + std::lock_guard lock(mutex_); + ProcessReportWritesLocked(pending_guest_commits); + } + + // Fire callbacks outside the lock. The callback writes directly to guest + // memory and may call back into the backend. + if (commit_guest_report_callback_) { + for (const PendingGuestCommit& commit : pending_guest_commits) { + commit_guest_report_callback_(commit.report_handle, commit.slot_base, + commit.begin_value, commit.delta_value, + callback_context_); + } + } + + return static_cast(pending_guest_commits.size()); +} + +bool XenosReportController::HasQueuedWriteForAddress( + uint32_t report_address) const { + std::lock_guard lock(mutex_); + return HasQueuedWriteForSlotLocked( + XenosZPDReport::GetSlotBase(report_address)); +} + +void XenosReportController::ResetStats() { + std::lock_guard lock(mutex_); + stats_ = {}; +} + +void XenosReportController::ProcessReportWritesLocked( + std::vector& pending_guest_commits) { + if (queued_report_writes_.empty()) { + return; + } + + std::unordered_set blocked_slot_bases; + + auto remove_queued_write = [this](uint32_t slot_base) { + auto queued_write_count = queued_report_write_slot_counts_.find(slot_base); + if (queued_write_count == queued_report_write_slot_counts_.end()) { + return; + } + if (--queued_write_count->second == 0) { + queued_report_write_slot_counts_.erase(queued_write_count); + } + }; + + auto write_it = queued_report_writes_.begin(); + while (write_it != queued_report_writes_.end()) { + QueuedReportWrite& queued_write = *write_it; + + auto existing_report = logical_reports_.find(queued_write.report_handle); + if (existing_report == logical_reports_.end()) { + if (cvars::occlusion_query_log) { + XELOGI( + "ZPD: Controller ProcessReportWritesLocked drop missing handle={}", + queued_write.report_handle); + } + write_it = queued_report_writes_.erase(write_it); + remove_queued_write(queued_write.slot_base); + } else { + LogicalReportState& report_state = existing_report->second; + + // Block unresolved later writes in the same slot, preserving FIFO order. + if (!report_state.resolved) { + blocked_slot_bases.insert(queued_write.slot_base); + ++write_it; + } else if (blocked_slot_bases.count(queued_write.slot_base)) { + if (cvars::occlusion_query_log) { + XELOGI( + "ZPD: Controller ProcessReportWritesLocked blocked handle={} " + "slot=0x{:08X}", + queued_write.report_handle, queued_write.slot_base); + } + ++write_it; + } else { + auto seq_it = slot_sequences_.find(report_state.slot_base); + uint64_t current_seq = + seq_it != slot_sequences_.end() ? seq_it->second : 0; + + // The slot has been reused since this write was queued. Discard. + if (current_seq != report_state.slot_sequence_id) { + if (cvars::occlusion_query_log) { + XELOGI( + "ZPD: Controller ProcessReportWritesLocked stale handle={} " + "slot=0x{:08X} report_seq={} current_seq={}", + queued_write.report_handle, queued_write.slot_base, + report_state.slot_sequence_id, current_seq); + } + write_it = queued_report_writes_.erase(write_it); + remove_queued_write(queued_write.slot_base); + logical_reports_.erase(existing_report); + } else { + pending_guest_commits.push_back( + {queued_write.report_handle, queued_write.slot_base, + report_state.begin_value, report_state.delta_value}); + + // Advance running total so next BeginReport picks up the right + // begin_value. + uint64_t end_value = static_cast(report_state.begin_value) + + static_cast(report_state.delta_value); + + slot_values_[report_state.slot_base] = + end_value > UINT32_MAX ? UINT32_MAX + : static_cast(end_value); + + write_it = queued_report_writes_.erase(write_it); + remove_queued_write(queued_write.slot_base); + logical_reports_.erase(existing_report); + ++stats_.writes_retired; + } + } + } + } +} + +bool XenosReportController::HasQueuedWriteForSlotLocked( + uint32_t slot_base) const { + return queued_report_write_slot_counts_.find(slot_base) != + queued_report_write_slot_counts_.end(); +} + +} // namespace gpu +} // namespace xe diff --git a/src/xenia/gpu/xenos_report_controller.h b/src/xenia/gpu/xenos_report_controller.h new file mode 100644 index 000000000..1ca805c03 --- /dev/null +++ b/src/xenia/gpu/xenos_report_controller.h @@ -0,0 +1,132 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_GPU_XENOS_REPORT_CONTROLLER_H_ +#define XENIA_GPU_XENOS_REPORT_CONTROLLER_H_ + +#include +#include +#include +#include +#include + +namespace xe { +namespace gpu { + +// Sequences ZPD (occlusion query) report writebacks. +// +// Guest report memory is updated directly when the report event fires. On the +// host, results take at least a command list boundary to land, often several +// frames. The guest has usually reused the slot by then. Results can also span +// multiple host query segments across submissions. +// +// Two rules at retirement: FIFO within a slot (a later write can't jump an +// older pending one), and stale generation discard (each BeginReport bumps +// the slot's sequence. Writes with a stale sequence are dropped so old host +// results don't reach a slot that's already been recycled). +// +// TODO(boma): QueryBatch Lock/Unlock is a separate model - not supported yet. +class XenosReportController { + public: + using ReportHandle = uint32_t; + static constexpr ReportHandle kInvalidReportHandle = 0; + + struct BeginReportResult { + ReportHandle report_handle = kInvalidReportHandle; + uint32_t begin_value = 0; + }; + + using CommitGuestReportCallback = void (*)(ReportHandle report_handle, + uint32_t slot_base, + uint32_t begin_value, + uint32_t delta_value, + void* callback_context); + + explicit XenosReportController( + CommitGuestReportCallback commit_guest_report_callback, + void* callback_context) + : commit_guest_report_callback_(commit_guest_report_callback), + callback_context_(callback_context) {} + + // Bumps the slot sequence (invalidates pending writes from prior lifetime) + // and snapshots begin_value from slot_values_. + BeginReportResult BeginReport(uint32_t report_address); + + // Queues a pending write. Deque order preserves FIFO within the slot. + void QueueReportWrite(uint32_t report_address, ReportHandle report_handle); + + void SetReportResolved(ReportHandle report_handle, uint32_t delta_value); + + // Retires resolved writes in FIFO order and fires the writeback callback. + // Callback runs outside the lock. + uint32_t RetireReports(); + + // Used by strict mode to check if a result is still pending. + bool HasQueuedWriteForAddress(uint32_t report_address) const; + + struct Stats { + uint64_t writes_retired = 0; + }; + + const Stats& stats() const { return stats_; } + void ResetStats(); + + private: + // Built under lock, flushed outside so writes to guest don't hold it. + struct PendingGuestCommit { + ReportHandle report_handle = kInvalidReportHandle; + uint32_t slot_base = 0; + uint32_t begin_value = 0; + uint32_t delta_value = 0; + }; + + struct LogicalReportState { + uint32_t slot_base = 0; + uint64_t slot_sequence_id = 0; + // Snapshotted at BEGIN, not at retirement. Guest will have reused by then. + uint32_t begin_value = 0; + uint32_t delta_value = 0; + bool resolved = false; + }; + + // Maintains FIFO ordering within the slot. + struct QueuedReportWrite { + ReportHandle report_handle = kInvalidReportHandle; + uint32_t slot_base = 0; + }; + + void ProcessReportWritesLocked( + std::vector& pending_guest_commits); + + bool HasQueuedWriteForSlotLocked(uint32_t slot_base) const; + + CommitGuestReportCallback commit_guest_report_callback_ = nullptr; + void* callback_context_ = nullptr; + + mutable std::mutex mutex_; + + std::deque queued_report_writes_; + std::unordered_map queued_report_write_slot_counts_; + std::unordered_map logical_reports_; + + // Bumped at each BeginReport. Stale generation writes get discarded. + std::unordered_map slot_sequences_; + + // Running total per slot. Seeded into begin_value at BEGIN, advanced at + // each retirement so counts accumulate, same as real hardware. + std::unordered_map slot_values_; + + Stats stats_; + ReportHandle next_report_handle_ = 1; +}; + +} // namespace gpu +} // namespace xe + +#endif // XENIA_GPU_XENOS_REPORT_CONTROLLER_H_ diff --git a/src/xenia/gpu/xenos_zpd_report.h b/src/xenia/gpu/xenos_zpd_report.h new file mode 100644 index 000000000..cded5da5d --- /dev/null +++ b/src/xenia/gpu/xenos_zpd_report.h @@ -0,0 +1,128 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2026 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_GPU_XENOS_ZPD_REPORT_H_ +#define XENIA_GPU_XENOS_ZPD_REPORT_H_ + +#include +#include + +#include "xenia/gpu/xenos.h" + +namespace xe { +namespace gpu { + +// Guest memory helpers for occlusion query ZPD reports. +struct XenosZPDReport { + static constexpr uint32_t kRecordSizeBytes = 0x20; + static constexpr uint32_t kRecordAlignMask = ~(kRecordSizeBytes - 1); + + // Each slot holds one BEGIN record and one END record. + // END is at the slot base, and BEGIN is +0x20. + static constexpr uint32_t kSlotSizeBytes = 0x40; + static constexpr uint32_t kSlotAlignMask = ~(kSlotSizeBytes - 1); + + static constexpr uint32_t kBatchPageSizeBytes = 0x1000; + static constexpr uint32_t kBatchPageAlignMask = ~(kBatchPageSizeBytes - 1); + + static constexpr uint32_t GetRecordBase(uint32_t address) { + return address & kRecordAlignMask; + } + + static constexpr uint32_t GetSlotBase(uint32_t address) { + return address & kSlotAlignMask; + } + + static constexpr uint32_t GetBatchPageBase(uint32_t address) { + return GetRecordBase(address) & kBatchPageAlignMask; + } + + static constexpr uint32_t GetBeginRecordBase(uint32_t address) { + return GetSlotBase(address) + kRecordSizeBytes; + } + + static constexpr uint32_t GetEndRecordBase(uint32_t address) { + return GetSlotBase(address); + } + + static constexpr bool IsBeginRecord(uint32_t address) { + uint32_t record_base = GetRecordBase(address); + return record_base && record_base == GetBeginRecordBase(record_base); + } + + static constexpr bool IsEndRecord(uint32_t address) { + uint32_t record_base = GetRecordBase(address); + return record_base && record_base == GetEndRecordBase(record_base); + } + + // Checks for the 0x20 checkpoint walk batched titles use in one page. + static constexpr bool IsBatchStep(uint32_t last_record_base, + uint32_t record_base) { + return last_record_base != 0 && + record_base == last_record_base + kRecordSizeBytes; + } + + // Boundary detection only looks at ZPass_A first, then ZFail_A. + // Some titles (4D5307E8) have unique, non-zero values in the B fields, which + // aren't understand well enough to count them yet. + // Proper support might need separate queries for both A & B. + static bool HasPendingSentinel( + const xenos::xe_gpu_depth_sample_counts* report) { + constexpr uint32_t kSentinelLE = 0xEDFEFFFFu; + constexpr uint32_t kSentinelBE = 0xFFFFFEEDu; + + if (report->ZPass_A == kSentinelLE || report->ZPass_A == kSentinelBE) { + return true; + } + if (report->ZFail_A == kSentinelLE || report->ZFail_A == kSentinelBE) { + return true; + } + return false; + } + + // Total_A mirrors ZPass_A and the rest are zeroed out since host queries can + // only provide a passing count. This is still enough to satisfy most titles. + static void WriteSampleCount(xenos::xe_gpu_depth_sample_counts* report, + uint32_t sample_count) { + report->Total_A = sample_count; + report->Total_B = 0; + report->ZFail_A = 0; + report->ZFail_B = 0; + report->ZPass_A = sample_count; + report->ZPass_B = 0; + report->StencilFail_A = 0; + report->StencilFail_B = 0; + } + + // Fake mode for titles (425307EC, 4D5309B1) that use QueryBatch and expect + // the sample count to accumulate across multiple records. + static constexpr uint32_t AddSamples(uint32_t value, uint32_t step) { + return value > UINT32_MAX - step ? UINT32_MAX : value + step; + } + + static void WriteReportDelta(xenos::xe_gpu_depth_sample_counts* begin_report, + xenos::xe_gpu_depth_sample_counts* end_report, + uint32_t begin_value, uint32_t delta_value, + bool write_begin_report) { + uint64_t end_value = + static_cast(begin_value) + static_cast(delta_value); + uint32_t clamped_value = + end_value > UINT32_MAX ? UINT32_MAX : static_cast(end_value); + + if (write_begin_report && begin_report && end_report != begin_report) { + WriteSampleCount(begin_report, begin_value); + } + WriteSampleCount(end_report, clamped_value); + } +}; + +} // namespace gpu +} // namespace xe + +#endif // XENIA_GPU_XENOS_ZPD_REPORT_H_ diff --git a/src/xenia/ui/config_helpers.h b/src/xenia/ui/config_helpers.h index f06b77dac..b78eddbf8 100644 --- a/src/xenia/ui/config_helpers.h +++ b/src/xenia/ui/config_helpers.h @@ -81,6 +81,7 @@ GetKnownEnumOptions() { #endif {"d3d12_readback_resolve", {"kCopy", "kComputeLuminance", "kComputeRGBA16"}}, + {"occlusion_query", {"fake", "fast", "strict"}}, {"readback_resolve", {"fast", "some", "full", "none"}}, {"render_target_path", {"performance", "accuracy"}}, {"postprocess_antialiasing", {"off", "fxaa", "fxaa_extreme"}}, diff --git a/src/xenia/ui/imgui_performance_dialog.cc b/src/xenia/ui/imgui_performance_dialog.cc index 4156b2694..26ac92fd1 100644 --- a/src/xenia/ui/imgui_performance_dialog.cc +++ b/src/xenia/ui/imgui_performance_dialog.cc @@ -24,7 +24,7 @@ DECLARE_bool(readback_memexport); DECLARE_bool(readback_memexport_fast); DECLARE_string(readback_resolve); DECLARE_bool(guest_display_refresh_cap); -DECLARE_bool(occlusion_query_enable); +DECLARE_string(occlusion_query); namespace xe { namespace ui { @@ -51,8 +51,15 @@ void ImGuiPerformanceDialog::LoadCurrentSettings() { // Load Emulated Display Uncapped (inverted from guest_display_refresh_cap) display_uncapped_ = !cvars::guest_display_refresh_cap; - // Load Occlusion Query setting - occlusion_query_ = cvars::occlusion_query_enable; + // Load Occlusion Query setting (0=fake, 1=fast, 2=strict) + const std::string& oq_mode = cvars::occlusion_query; + if (oq_mode == "fast") { + occlusion_query_mode_ = 1; + } else if (oq_mode == "strict") { + occlusion_query_mode_ = 2; + } else { + occlusion_query_mode_ = 0; // Default to "fake" + } // Load Readback Resolve setting (0=none, 1=some, 2=fast, 3=full) const std::string& resolve_mode = cvars::readback_resolve; @@ -151,11 +158,33 @@ void ImGuiPerformanceDialog::OnEmulatedDisplayUncappedChanged(bool uncapped) { ShowNotification("Emulated Display", uncapped ? "Uncapped" : "Capped"); } -void ImGuiPerformanceDialog::OnOcclusionQueryChanged(bool enabled) { - SetOcclusionQueryEnable(enabled); - config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", - "occlusion_query_enable", enabled); - ShowNotification("Occlusion Queries", enabled ? "Enabled" : "Disabled"); +void ImGuiPerformanceDialog::OnOcclusionQueryChanged(int value) { + auto emulator = emulator_window_->emulator(); + if (!emulator) return; + + auto graphics_system = emulator->graphics_system(); + if (!graphics_system) return; + + auto command_processor = graphics_system->command_processor(); + if (!command_processor) return; + + gpu::ZPDMode mode; + switch (value) { + case 1: + mode = gpu::ZPDMode::kFast; + break; + case 2: + mode = gpu::ZPDMode::kStrict; + break; + default: + mode = gpu::ZPDMode::kFake; + break; + } + + command_processor->SetZPDMode(mode); + + const char* mode_names[] = {"Fake", "Fast", "Strict"}; + ShowNotification("Occlusion Query Mode", mode_names[value]); } void ImGuiPerformanceDialog::OnClearMemoryPageStateChanged(bool enabled) { @@ -272,6 +301,42 @@ void ImGuiPerformanceDialog::OnDraw(ImGuiIO& io) { ImGui::Separator(); ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, xbox_green); + ImGui::Text("Occlusion Query Mode"); + ImGui::PopStyleColor(); + + ImGui::Indent(10); + ImGui::PushID("occlusion_query"); + const char* oq_labels[] = {"Fake", "Fast", "Strict"}; + for (int i = 0; i < 3; i++) { + bool is_selected = (occlusion_query_mode_ == i); + bool is_highlighted = (occlusion_query_highlight_ == i); + + if (is_highlighted && !is_selected) { + ImGui::PushStyleColor(ImGuiCol_Text, highlight_color); + } + + if (ImGui::RadioButton(oq_labels[i], is_selected)) { + if (!is_selected) { + occlusion_query_mode_ = i; + occlusion_query_highlight_ = i; + OnOcclusionQueryChanged(i); + } + } + + if (is_highlighted && !is_selected) { + ImGui::PopStyleColor(); + } + + if (i < 2) ImGui::SameLine(); + } + ImGui::PopID(); + ImGui::Unindent(10); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + // Other section ImGui::PushStyleColor(ImGuiCol_Text, xbox_green); ImGui::Text("Other"); @@ -283,11 +348,6 @@ void ImGuiPerformanceDialog::OnDraw(ImGuiIO& io) { OnEmulatedDisplayUncappedChanged(display_uncapped_); } - if (ImGui::Checkbox("Enable hardware occlusion queries", - &occlusion_query_)) { - OnOcclusionQueryChanged(occlusion_query_); - } - if (ImGui::Checkbox("Clear memory page state on GPU cache invalidation", &clear_memory_page_state_)) { OnClearMemoryPageStateChanged(clear_memory_page_state_); diff --git a/src/xenia/ui/imgui_performance_dialog.h b/src/xenia/ui/imgui_performance_dialog.h index 151fd8e6f..3a26f018e 100644 --- a/src/xenia/ui/imgui_performance_dialog.h +++ b/src/xenia/ui/imgui_performance_dialog.h @@ -49,8 +49,8 @@ class ImGuiPerformanceDialog : public ImGuiGamepadDialog { // Setting change handlers void OnReadbackResolveChanged(int value); void OnReadbackMemexportChanged(int value); + void OnOcclusionQueryChanged(int value); void OnEmulatedDisplayUncappedChanged(bool uncapped); - void OnOcclusionQueryChanged(bool enabled); void OnClearMemoryPageStateChanged(bool enabled); app::EmulatorWindow* emulator_window_; @@ -59,13 +59,14 @@ class ImGuiPerformanceDialog : public ImGuiGamepadDialog { // Current settings state (selected values) int readback_resolve_mode_ = 2; // 0=None, 1=Some, 2=Fast, 3=Full int readback_memexport_mode_ = 1; // 0=None, 1=Fast, 2=Full + int occlusion_query_mode_ = 0; // 0=Fake, 1=Fast, 2=Strict bool display_uncapped_ = false; - bool occlusion_query_ = false; bool clear_memory_page_state_ = false; // Highlight positions for navigation int resolve_highlight_ = 2; int memexport_highlight_ = 1; + int occlusion_query_highlight_ = 0; }; } // namespace ui diff --git a/src/xenia/ui/vulkan/functions/device_1_2_ext_host_query_reset.inc b/src/xenia/ui/vulkan/functions/device_1_2_ext_host_query_reset.inc new file mode 100644 index 000000000..b3ef98998 --- /dev/null +++ b/src/xenia/ui/vulkan/functions/device_1_2_ext_host_query_reset.inc @@ -0,0 +1,3 @@ +// VK_EXT_host_query_reset functions used in Xenia. +// Promoted to Vulkan 1.2 core. +XE_UI_VULKAN_FUNCTION_PROMOTED(vkResetQueryPoolEXT, vkResetQueryPool) diff --git a/src/xenia/ui/vulkan/vulkan_device.cc b/src/xenia/ui/vulkan/vulkan_device.cc index 7cef624cb..b1af406a5 100644 --- a/src/xenia/ui/vulkan/vulkan_device.cc +++ b/src/xenia/ui/vulkan/vulkan_device.cc @@ -179,6 +179,7 @@ std::unique_ptr VulkanDevice::CreateIfSupported( } bool ext_1_2_KHR_sampler_mirror_clamp_to_edge = false; + bool ext_1_2_EXT_host_query_reset = false; bool ext_1_1_KHR_maintenance1 = false; bool ext_1_2_KHR_shader_float_controls = false; bool ext_EXT_fragment_shader_interlock = false; @@ -203,6 +204,7 @@ std::unique_ptr VulkanDevice::CreateIfSupported( XE_UI_VULKAN_STRUCT_PROMOTED_EXTENSION(KHR_sampler_ycbcr_conversion, 1, 1) // #198. Also must be enabled for VK_KHR_spirv_1_4. XE_UI_VULKAN_LOCAL_PROMOTED_EXTENSION(KHR_shader_float_controls, 1, 2) + XE_UI_VULKAN_LOCAL_PROMOTED_EXTENSION(EXT_host_query_reset, 1, 2) // #252. XE_UI_VULKAN_LOCAL_EXTENSION(EXT_fragment_shader_interlock) // #55. @@ -298,6 +300,9 @@ std::unique_ptr VulkanDevice::CreateIfSupported( VulkanFeatures features_1_2; + VulkanFeatures + features_EXT_host_query_reset; VulkanFeatures features_1_3; @@ -347,6 +352,9 @@ std::unique_ptr VulkanDevice::CreateIfSupported( if (get_physical_device_properties2_supported) { if (properties.apiVersion >= VK_MAKE_API_VERSION(0, 1, 2, 0)) { features_1_2.Link(supported_features_2, device_create_info); + } else if (ext_1_2_EXT_host_query_reset) { + features_EXT_host_query_reset.Link(supported_features_2, + device_create_info); } if (properties.apiVersion >= VK_MAKE_API_VERSION(0, 1, 3, 0)) { features_1_3.Link(supported_features_2, device_create_info); @@ -690,12 +698,18 @@ std::unique_ptr VulkanDevice::CreateIfSupported( XE_UI_VULKAN_FEATURE_2(features_1_2, samplerMirrorClampToEdge); XE_UI_VULKAN_FEATURE_2(features_1_2, uniformBufferStandardLayout); XE_UI_VULKAN_FEATURE_2(features_1_2, scalarBlockLayout); + XE_UI_VULKAN_FEATURE_2(features_1_2, hostQueryReset); } } else { if (ext_1_2_KHR_sampler_mirror_clamp_to_edge) { XE_UI_VULKAN_FEATURE_IMPLIED(samplerMirrorClampToEdge) } + if (ext_1_2_EXT_host_query_reset && with_gpu_emulation) { + XE_UI_VULKAN_FEATURE_2(features_EXT_host_query_reset, hostQueryReset); + } } + device->extensions_.ext_1_2_EXT_host_query_reset = + ext_1_2_EXT_host_query_reset; if (properties.apiVersion >= VK_MAKE_API_VERSION(0, 1, 3, 0)) { if (with_gpu_emulation) { @@ -852,6 +866,9 @@ std::unique_ptr VulkanDevice::CreateIfSupported( if (properties.apiVersion >= VK_MAKE_API_VERSION(0, 1, 1, 0)) { #include "xenia/ui/vulkan/functions/device_1_1_khr_bind_memory2.inc" #include "xenia/ui/vulkan/functions/device_1_1_khr_get_memory_requirements2.inc" + } + if (properties.apiVersion >= VK_MAKE_API_VERSION(0, 1, 2, 0)) { +#include "xenia/ui/vulkan/functions/device_1_2_ext_host_query_reset.inc" } if (properties.apiVersion >= VK_MAKE_API_VERSION(0, 1, 3, 0)) { #include "xenia/ui/vulkan/functions/device_1_3_khr_dynamic_rendering.inc" @@ -873,6 +890,11 @@ std::unique_ptr VulkanDevice::CreateIfSupported( #include "xenia/ui/vulkan/functions/device_1_1_khr_bind_memory2.inc" } } + if (properties.apiVersion < VK_MAKE_API_VERSION(0, 1, 2, 0)) { + if (device->extensions_.ext_1_2_EXT_host_query_reset) { +#include "xenia/ui/vulkan/functions/device_1_2_ext_host_query_reset.inc" + } + } if (properties.apiVersion < VK_MAKE_API_VERSION(0, 1, 3, 0)) { if (device->extensions_.ext_1_3_KHR_maintenance4) { #include "xenia/ui/vulkan/functions/device_1_3_khr_maintenance4.inc" diff --git a/src/xenia/ui/vulkan/vulkan_device.h b/src/xenia/ui/vulkan/vulkan_device.h index aba583488..55f57e2d3 100644 --- a/src/xenia/ui/vulkan/vulkan_device.h +++ b/src/xenia/ui/vulkan/vulkan_device.h @@ -126,6 +126,10 @@ class VulkanDevice { bool scalarBlockLayout = false; + // VK_EXT_host_query_reset (promoted to 1.2) + + bool hostQueryReset = false; + // VK_KHR_portability_subset (#164) bool constantAlphaColorBlendFactors = false; @@ -200,6 +204,7 @@ class VulkanDevice { bool ext_1_1_KHR_bind_memory2 = false; // #158 bool ext_1_2_KHR_spirv_1_4 = false; // #237 bool ext_EXT_memory_budget = false; // #238 + bool ext_1_2_EXT_host_query_reset = false; // promoted to 1.2 // Has optional features not implied by this being true. bool ext_1_3_KHR_maintenance4 = false; // #414 // VK_KHR_dynamic_rendering (#55, promoted to 1.3) @@ -230,6 +235,8 @@ class VulkanDevice { #include "xenia/ui/vulkan/functions/device_1_1_khr_get_memory_requirements2.inc" // VK_KHR_bind_memory2 (#158, promoted to 1.1) #include "xenia/ui/vulkan/functions/device_1_1_khr_bind_memory2.inc" + // VK_EXT_host_query_reset (promoted to 1.2) +#include "xenia/ui/vulkan/functions/device_1_2_ext_host_query_reset.inc" // VK_KHR_maintenance4 (#414, promoted to 1.3) #include "xenia/ui/vulkan/functions/device_1_3_khr_maintenance4.inc" // VK_KHR_dynamic_rendering (#55, promoted to 1.3)