diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c5781c24..90be68252 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -549,6 +549,11 @@ add_executable(jpeg_enc_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/JpegEncTests.c target_link_libraries(jpeg_enc_tests common fmt::fmt) target_include_directories(jpeg_enc_tests PRIVATE ${inc_headers}) +add_executable(occlusion_queries_tests EXCLUDE_FROM_ALL + "${KYTY_TESTS_DIR}/OcclusionQueriesTests.cpp") +target_link_libraries(occlusion_queries_tests common fmt::fmt Vulkan::Headers) +target_include_directories(occlusion_queries_tests PRIVATE ${inc_headers}) + add_executable(ces_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/CesTests.cpp") target_link_libraries(ces_tests common fmt::fmt SDL3::SDL3-static) target_include_directories(ces_tests PRIVATE ${inc_headers}) @@ -662,6 +667,7 @@ if(BUILD_TESTING) add_test(NAME png_enc COMMAND $) add_test(NAME jpeg_dec COMMAND $) add_test(NAME jpeg_enc COMMAND $) + add_test(NAME occlusion_queries COMMAND $) add_test(NAME ces COMMAND $) add_test(NAME save_data_dialog COMMAND $) add_test(NAME http_uri_parse COMMAND $) @@ -754,6 +760,7 @@ if(BUILD_TESTING) png_enc_tests jpeg_dec_tests jpeg_enc_tests + occlusion_queries_tests ces_tests save_data_dialog_tests http_uri_parse_tests diff --git a/src/common/emulatorConfig.cpp b/src/common/emulatorConfig.cpp index 218dcaf0f..1bb24787f 100644 --- a/src/common/emulatorConfig.cpp +++ b/src/common/emulatorConfig.cpp @@ -197,6 +197,10 @@ bool TessellationEnabled() { return g_config->tessellation_enabled; } +bool OcclusionQueriesEnabled() { + return g_config->occlusion_queries_enabled; +} + bool TrophyEnabled() { return g_config->trophy_enabled; } diff --git a/src/common/emulatorConfig.h b/src/common/emulatorConfig.h index 71d54de0b..2671d6bff 100644 --- a/src/common/emulatorConfig.h +++ b/src/common/emulatorConfig.h @@ -88,6 +88,7 @@ struct ConfigOptions { bool readback_tiled_images = false; bool sync_raw_image_buffers = false; bool tessellation_enabled = false; + bool occlusion_queries_enabled = false; bool trophy_enabled = true; uint32_t trophy_duration_seconds = 6; bool perf_overlay_enabled = false; @@ -154,6 +155,7 @@ bool ReadbackLinearImagesEnabled(); bool ReadbackTiledImagesEnabled(); bool SyncRawImageBuffersEnabled(); bool TessellationEnabled(); +bool OcclusionQueriesEnabled(); bool TrophyEnabled(); uint32_t GetTrophyDurationSeconds(); bool PlayGoHackEnabled(); diff --git a/src/graphics/guest_gpu/graphicsRun.cpp b/src/graphics/guest_gpu/graphicsRun.cpp index 1cc557ef2..945bdf3a4 100644 --- a/src/graphics/guest_gpu/graphicsRun.cpp +++ b/src/graphics/guest_gpu/graphicsRun.cpp @@ -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(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(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); diff --git a/src/graphics/host_gpu/graphicContext.h b/src/graphics/host_gpu/graphicContext.h index c4c56039c..a80af2064 100644 --- a/src/graphics/host_gpu/graphicContext.h +++ b/src/graphics/host_gpu/graphicContext.h @@ -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 = {}; diff --git a/src/graphics/host_gpu/renderer/occlusionQueries.cpp b/src/graphics/host_gpu/renderer/occlusionQueries.cpp new file mode 100644 index 000000000..6d9be4339 --- /dev/null +++ b/src/graphics/host_gpu/renderer/occlusionQueries.cpp @@ -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 + +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(m_begun++ % QUERY_COUNT); + command.beginQuery(m_pool, query, m_control); + return static_cast(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(read % QUERY_COUNT); + const auto count = + static_cast(std::min(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(dump.address), + m_samples + dump.uncounted, (dump.address & 8u) == 0); + } + m_dumps.clear(); +} + +} // namespace Libs::Graphics diff --git a/src/graphics/host_gpu/renderer/occlusionQueries.h b/src/graphics/host_gpu/renderer/occlusionQueries.h new file mode 100644 index 000000000..904729ad0 --- /dev/null +++ b/src/graphics/host_gpu/renderer/occlusionQueries.h @@ -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 +#include + +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(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 m_dumps; + std::vector 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_ diff --git a/src/graphics/host_gpu/renderer/renderContext.cpp b/src/graphics/host_gpu/renderer/renderContext.cpp index 2cb9e75b0..90a54a598 100644 --- a/src/graphics/host_gpu/renderer/renderContext.cpp +++ b/src/graphics/host_gpu/renderer/renderContext.cpp @@ -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()); } diff --git a/src/graphics/host_gpu/renderer/renderContext.h b/src/graphics/host_gpu/renderer/renderContext.h index 665e4516a..aeff01afd 100644 --- a/src/graphics/host_gpu/renderer/renderContext.h +++ b/src/graphics/host_gpu/renderer/renderContext.h @@ -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 m_gpu; diff --git a/src/graphics/host_gpu/renderer/renderDraw.cpp b/src/graphics/host_gpu/renderer/renderDraw.cpp index 6f980aee3..97d38562b 100644 --- a/src/graphics/host_gpu/renderer/renderDraw.cpp +++ b/src/graphics/host_gpu/renderer/renderDraw.cpp @@ -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); diff --git a/src/graphics/presentation/window/vulkanWindow.cpp b/src/graphics/presentation/window/vulkanWindow.cpp index f0b8598b9..c1704ed8c 100644 --- a/src/graphics/presentation/window/vulkanWindow.cpp +++ b/src/graphics/presentation/window/vulkanWindow.cpp @@ -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 && diff --git a/src/launcher/forms/configuration_edit_dialog.ui b/src/launcher/forms/configuration_edit_dialog.ui index 9845e149d..5f9f5c9f0 100644 --- a/src/launcher/forms/configuration_edit_dialog.ui +++ b/src/launcher/forms/configuration_edit_dialog.ui @@ -180,6 +180,12 @@ + + Count the samples of occlusion queries on the GPU, so hidden lights and objects stay hidden. Costs some performance. + Enable occlusion queries + + + Enable HDR10 output for supported games. Requires HDR enabled on the host display. Enable HDR display support diff --git a/src/launcher/include/configuration.h b/src/launcher/include/configuration.h index bb2560a3d..b1a0c07c4 100644 --- a/src/launcher/include/configuration.h +++ b/src/launcher/include/configuration.h @@ -129,6 +129,7 @@ public: bool readback_tiled_images = false; bool sync_raw_image_buffers = false; bool tessellation_enabled = false; + bool occlusion_queries_enabled = false; bool trophy_enabled = true; int trophy_duration_seconds = 6; bool perf_overlay_enabled = false; @@ -169,6 +170,7 @@ public: readback_tiled_images = other.readback_tiled_images; sync_raw_image_buffers = other.sync_raw_image_buffers; tessellation_enabled = other.tessellation_enabled; + occlusion_queries_enabled = other.occlusion_queries_enabled; trophy_enabled = other.trophy_enabled; trophy_duration_seconds = other.trophy_duration_seconds; perf_overlay_enabled = other.perf_overlay_enabled; diff --git a/src/launcher/src/configuration.cpp b/src/launcher/src/configuration.cpp index e64f9ba76..de01d6c5e 100644 --- a/src/launcher/src/configuration.cpp +++ b/src/launcher/src/configuration.cpp @@ -16,6 +16,7 @@ QVariantMap Configuration::GameSettings() const { KYTY_CFG_SET(readback_tiled_images); KYTY_CFG_SET(sync_raw_image_buffers); KYTY_CFG_SET(tessellation_enabled); + KYTY_CFG_SET(occlusion_queries_enabled); KYTY_CFG_SET(trophy_enabled); KYTY_CFG_SET(trophy_duration_seconds); KYTY_CFG_SET(perf_overlay_enabled); @@ -66,6 +67,7 @@ void Configuration::ReadGameSettingsValues(const Settings& s) { KYTY_CFG_GET(readback_tiled_images); KYTY_CFG_GET(sync_raw_image_buffers); KYTY_CFG_GET(tessellation_enabled); + KYTY_CFG_GET(occlusion_queries_enabled); trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool(); KYTY_CFG_GET(trophy_duration_seconds); KYTY_CFG_GET(perf_overlay_enabled); diff --git a/src/launcher/src/configurationEditDialog.cpp b/src/launcher/src/configurationEditDialog.cpp index fa8caeab0..70ae716ce 100644 --- a/src/launcher/src/configurationEditDialog.cpp +++ b/src/launcher/src/configurationEditDialog.cpp @@ -289,6 +289,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) { m_ui->checkBox_readback_tiled->setChecked(info.readback_tiled_images); m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers); m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled); + m_ui->checkBox_occlusion_queries->setChecked(info.occlusion_queries_enabled); m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen); m_ui->checkBox_network_connected->setChecked(info.network_connected); m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled); @@ -483,6 +484,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo info.readback_tiled_images = ui.checkBox_readback_tiled->isChecked(); info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked(); info.tessellation_enabled = ui.checkBox_tessellation->isChecked(); + info.occlusion_queries_enabled = ui.checkBox_occlusion_queries->isChecked(); info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked(); info.network_connected = ui.checkBox_network_connected->isChecked(); info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked(); diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index caf98c553..7768cc21e 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -287,6 +287,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) { AddFlag(args, info.skip_notice_screen, "--skip-notice-screen"); AddFlag(args, info.network_connected, "--network-connected"); AddFlag(args, info.tessellation_enabled, "--tessellation"); + AddFlag(args, info.occlusion_queries_enabled, "--occlusion-queries"); args << "--vblank-frequency" << QString::number(info.vblank_frequency); args << "--console-language" << QString::number(info.console_language); AddFlag(args, info.vulkan_validation_enabled, "--vulkan-validation"); diff --git a/src/main.cpp b/src/main.cpp index 451a1eb89..a2cc5c03c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -78,6 +78,7 @@ static void PrintUsage() { " Implies --vulkan-validation; very slow.\n"); ::printf(" --shader-validation Enable shader validation.\n"); ::printf(" --tessellation Draw tessellation patches; skipped by default.\n"); + ::printf(" --occlusion-queries Count occlusion query samples on the GPU.\n"); ::printf(" --shader-optimization-type None, Size, or Performance.\n"); ::printf(" --shader-log-direction Silent, Console, or File.\n"); ::printf(" --shader-log-folder Shader log output folder.\n"); @@ -202,6 +203,7 @@ static constexpr Flag FLAGS[] = { {"--gpu-assisted-validation", &ConfigOptions::gpu_assisted_validation_enabled}, {"--shader-validation", &ConfigOptions::shader_validation_enabled}, {"--tessellation", &ConfigOptions::tessellation_enabled}, + {"--occlusion-queries", &ConfigOptions::occlusion_queries_enabled}, {"--command-buffer-dump", &ConfigOptions::command_buffer_dump_enabled}, {"--graphics-debug-dump", &ConfigOptions::graphics_debug_dump_enabled}, {"--profile", &ConfigOptions::profiler_enabled}, diff --git a/tests/OcclusionQueriesTests.cpp b/tests/OcclusionQueriesTests.cpp new file mode 100644 index 000000000..ee2fb486a --- /dev/null +++ b/tests/OcclusionQueriesTests.cpp @@ -0,0 +1,55 @@ +#include "graphics/host_gpu/renderer/occlusionQueries.h" + +#include +#include +#include + +namespace { +using namespace Libs::Graphics; + +#define CHECK(condition) \ + do { \ + if (!(condition)) { \ + std::fprintf(stderr, "OcclusionQueriesTests:%d: %s\n", __LINE__, #condition); \ + std::abort(); \ + } \ + } while (false) + +using Results = std::array; + +// The samples a query passed, as ZPass predication sums them; -1 before every counter is written. +int64_t Passed(const Results& results) { + int64_t samples = 0; + for (uint32_t db = 0; db < OCCLUSION_QUERY_DBS; db++) { + const auto begin = results[db * 2]; + const auto end = results[db * 2 + 1]; + if ((begin & end & OCCLUSION_QUERY_READY) == 0) { + return -1; + } + samples += static_cast(end - begin); + } + return samples; +} + +void TestCounters() { + Results results {}; + WriteZPassCounters(results.data(), 100, true); + CHECK(Passed(results) == -1); + WriteZPassCounters(results.data() + 1, 100, false); + CHECK(Passed(results) == 0); + WriteZPassCounters(results.data() + 1, 142, false); + CHECK(Passed(results) == 42); + CHECK(results[0] == (OCCLUSION_QUERY_READY | 100) && results[30] == OCCLUSION_QUERY_READY); + + // A begin clears the end a previous query wrote late, so the query is not ready until its own. + WriteZPassCounters(results.data(), 142, true); + CHECK(Passed(results) == -1); + WriteZPassCounters(results.data() + 1, 150, false); + CHECK(Passed(results) == 8); +} +} // namespace + +int main() { + TestCounters(); + std::puts("Occlusion query tests passed"); +}