mirror of
https://github.com/izzy2lost/xenia-edge.git
synced 2026-07-06 00:20:26 -07:00
[GPU] Rewrite hardware occlusion (ZPD) implementation
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -12,17 +12,22 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#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<XenosReportController> zpd_report_controller_;
|
||||
std::unordered_map<XenosReportController::ReportHandle, ZPDReport>
|
||||
logical_zpd_reports_;
|
||||
ActiveZPDSegment zpd_active_segment_{};
|
||||
std::deque<PendingQueryResolve> zpd_resolves_in_flight_;
|
||||
|
||||
// Cached delta per END.
|
||||
// Fast mode uses this for speculative writeback and orphaned END replay.
|
||||
std::unordered_map<uint32_t, uint32_t> 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;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<ui::d3d12::D3D12Provider*>(
|
||||
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<ui::d3d12::D3D12GPUCompletionTimeline> completion_timeline_;
|
||||
bool submission_open_ = false;
|
||||
|
||||
@@ -567,6 +603,8 @@ class D3D12CommandProcessor final : public CommandProcessor {
|
||||
|
||||
std::unique_ptr<D3D12RenderTargetCache> render_target_cache_;
|
||||
|
||||
std::unique_ptr<D3D12ZPDQueryPool> zpd_host_query_pool_;
|
||||
|
||||
std::unique_ptr<ui::d3d12::D3D12UploadBufferPool> constant_buffer_pool_;
|
||||
|
||||
static constexpr uint32_t kViewBindfulHeapSize = 32768;
|
||||
@@ -723,42 +761,6 @@ class D3D12CommandProcessor final : public CommandProcessor {
|
||||
Microsoft::WRL::ComPtr<ID3D12Resource> fxaa_source_texture_;
|
||||
uint64_t fxaa_source_texture_submission_ = 0;
|
||||
|
||||
// Occlusion query resources.
|
||||
Microsoft::WRL::ComPtr<ID3D12QueryHeap> occlusion_query_heap_;
|
||||
Microsoft::WRL::ComPtr<ID3D12Resource> 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<PendingOcclusionQuery> 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<D3D12_RESOURCE_BARRIER> barriers_;
|
||||
|
||||
|
||||
@@ -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 <algorithm>
|
||||
|
||||
#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<uint64_t*>(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<ResolveRange> 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
|
||||
@@ -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 <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#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<ID3D12QueryHeap> query_heap_;
|
||||
|
||||
// Persistently mapped. Results readable once the fence signals.
|
||||
Microsoft::WRL::ComPtr<ID3D12Resource> readback_buffer_;
|
||||
uint64_t* readback_mapping_ = nullptr;
|
||||
|
||||
uint32_t capacity_ = 0;
|
||||
std::vector<uint32_t> free_indices_;
|
||||
|
||||
// Bumped on each acquire so stale readbacks from a recycled slot get dropped.
|
||||
std::vector<uint32_t> index_generations_;
|
||||
|
||||
std::vector<uint8_t> 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_
|
||||
@@ -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<xe_mutex> 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) {
|
||||
|
||||
@@ -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<const Pipeline*>(handle)->state.load(
|
||||
std::memory_order_acquire);
|
||||
}
|
||||
ID3D12PipelineState* AwaitD3D12PipelineByHandle(void* handle);
|
||||
|
||||
ID3D12RootSignature* GetRootSignatureByHandle(void* handle) const {
|
||||
return reinterpret_cast<const Pipeline*>(handle)
|
||||
|
||||
+13
-19
@@ -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; }
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>();
|
||||
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<xe_gpu_depth_sample_counts*>(
|
||||
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<uint32_t>(cvars::occlusion_query_fake_lower_threshold))
|
||||
? static_cast<uint32_t>(
|
||||
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<xe_gpu_depth_sample_counts*>(
|
||||
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<uint32_t>(
|
||||
cvars::query_occlusion_sample_lower_threshold)
|
||||
? static_cast<uint32_t>(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<uint32_t>(cvars::occlusion_query_fake_lower_threshold))
|
||||
? static_cast<uint32_t>(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<reg::PA_SC_VIZ_QUERY>();
|
||||
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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<const ui::vulkan::VulkanProvider*>(
|
||||
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<VkSemaphore> 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<VulkanRenderTargetCache> render_target_cache_;
|
||||
|
||||
std::unique_ptr<VulkanZPDQueryPool> zpd_host_query_pool_;
|
||||
|
||||
std::unique_ptr<VulkanPipelineCache> pipeline_cache_;
|
||||
|
||||
std::unique_ptr<VulkanTextureCache> texture_cache_;
|
||||
@@ -887,25 +926,6 @@ class VulkanCommandProcessor final : public CommandProcessor {
|
||||
// Per-memexport double-buffered readback for fast mode (delayed sync)
|
||||
std::unordered_map<uint64_t, ReadbackBuffer> 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<PendingOcclusionQuery> pending_occlusion_queries_;
|
||||
|
||||
// Debug marker support for RenderDoc/debug tools.
|
||||
bool debug_markers_enabled_ = false;
|
||||
void UpdateDebugMarkersEnabled();
|
||||
|
||||
@@ -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<std::mutex> 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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 <algorithm>
|
||||
|
||||
#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<uint64_t*>(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<ResolveRange> 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<VkDeviceSize>(range.start) * sizeof(uint64_t);
|
||||
VkDeviceSize size = static_cast<VkDeviceSize>(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
|
||||
@@ -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 <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#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<uint32_t> free_indices_;
|
||||
|
||||
// Bumped on each acquire so stale copies from recycled slots get dropped.
|
||||
std::vector<uint32_t> index_generations_;
|
||||
|
||||
std::vector<uint8_t> 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_
|
||||
@@ -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 <unordered_set>
|
||||
|
||||
#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<std::mutex> 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<std::mutex> 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<std::mutex> 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<PendingGuestCommit> pending_guest_commits;
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<uint32_t>(pending_guest_commits.size());
|
||||
}
|
||||
|
||||
bool XenosReportController::HasQueuedWriteForAddress(
|
||||
uint32_t report_address) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return HasQueuedWriteForSlotLocked(
|
||||
XenosZPDReport::GetSlotBase(report_address));
|
||||
}
|
||||
|
||||
void XenosReportController::ResetStats() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
stats_ = {};
|
||||
}
|
||||
|
||||
void XenosReportController::ProcessReportWritesLocked(
|
||||
std::vector<PendingGuestCommit>& pending_guest_commits) {
|
||||
if (queued_report_writes_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::unordered_set<uint32_t> 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<uint64_t>(report_state.begin_value) +
|
||||
static_cast<uint64_t>(report_state.delta_value);
|
||||
|
||||
slot_values_[report_state.slot_base] =
|
||||
end_value > UINT32_MAX ? UINT32_MAX
|
||||
: static_cast<uint32_t>(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
|
||||
@@ -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 <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
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<PendingGuestCommit>& 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<QueuedReportWrite> queued_report_writes_;
|
||||
std::unordered_map<uint32_t, uint32_t> queued_report_write_slot_counts_;
|
||||
std::unordered_map<ReportHandle, LogicalReportState> logical_reports_;
|
||||
|
||||
// Bumped at each BeginReport. Stale generation writes get discarded.
|
||||
std::unordered_map<uint32_t, uint64_t> 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<uint32_t, uint32_t> slot_values_;
|
||||
|
||||
Stats stats_;
|
||||
ReportHandle next_report_handle_ = 1;
|
||||
};
|
||||
|
||||
} // namespace gpu
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_GPU_XENOS_REPORT_CONTROLLER_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 <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#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<uint64_t>(begin_value) + static_cast<uint64_t>(delta_value);
|
||||
uint32_t clamped_value =
|
||||
end_value > UINT32_MAX ? UINT32_MAX : static_cast<uint32_t>(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_
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user