[GPU/ZPD] Inline XenosReportController into CommandProcessor

This commit is contained in:
Herman S.
2026-04-06 01:14:33 +09:00
parent e8065ab248
commit 0fdb5ff824
7 changed files with 85 additions and 434 deletions
+54 -50
View File
@@ -582,15 +582,10 @@ bool CommandProcessor::Restore(ByteStream* stream) {
bool CommandProcessor::SetupContext() {
ResetZPDState();
zpd_report_controller_ = std::make_unique<XenosReportController>(
&CommandProcessor::ZPDReportCallback, this);
return true;
}
void CommandProcessor::ShutdownContext() {
ResetZPDState();
zpd_report_controller_.reset();
}
void CommandProcessor::ShutdownContext() { ResetZPDState(); }
void CommandProcessor::InitializeRingBuffer(uint32_t ptr, uint32_t size_log2) {
read_ptr_index_ = 0;
@@ -1072,18 +1067,24 @@ bool CommandProcessor::BeginZPDReport(uint32_t report_address) {
uint32_t slot_base = XenosZPDReport::GetSlotBase(report_address);
uint32_t begin_record = XenosZPDReport::GetBeginRecordBase(slot_base);
uint32_t end_record = XenosZPDReport::GetEndRecordBase(slot_base);
XenosReportController::BeginReportResult begin_report_result =
zpd_report_controller_->BeginReport(report_address, begin_record);
XenosReportController::ReportHandle report_handle =
begin_report_result.report_handle;
if (report_handle == XenosReportController::kInvalidReportHandle) {
if (!slot_base) {
return false;
}
// Bump slot sequence — invalidates pending writes from prior lifetime.
uint64_t slot_sequence_id = ++zpd_slot_sequences_[slot_base];
ReportHandle report_handle = zpd_next_report_handle_++;
if (report_handle == kInvalidReportHandle) {
report_handle = zpd_next_report_handle_++;
}
ZPDReport& logical = logical_zpd_reports_[report_handle];
logical.slot_base = slot_base;
logical.slot_sequence_id = slot_sequence_id;
logical.begin_record = begin_record;
logical.end_record = end_record;
logical.begin_value = begin_report_result.begin_value;
logical.begin_value = zpd_slot_values_[slot_base];
logical.accumulated_samples = 0;
logical.last_segment_end_submission = 0;
logical.pending_segments = 0;
@@ -1118,7 +1119,7 @@ bool CommandProcessor::EndZPDReport(uint32_t report_address,
return false;
}
XenosReportController::ReportHandle report_handle =
CommandProcessor::ReportHandle report_handle =
zpd_active_segment_.report_handle;
uint32_t stored_end_record = zpd_active_segment_.end_record;
uint32_t report_record_base = XenosZPDReport::GetRecordBase(report_address);
@@ -1186,10 +1187,9 @@ bool CommandProcessor::EndZPDReport(uint32_t report_address,
}
}
zpd_report_controller_->QueueReportWrite(report_record_base, report_handle);
if (resolved_immediately) {
zpd_report_controller_->SetReportResolved(report_handle, final_value);
zpd_report_controller_->RetireReports();
CommitZPDReport(logical, final_value);
logical_zpd_reports_.erase(it);
}
bool has_cross_slot_end =
@@ -1208,8 +1208,7 @@ bool CommandProcessor::EndZPDReport(uint32_t report_address,
} else if (!resolved_immediately) {
PumpQueryResolves();
if (zpd_report_controller_->HasQueuedWriteForAddress(report_record_base) &&
zpd_pending_retire_handle_ != report_handle) {
if (zpd_pending_retire_handle_ != report_handle) {
zpd_pending_retire_handle_ = report_handle;
zpd_pending_retire_stalls_ = 0;
}
@@ -1375,18 +1374,15 @@ void CommandProcessor::DrainQueryResolves(uint64_t completed_submission) {
}
fast_zpd_report_cached_values_[logical.end_record] = final_value;
}
zpd_report_controller_->SetReportResolved(resolve.report_handle,
final_value);
any_resolved = true;
if (IsZPDReportCurrent(logical)) {
CommitZPDReport(logical, final_value);
}
logical_zpd_reports_.erase(it);
}
}
}
}
}
if (any_resolved) {
zpd_report_controller_->RetireReports();
}
}
void CommandProcessor::PumpQueryResolves() {
@@ -1408,7 +1404,7 @@ void CommandProcessor::PumpQueryResolves() {
}
bool CommandProcessor::AwaitQueryResolve(
XenosReportController::ReportHandle report_handle) {
CommandProcessor::ReportHandle report_handle) {
if (GetZPDMode() == ZPDMode::kFake) {
return false;
}
@@ -1430,7 +1426,12 @@ bool CommandProcessor::AwaitQueryResolve(
uint64_t wait_for_submission = it->second.last_segment_end_submission;
if (wait_for_submission == 0) {
if (it->second.pending_segments == 0 && it->second.ended) {
zpd_report_controller_->RetireReports();
// Already fully resolved — commit if the slot is still current.
if (IsZPDReportCurrent(it->second)) {
CommitZPDReport(it->second,
NormalizeSampleCount(it->second.accumulated_samples));
}
logical_zpd_reports_.erase(it);
return true;
}
return false;
@@ -1466,16 +1467,15 @@ bool CommandProcessor::AwaitQueryResolve(
void CommandProcessor::PumpPendingRetire() {
if (zpd_batch_fake_) {
zpd_pending_retire_handle_ = XenosReportController::kInvalidReportHandle;
zpd_pending_retire_handle_ = kInvalidReportHandle;
zpd_pending_retire_stalls_ = 0;
return;
}
XenosReportController::ReportHandle handle_to_await =
zpd_pending_retire_handle_;
CommandProcessor::ReportHandle handle_to_await = zpd_pending_retire_handle_;
if (AwaitQueryResolve(handle_to_await)) {
zpd_pending_retire_handle_ = XenosReportController::kInvalidReportHandle;
zpd_pending_retire_handle_ = kInvalidReportHandle;
zpd_pending_retire_stalls_ = 0;
return;
}
@@ -1484,7 +1484,7 @@ void CommandProcessor::PumpPendingRetire() {
if (logical_report == logical_zpd_reports_.end()) {
// If the report is already gone it retired through another path.
// Clear so we don't spin on a handle that no longer exists.
zpd_pending_retire_handle_ = XenosReportController::kInvalidReportHandle;
zpd_pending_retire_handle_ = kInvalidReportHandle;
zpd_pending_retire_stalls_ = 0;
return;
}
@@ -1498,14 +1498,13 @@ void CommandProcessor::PumpPendingRetire() {
"handle={}, abandoning",
handle_to_await);
}
// Notify the controller so it can retire the report and clean up its
// internal maps. Use the cached delta to avoid a sudden occlusion flash.
if (zpd_report_controller_) {
zpd_report_controller_->SetReportResolved(
handle_to_await, logical_report->second.cached_delta);
// Write the cached delta to guest memory to avoid a sudden occlusion flash.
if (IsZPDReportCurrent(logical_report->second)) {
CommitZPDReport(logical_report->second,
logical_report->second.cached_delta);
}
logical_zpd_reports_.erase(logical_report);
zpd_pending_retire_handle_ = XenosReportController::kInvalidReportHandle;
zpd_pending_retire_handle_ = kInvalidReportHandle;
zpd_pending_retire_stalls_ = 0;
}
}
@@ -1530,20 +1529,25 @@ void CommandProcessor::WriteZPDReport(uint32_t begin_record,
write_begin_record);
}
void CommandProcessor::ZPDReportCallback(
XenosReportController::ReportHandle report_handle, uint32_t slot_base,
uint32_t begin_record, uint32_t begin_value, uint32_t delta_value,
void* callback_context) {
CommandProcessor* processor =
reinterpret_cast<CommandProcessor*>(callback_context);
void CommandProcessor::CommitZPDReport(ZPDReport& report,
uint32_t delta_value) {
uint32_t end_record = XenosZPDReport::GetEndRecordBase(report.slot_base);
WriteZPDReport(report.begin_record, end_record, report.begin_value,
delta_value, report.begin_record != 0);
uint32_t end_record = XenosZPDReport::GetEndRecordBase(slot_base);
// Advance running total so the next BeginReport on this slot picks up
// the correct begin_value.
uint64_t end_value = static_cast<uint64_t>(report.begin_value) +
static_cast<uint64_t>(delta_value);
zpd_slot_values_[report.slot_base] =
end_value > UINT32_MAX ? UINT32_MAX : static_cast<uint32_t>(end_value);
}
// begin_record comes from the controller. Erase the CP-side entry.
processor->logical_zpd_reports_.erase(report_handle);
processor->WriteZPDReport(begin_record, end_record, begin_value, delta_value,
begin_record != 0);
bool CommandProcessor::IsZPDReportCurrent(const ZPDReport& report) const {
auto seq_it = zpd_slot_sequences_.find(report.slot_base);
uint64_t current_seq =
seq_it != zpd_slot_sequences_.end() ? seq_it->second : 0;
return current_seq == report.slot_sequence_id;
}
uint32_t CommandProcessor::NormalizeSampleCount(uint64_t samples) const {
+24 -17
View File
@@ -26,7 +26,6 @@
#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"
@@ -117,6 +116,10 @@ enum class GammaRampType {
};
class CommandProcessor {
public:
using ReportHandle = uint32_t;
static constexpr ReportHandle kInvalidReportHandle = 0;
protected:
RingBuffer
reader_; // chrispy: instead of having ringbuffer on stack, have it near
@@ -332,9 +335,11 @@ class CommandProcessor {
// Raw host count across all segments, normalized at retirement.
uint64_t accumulated_samples = 0;
uint64_t last_segment_end_submission = 0;
uint64_t slot_sequence_id = 0;
uint32_t slot_base = 0;
uint32_t begin_record = 0;
uint32_t end_record = 0;
// Snapshotted from controller at BEGIN.
// Snapshotted at BEGIN from zpd_slot_values_.
uint32_t begin_value = 0;
uint32_t pending_segments = 0;
// Last known delta. Carried forward on forced close so slot doesn't
@@ -345,8 +350,7 @@ class CommandProcessor {
// TODO(boma): Replace with a map keyed by slot_base for concurrent slots.
struct ActiveZPDSegment {
XenosReportController::ReportHandle report_handle =
XenosReportController::kInvalidReportHandle;
ReportHandle report_handle = kInvalidReportHandle;
uint32_t slot_base = 0;
uint32_t begin_record = 0;
uint32_t end_record = 0;
@@ -361,8 +365,7 @@ class CommandProcessor {
uint64_t submission = 0;
uint32_t query_index = UINT32_MAX;
uint32_t query_generation = 0;
XenosReportController::ReportHandle report_handle =
XenosReportController::kInvalidReportHandle;
ReportHandle report_handle = kInvalidReportHandle;
};
// Logged by the backend every 100 frames if ZPD logging cvar is true.
@@ -445,7 +448,7 @@ class CommandProcessor {
// 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);
bool AwaitQueryResolve(ReportHandle report_handle);
// Writes guest report with begin_value read from guest memory.
// Orphan END path only when no controller snapshot is available.
@@ -460,13 +463,17 @@ class CommandProcessor {
// 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_record, uint32_t begin_value, uint32_t delta_value,
void* callback_context);
// Writes the final report to guest memory and advances the slot running
// total. Called when a report fully resolves or is abandoned.
void CommitZPDReport(ZPDReport& report, uint32_t delta_value);
// Checks that the report's slot sequence is still current (not reused).
bool IsZPDReportCurrent(const ZPDReport& report) const;
void ResetZPDState() {
zpd_active_segment_ = {};
zpd_next_report_handle_ = 1;
zpd_slot_sequences_.clear();
zpd_slot_values_.clear();
logical_zpd_reports_.clear();
fast_zpd_report_cached_values_.clear();
zpd_batch_fake_ = false;
@@ -475,7 +482,7 @@ class CommandProcessor {
zpd_batch_last_record_ = 0;
zpd_batch_run_ = 0;
fake_zpd_sample_count_ = 0;
zpd_pending_retire_handle_ = XenosReportController::kInvalidReportHandle;
zpd_pending_retire_handle_ = kInvalidReportHandle;
zpd_pending_retire_stalls_ = 0;
zpd_resolves_in_flight_.clear();
}
@@ -515,9 +522,10 @@ 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_;
ReportHandle zpd_next_report_handle_ = 1;
std::unordered_map<uint32_t, uint64_t> zpd_slot_sequences_;
std::unordered_map<uint32_t, uint32_t> zpd_slot_values_;
std::unordered_map<ReportHandle, ZPDReport> logical_zpd_reports_;
ActiveZPDSegment zpd_active_segment_{};
std::deque<PendingQueryResolve> zpd_resolves_in_flight_;
@@ -534,8 +542,7 @@ class CommandProcessor {
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;
ReportHandle zpd_pending_retire_handle_ = kInvalidReportHandle;
uint32_t zpd_pending_retire_stalls_ = 0;
// Set by the backend when resolution scale changes.
@@ -28,7 +28,6 @@
#include "xenia/gpu/packet_disassembler.h"
#include "xenia/gpu/registers.h"
#include "xenia/gpu/xenos.h"
#include "xenia/gpu/xenos_report_controller.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/ui/d3d12/d3d12_presenter.h"
#include "xenia/ui/d3d12/d3d12_util.h"
@@ -3908,20 +3907,15 @@ bool D3D12CommandProcessor::BeginSubmission(bool is_guest_command) {
// Log guest ZPD report stats every 100 frames.
if (GetZPDMode() != ZPDMode::kFake && cvars::occlusion_query_log &&
zpd_host_query_pool_ && zpd_host_query_pool_->capacity() &&
zpd_report_controller_ &&
frame_current_ - zpd_stats_.last_log_frame >= 100) {
XenosReportController::Stats report_stats =
zpd_report_controller_->stats();
XELOGI(
"Occlusion Query Stats (last 100 frames): "
"LogicalBegun={}, LogicalEnded={}, SegBegun={}, SegEnded={}, "
"WritesRetired={}, PoolExhausted={}, Failed={}",
"PoolExhausted={}, Failed={}",
zpd_stats_.logical_begun, zpd_stats_.logical_ended,
zpd_stats_.segments_begun, zpd_stats_.segments_ended,
report_stats.writes_retired, zpd_stats_.pool_exhausted,
zpd_stats_.failed);
zpd_stats_.pool_exhausted, zpd_stats_.failed);
zpd_report_controller_->ResetStats();
zpd_stats_.Reset(frame_current_);
}
@@ -1257,7 +1257,7 @@ bool COMMAND_PROCESSOR::ExecutePacketType3_EVENT_WRITE_ZPD(
// 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_handle_ = CommandProcessor::kInvalidReportHandle;
zpd_pending_retire_stalls_ = 0;
XELOGI(
"ZPD: Batched occlusion query pattern detected, falling back to "
@@ -1271,8 +1271,7 @@ bool COMMAND_PROCESSOR::ExecutePacketType3_EVENT_WRITE_ZPD(
zpd_batch_run_ = 0;
}
if (COMMAND_PROCESSOR::GetZPDMode() != ZPDMode::kFake &&
zpd_report_controller_) {
if (COMMAND_PROCESSOR::GetZPDMode() != ZPDMode::kFake) {
if (logical_active && is_end_record) {
if (slot_base == zpd_active_segment_.slot_base) {
COMMAND_PROCESSOR::EndZPDReport(report_address, false);
@@ -1302,7 +1301,7 @@ bool COMMAND_PROCESSOR::ExecutePacketType3_EVENT_WRITE_ZPD(
cached_delta, false);
} else {
// In strict mode, just pump in case a previous report has resolved.
zpd_report_controller_->RetireReports();
COMMAND_PROCESSOR::PumpQueryResolves();
}
return true;
}
@@ -32,7 +32,6 @@
#include "xenia/gpu/vulkan/vulkan_shared_memory.h"
#include "xenia/gpu/vulkan/vulkan_zpd_query_pool.h"
#include "xenia/gpu/xenos.h"
#include "xenia/gpu/xenos_report_controller.h"
#include "xenia/kernel/kernel_state.h"
#include "xenia/kernel/user_module.h"
#include "xenia/ui/vulkan/vulkan_presenter.h"
@@ -5097,20 +5096,15 @@ bool VulkanCommandProcessor::BeginSubmission(bool is_guest_command) {
// Log guest ZPD report stats every 100 frames.
if (GetZPDMode() != ZPDMode::kFake && cvars::occlusion_query_log &&
zpd_host_query_pool_ && zpd_host_query_pool_->capacity() &&
zpd_report_controller_ &&
frame_current_ - zpd_stats_.last_log_frame >= 100) {
XenosReportController::Stats report_stats =
zpd_report_controller_->stats();
XELOGI(
"Occlusion Query Stats (last 100 frames): "
"LogicalBegun={}, LogicalEnded={}, SegBegun={}, SegEnded={}, "
"WritesRetired={}, PoolExhausted={}, Failed={}",
"PoolExhausted={}, Failed={}",
zpd_stats_.logical_begun, zpd_stats_.logical_ended,
zpd_stats_.segments_begun, zpd_stats_.segments_ended,
report_stats.writes_retired, zpd_stats_.pool_exhausted,
zpd_stats_.failed);
zpd_stats_.pool_exhausted, zpd_stats_.failed);
zpd_report_controller_->ResetStats();
zpd_stats_.Reset(frame_current_);
}
-215
View File
@@ -1,215 +0,0 @@
/**
******************************************************************************
* 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, uint32_t begin_record) {
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.begin_record = begin_record;
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_record, 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_record, 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
@@ -1,132 +0,0 @@
/**
******************************************************************************
* 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_record,
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, uint32_t begin_record);
// 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_record = 0;
uint32_t begin_value = 0;
uint32_t delta_value = 0;
};
struct LogicalReportState {
uint32_t slot_base = 0;
uint32_t begin_record = 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_