graphics: count occlusion queries on the host GPU behind --occlusion-queries

This commit is contained in:
nmzik
2026-10-23 18:51:35 +02:00
parent ba0e93daac
commit 246d511568
18 changed files with 288 additions and 14 deletions
+19 -13
View File
@@ -237,6 +237,8 @@ void CommandProcessor::BufferInit() {
void CommandProcessor::BufferFlush() {
GetScheduler().Flush();
// The occlusion counters reach a guest that polls them without waiting for a later fence.
m_renderer.GetOcclusionQueries().Publish();
}
void CommandProcessor::BufferWait() {
@@ -803,6 +805,7 @@ void CommandProcessor::SetPredication(uint32_t condition, uint32_t op, uint32_t
return;
case 0x01: {
EXIT_NOT_IMPLEMENTED(address == nullptr);
m_renderer.GetOcclusionQueries().Publish();
// One begin/end pair per DB; bit 63 marks each counter ready.
constexpr uint64_t ready_bit = 1ull << 63u;
const auto* results = reinterpret_cast<const volatile uint64_t*>(address);
@@ -1047,6 +1050,8 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
EXIT_NOT_IMPLEMENTED(cache_policy != 0x00000000);
EXIT_NOT_IMPLEMENTED(event_write_dest != 0x00000000);
// Dumped occlusion counters are written before any end-of-pipe signal reaches the guest.
m_renderer.GetOcclusionQueries().Publish();
bool with_interrupt = false;
switch (interrupt_selector) {
@@ -1213,6 +1218,7 @@ void CommandProcessor::WriteAtEndOfPipe64(uint32_t cache_policy, uint32_t event_
}
void CommandProcessor::TriggerEopEventAtEndOfPipe(uint32_t interrupt_context_id) {
m_renderer.GetOcclusionQueries().Publish();
(void)CurrentBuffer().Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, event_id = m_interrupt_event_id, interrupt_context_id] {
@@ -1270,15 +1276,19 @@ void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
"\n",
event_index, event_address);
}
if (auto& queries = m_renderer.GetOcclusionQueries(); queries.Enabled()) {
queries.Dump(event_address);
break;
}
static std::once_flag warning_once;
std::call_once(warning_once, [] {
std::printf("Warning: game uses occlusion queries, which are currently treated as "
"always visible; GPU usage may be higher and FPS may be lower.\n");
std::printf("Warning: game uses occlusion queries, which are treated as always "
"visible without --occlusion-queries; GPU usage may be higher and FPS "
"may be lower.\n");
});
// Until host occlusion queries are implemented, publish an always-visible result. The
// PS5 layout contains one interleaved begin/end pair per DB, and bit 63 marks a result
// ready.
// Without host occlusion queries, publish an always-visible result. The PS5 layout
// contains one interleaved begin/end pair per DB, and bit 63 marks a result ready.
constexpr uint64_t ready_bit = 1ull << 63u;
constexpr uint64_t counter_mask = ready_bit - 1u;
auto* results = reinterpret_cast<volatile uint64_t*>(event_address);
@@ -1298,6 +1308,7 @@ void CommandProcessor::Flip() {
if (GraphicsRunDebugDumpEnabled()) {
LOGF("CommandProcessor::Flip()\n");
}
m_renderer.GetOcclusionQueries().Publish();
auto& command = CurrentBuffer();
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
@@ -1309,8 +1320,6 @@ void CommandProcessor::Flip() {
}
void CommandProcessor::Flip(void* dst_gpu_addr, uint32_t value) {
auto& command = CurrentBuffer();
if (GraphicsRunDebugDumpEnabled()) {
LOGF("CommandProcessor::Flip()\n"
"\t dst_gpu_addr = 0x%016" PRIx64 "\n"
@@ -1319,13 +1328,9 @@ void CommandProcessor::Flip(void* dst_gpu_addr, uint32_t value) {
}
EXIT_IF(dst_gpu_addr == nullptr);
m_renderer.GetOcclusionQueries().Publish();
std::memcpy(dst_gpu_addr, &value, sizeof(value));
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg);
(void)command.Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, request] { renderer.GetVideoOut().CompleteFlip(request); });
GetScheduler().Flush();
Flip();
}
void CommandProcessor::FlipWithInterrupt(uint32_t eop_event_type, uint32_t cache_action,
@@ -1345,6 +1350,7 @@ void CommandProcessor::FlipWithInterrupt(uint32_t eop_event_type, uint32_t cache
EXIT("unknown event type\n");
}
EXIT_IF(dst_gpu_addr == nullptr);
m_renderer.GetOcclusionQueries().Publish();
std::memcpy(dst_gpu_addr, &value, sizeof(value));
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg);
+2
View File
@@ -36,6 +36,8 @@ struct GraphicContext {
bool provoking_vertex_last_enabled = false;
bool maintenance8_enabled = false;
bool conditional_rendering_enabled = false;
bool occlusion_queries_enabled = false;
bool occlusion_query_precise_enabled = false;
bool supports_block_texel_view = false;
bool mesh_shader_enabled = false;
vk::PhysicalDeviceMeshShaderPropertiesEXT mesh_shader_properties = {};
@@ -0,0 +1,82 @@
#include "graphics/host_gpu/renderer/occlusionQueries.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/commandScheduler.h"
#include <algorithm>
namespace Libs::Graphics {
// Queries no dump has read yet; past them a draw counts one sample.
constexpr uint32_t QUERY_COUNT = 16384;
OcclusionQueries::OcclusionQueries(GraphicContext& graphics, CommandScheduler& scheduler)
: m_scheduler(scheduler) {
if (!graphics.occlusion_queries_enabled) {
return;
}
m_device = graphics.device;
const vk::QueryPoolCreateInfo info {.queryType = vk::QueryType::eOcclusion,
.queryCount = QUERY_COUNT};
RequireVulkanSuccess(m_device.createQueryPool(&info, nullptr, &m_pool), "vkCreateQueryPool");
m_device.resetQueryPool(m_pool, 0, QUERY_COUNT);
m_results.resize(QUERY_COUNT);
if (graphics.occlusion_query_precise_enabled) {
m_control = vk::QueryControlFlagBits::ePrecise;
}
}
OcclusionQueries::~OcclusionQueries() {
if (m_pool) {
m_device.destroyQueryPool(m_pool, nullptr);
}
}
int32_t OcclusionQueries::BeginQuery(vk::CommandBuffer command) {
if (m_begun - m_read == QUERY_COUNT) {
m_uncounted++;
return -1;
}
const auto query = static_cast<uint32_t>(m_begun++ % QUERY_COUNT);
command.beginQuery(m_pool, query, m_control);
return static_cast<int32_t>(query);
}
void OcclusionQueries::Dump(uint64_t address) {
// Begins dump to the results, ends 8 bytes into them.
if ((address & 8u) == 0) {
m_open++;
} else if (m_open != 0) {
m_open--;
}
m_dumps.push_back({address, m_scheduler.CurrentTick(), m_begun, m_uncounted});
}
void OcclusionQueries::PublishDumps() {
if (const auto last = m_dumps.back().queries; last != m_read) {
m_scheduler.Wait(m_dumps.back().tick);
// The unread queries wrap around the pool at most once.
for (uint64_t read = m_read; read < last;) {
const auto first = static_cast<uint32_t>(read % QUERY_COUNT);
const auto count =
static_cast<uint32_t>(std::min<uint64_t>(last - read, QUERY_COUNT - first));
RequireVulkanSuccess(
m_device.getQueryPoolResults(m_pool, first, count, count * sizeof(uint64_t),
m_results.data() + first, sizeof(uint64_t),
vk::QueryResultFlagBits::e64),
"vkGetQueryPoolResults");
m_device.resetQueryPool(m_pool, first, count);
read += count;
}
}
for (const auto& dump: m_dumps) {
for (; m_read < dump.queries; m_read++) {
m_samples += m_results[m_read % QUERY_COUNT];
}
WriteZPassCounters(reinterpret_cast<volatile uint64_t*>(dump.address),
m_samples + dump.uncounted, (dump.address & 8u) == 0);
}
m_dumps.clear();
}
} // namespace Libs::Graphics
@@ -0,0 +1,85 @@
#ifndef EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_OCCLUSIONQUERIES_H_
#define EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_OCCLUSIONQUERIES_H_
#include "common/common.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include <cstdint>
#include <vector>
namespace Libs::Graphics {
class CommandScheduler;
struct GraphicContext;
// The guest's query results (16-byte aligned): a begin/end pair of running z-pass counters per DB,
// 16 bytes apart; bit 63 marks a written counter. A query's samples are the sum of end - begin.
constexpr uint32_t OCCLUSION_QUERY_DBS = 16;
constexpr uint64_t OCCLUSION_QUERY_READY = 1ull << 63u;
// A dump of the running count, all of it in DB 0. A begin also clears its end counters.
inline void WriteZPassCounters(volatile uint64_t* results, uint64_t samples, bool begin) {
for (uint32_t db = 0; db < OCCLUSION_QUERY_DBS; db++) {
results[db * 2] = OCCLUSION_QUERY_READY | (db == 0 ? samples : 0);
if (begin) {
results[db * 2 + 1] = 0;
}
}
}
// The guest's z-pass counters from host occlusion queries: a draw counts its samples while a guest
// query is open, and the dumps' counters are written before the guest can see the GPU ran them.
class OcclusionQueries {
public:
OcclusionQueries(GraphicContext& graphics, CommandScheduler& scheduler);
~OcclusionQueries();
KYTY_CLASS_NO_COPY(OcclusionQueries);
[[nodiscard]] bool Enabled() const { return m_pool != nullptr; }
// In the draw's rendering: the query that counts its samples, or -1.
[[nodiscard]] int32_t Begin(vk::CommandBuffer command) {
return m_open == 0 ? -1 : BeginQuery(command);
}
void End(vk::CommandBuffer command, int32_t query) const {
if (query >= 0) {
command.endQuery(m_pool, static_cast<uint32_t>(query));
}
}
// A PIXEL_PIPE_STAT_DUMP to address, after the draws recorded so far.
void Dump(uint64_t address);
// Waits for the GPU to run the dumps and writes their counters.
void Publish() {
if (!m_dumps.empty()) {
PublishDumps();
}
}
private:
struct PendingDump {
uint64_t address;
uint64_t tick;
uint64_t queries;
uint64_t uncounted;
};
int32_t BeginQuery(vk::CommandBuffer command);
void PublishDumps();
CommandScheduler& m_scheduler;
vk::Device m_device;
vk::QueryPool m_pool;
vk::QueryControlFlags m_control;
std::vector<PendingDump> m_dumps;
std::vector<uint64_t> m_results;
uint32_t m_open = 0;
uint64_t m_begun = 0;
uint64_t m_read = 0;
uint64_t m_samples = 0;
uint64_t m_uncounted = 0;
};
} // namespace Libs::Graphics
#endif // EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_OCCLUSIONQUERIES_H_
@@ -16,7 +16,7 @@ RenderContext::RenderContext(GraphicContext& graphics)
m_pipeline_cache(graphics, &m_image_heap), m_sampler_cache(graphics),
m_buffer_cache(graphics, m_command_scheduler, m_page_manager, m_texture_cache),
m_texture_cache(graphics, m_command_scheduler, m_page_manager, m_buffer_cache),
m_image_heap(*this, m_page_manager) {
m_image_heap(*this, m_page_manager), m_occlusion_queries(graphics, m_command_scheduler) {
EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
}
@@ -11,6 +11,7 @@
#include "graphics/host_gpu/renderer/cache/samplerCache.h"
#include "graphics/host_gpu/renderer/cache/textureCache.h"
#include "graphics/host_gpu/renderer/commandScheduler.h"
#include "graphics/host_gpu/renderer/occlusionQueries.h"
#include "graphics/host_gpu/renderer/pipeline/descriptorHeap.h"
#include "graphics/host_gpu/renderer/pipeline/imageHeap.h"
#include "graphics/host_gpu/renderer/pipeline/pipelineCache.h"
@@ -49,6 +50,7 @@ public:
BufferCache& GetBufferCache() { return m_buffer_cache; }
TextureCache& GetTextureCache() { return m_texture_cache; }
RenderExecutor& GetRenderExecutor() { return m_render_executor; }
OcclusionQueries& GetOcclusionQueries() { return m_occlusion_queries; }
[[nodiscard]] bool HandleFault(PageFaultAccess access, uint64_t fault_vaddr) noexcept;
[[nodiscard]] bool InvalidateMemory(uint64_t vaddr, uint64_t size);
@@ -79,6 +81,7 @@ private:
BufferCache m_buffer_cache;
TextureCache m_texture_cache;
ImageHeap m_image_heap;
OcclusionQueries m_occlusion_queries;
mutable std::shared_mutex m_mapped_ranges_mutex;
RangeSet m_mapped_ranges;
std::unique_ptr<GuestGpu> m_gpu;
@@ -1284,6 +1284,8 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
if (!draw.IsIndexed()) {
SetDrawDebugPhase(buffer, submit_id, draw, 0x500u);
}
auto& occlusion_queries = m_context.GetOcclusionQueries();
const int32_t occlusion_query = occlusion_queries.Begin(vk_buffer);
if (mesh_active) {
vk_buffer.drawMeshTasksEXT(mesh_groups, draw.instance_count, 1);
} else if (args_buffer.first == nullptr) {
@@ -1295,6 +1297,7 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
vk_buffer.drawIndirect(args_buffer.first->Handle(), args_buffer.second,
emit.indirect.count, emit.indirect.stride);
}
occlusion_queries.End(vk_buffer, occlusion_query);
Common::DebugCounters::Add(Common::DebugCounters::Counter::Draws);
Common::DebugCounters::Add(Common::DebugCounters::Counter::DrawInstances, draw.instance_count);
@@ -655,6 +655,16 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
vk::SampleCountFlagBits::e1;
}
graphics.sample_rate_shading_enabled = true;
// Occlusion queries reset their queries from the host and count samples exactly where they can.
graphics.occlusion_queries_enabled =
Config::OcclusionQueriesEnabled() && supported_features12.hostQueryReset;
graphics.occlusion_query_precise_enabled =
graphics.occlusion_queries_enabled && supported_features2.features.occlusionQueryPrecise;
features12.hostQueryReset = graphics.occlusion_queries_enabled;
device_features.occlusionQueryPrecise = graphics.occlusion_query_precise_enabled;
LOGF("Vulkan occlusion queries: %s, precise: %s\n",
graphics.occlusion_queries_enabled ? "true" : "false",
graphics.occlusion_query_precise_enabled ? "true" : "false");
device_features.shaderInt64 = VK_TRUE;
device_features.shaderFloat64 =
supported_features2.features.shaderFloat64 &&