[GPU] Rewrite hardware occlusion (ZPD) implementation

This commit is contained in:
goldislead
2026-04-05 23:15:44 +09:00
committed by Heel
parent 7acf88d59f
commit 397fc92841
26 changed files with 3033 additions and 851 deletions
File diff suppressed because it is too large Load Diff
+224 -5
View File
@@ -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
+56 -54
View File
@@ -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_;
+263
View File
@@ -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_
+31
View File
@@ -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) {
+5
View File
@@ -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
View File
@@ -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; }
+4 -4
View File
@@ -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.
+151 -31
View File
@@ -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
+57 -37
View File
@@ -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_
+213
View File
@@ -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
+132
View File
@@ -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_
+128
View File
@@ -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