1 Commits
Author SHA1 Message Date
jpolo1224 4797ad8a9a 0.7.1: revert the 0.7 renderer to 0.6
0.7 introduced corruption in several games that were fine on 0.6 -- flashing
and flickering in Sonic Unleashed and Dragon Ball among others. Emu/RSX is
returned to its 0.6 state in full; everything outside the renderer is kept.

The main cause was vertex cache retention. On 0.6 the attribute ring was too
small for retention to engage, so raising the ring to 192M did not add a code
path, it switched an existing one on in every game at once, and reusing stale
vertex data is what the flashing was.

Bisecting also showed the render pass work was not responsible: reverting it
alone changed nothing. It can come back, but on its own and with testing
behind it rather than as part of a batch.

Kept from 0.7: the ARM64 PPU float to integer fix, the PPU cache build
identity, the SPU checksum and block state fixes, Oboe, ADPF, and the crash
and stability ports.

Sonic Unleashed does not render FMV cutscenes. That reproduces with the 0.6
renderer as well, so it is not from any of this and is still open.
2026-08-13 19:46:13 -04:00
32 changed files with 105 additions and 2295 deletions
+2 -2
View File
@@ -29,8 +29,8 @@ android {
applicationId = "com.armsx3"
minSdk = 26
targetSdk = 37
versionCode = 11
versionName = "0.7"
versionCode = 12
versionName = "0.7.1"
// ARMSX2's UI reads these. STORAGE_ALL_FILES gates the all-files storage path in
// onboarding; IN_APP_UPDATER gates the in-app GitHub-release updater.
@@ -122,30 +122,13 @@ namespace rsx
texture_cache_predictor_entry_history_queue<max_write_history_size> write_history;
static const u32 max_confidence = 8; // Cannot be more "confident" than this value
#ifdef __ANDROID__
// Mobile-tiler tuning. Here the GPU is usually idle while the CPU stalls on synchronous
// colour-buffer readbacks (Write Color Buffers titles such as Demon's Souls). With no GPU
// contention a speculative pre-flush is close to free, so engage the predictor sooner:
// cross the threshold on the first repeat of a stable readback region, so the pre-flush
// fires at framebuffer setup and the later CPU read finds an already-signalled fence
// instead of spinning on a fresh GPU copy.
static const u32 confident_threshold = 4;
static const u32 starting_confidence = 4;
#else
static const u32 confident_threshold = 6; // We are confident if confidence >= confidence_threshold
static const u32 starting_confidence = 3;
#endif
static const u32 confidence_guessed_flush = 2; // Confidence granted when we correctly guess there will be a flush
static const u32 confidence_guessed_no_flush = 1; // Confidence granted when we correctly guess there won't be a flush
static const u32 confidence_incorrect_guess = -2; // Confidence granted when our guess is incorrect
#ifdef __ANDROID__
// A wrong speculative flush costs little on an idle GPU, so do not punish it into a
// multi-frame confidence rebuild.
static const u32 confidence_mispredict = -2;
#else
static const u32 confidence_mispredict = -4; // Confidence granted when a speculative flush is incorrect
#endif
u32 confidence;
-33
View File
@@ -744,15 +744,10 @@ namespace rsx
// why it is game- and timing-dependent rather than reliable.
fifo_ctrl->sync_get_force();
// Spin budget for the idle wait below. Reset whenever a fresh idle period starts, so
// each drain gets its own short hot window before parking.
static thread_local u32 s_fifo_idle_spins = 0;
if (performance_counters.state == FIFO::state::running)
{
performance_counters.FIFO_idle_timestamp = get_system_time();
performance_counters.state = FIFO::state::empty;
s_fifo_idle_spins = 0;
}
else
{
@@ -781,36 +776,8 @@ namespace rsx
// It has now caused two wrong conclusions in one session: once reading a
// starving RSX as CPU-bound decode, and once reading a thread stuck in an
// occlusion query wait as the same thing.
// UPDATE: the yield is now itself the problem, and it is measured. A native
// profile of Arkham City gameplay put ~11% of TOTAL process CPU in sched_yield
// reached from here -- 93% of the RSX thread's kernel time, and its single largest
// cost. sched_yield is close to the worst available wait on this device: it is a
// syscall, it forces a scheduler pass, and with ~14 hot threads over 8 cores it is
// usually rescheduled immediately -- so it burns a core one of the five SPU threads
// actually wants, while doing nothing to notice the guest sooner.
//
// A short hot spin first, so a PUT that lands within microseconds is still caught
// without paying any wake latency; only sustained idle parks. WFE costs no syscall
// and the architected event stream bounds the park to tens of microseconds, so the
// RSX still sits on the frame's dependency chain rather than sleeping through work.
//
// The pre-spin is not optional: ouroboros420/rpcsx parked bare here (e31ef44ef) and
// had to walk it back (832c23078) when the wake latency cost frametime smoothness.
RSX_PROF_SCOPE(idle);
#if defined(ARCH_ARM64)
if (s_fifo_idle_spins < 8)
{
s_fifo_idle_spins++;
utils::pause();
}
else
{
utils::wait_for_event();
}
#else
std::this_thread::yield();
#endif
}
return;
-54
View File
@@ -15,12 +15,7 @@
#include "gcm_printing.h"
#include "RSXDisAsm.h"
#ifdef __ANDROID__
#include <unistd.h> // ::gettid() for the ADPF feed
#endif
#include "Emu/System.h"
#include "Emu/system_utils.hpp"
#include "Emu/Cell/PPUThread.h"
#include "Emu/Cell/SPUThread.h"
#include "Emu/Cell/timers.hpp"
@@ -2783,55 +2778,6 @@ namespace rsx
{
m_eng_interrupt_mask.clear(rsx::display_interrupt);
#ifdef __ANDROID__
// ADPF feed: publish this frame's real CPU cost and the presenting thread's OS tid so
// the app can drive PerformanceHintManager. Measured at a fixed point each iteration, so
// the previous iteration's frame-limiter sleep lands in the idle delta and is excluded.
// Advisory only: these go to atomics nothing in the core reads back.
// Ported from ouroboros420/rpcsx (3d4ba6060).
{
static thread_local u64 s_last_now = 0;
static thread_local u64 s_last_idle = 0;
static thread_local s32 s_tid = 0;
if (s_tid == 0)
{
s_tid = static_cast<s32>(::gettid());
}
// Republished every flip so a recreated RSX thread overwrites a stale tid, rather
// than leaving the app's hint session pointed at a dead thread after a restart.
rpcs3::utils::set_rsx_thread_tid(s_tid);
const u64 now_us = get_system_time();
const u64 idle_us = performance_counters.idle_time.load();
if (s_last_now != 0 && now_us > s_last_now)
{
const u64 wall = now_us - s_last_now;
// The flip-to-flip deadline. Without it the hint judges a 30fps game against a
// 60fps target and over-boosts, which is pure heat.
rpcs3::utils::report_frame_period_ns(wall * 1000);
// idle_time is reset periodically by get_load(), so a delta that went backwards is
// a reset, not a real frame. Idle can also exceed the wall window (it accrues from
// FIFO/semaphore paths). Reporting work == wall in either case would feed a bogus
// fully-busy sample and over-boost; skipping leaves the last good one in place.
if (idle_us >= s_last_idle)
{
if (const u64 idle_delta = idle_us - s_last_idle; idle_delta < wall)
{
rpcs3::utils::report_frame_work_ns((wall - idle_delta) * 1000);
}
}
}
s_last_now = now_us;
s_last_idle = idle_us;
}
#endif
if (async_flip_requested & flip_request::any)
{
// Deferred flip
-54
View File
@@ -445,33 +445,6 @@ namespace rsx
u32 processed = 0;
const bool has_unclaimed = (m_pending_writes.back().sink == 0);
// Batch-prefetch every GPU occlusion result this drain is about to read, in one round
// trip. The VK backend collapses N blocking reads into a single copy + fence and primes
// its per-query cache; other backends no-op and the loop below reads per-query as before.
// The filter is implemented && num_draws, a superset of what the loop actually reads --
// the dynamic have_result early-out only skips some, costing at most a wasted copy.
{
std::vector<occlusion_query_info*> prefetch_set;
prefetch_set.reserve(m_pending_writes.size());
for (auto& writer : m_pending_writes)
{
if (!writer.sink)
break;
auto query = writer.query;
if (!query || !query->num_draws)
continue;
if (writer.type == CELL_GCM_ZPASS_PIXEL_CNT || writer.type == CELL_GCM_ZCULL_STATS3)
prefetch_set.push_back(query);
}
if (prefetch_set.size() > 1)
prefetch_occlusion_query_results(prefetch_set);
}
// Write all claimed reports unconditionally
for (auto& writer : m_pending_writes)
{
@@ -606,33 +579,6 @@ namespace rsx
}
u32 processed = 0;
// Batch-prefetch every GPU occlusion result this drain is about to read, in one round
// trip. The VK backend collapses N blocking reads into a single copy + fence and primes
// its per-query cache; other backends no-op and the loop below reads per-query as before.
// The filter is implemented && num_draws, a superset of what the loop actually reads --
// the dynamic have_result early-out only skips some, costing at most a wasted copy.
{
std::vector<occlusion_query_info*> prefetch_set;
prefetch_set.reserve(m_pending_writes.size());
for (auto& writer : m_pending_writes)
{
if (!writer.sink)
break;
auto query = writer.query;
if (!query || !query->num_draws)
continue;
if (writer.type == CELL_GCM_ZPASS_PIXEL_CNT || writer.type == CELL_GCM_ZCULL_STATS3)
prefetch_set.push_back(query);
}
if (prefetch_set.size() > 1)
prefetch_occlusion_query_results(prefetch_set);
}
for (auto& writer : m_pending_writes)
{
if (!writer.sink)
-16
View File
@@ -87,19 +87,8 @@ namespace rsx
enum constants
{
#ifdef __ANDROID__
// Mobile-tiler tuning. The GPU finishes occlusion queries in microseconds but is
// otherwise idle (we are sync-bound, not GPU-bound), so the desktop cadence leaves
// completed results unharvested for up to 300us and the guest's Reports-area read
// then force-drains them one at a time -- the per-frame ZCULL hitch under Accurate
// ZCULL stats. Harvest 4x more often so ready results are picked up without blocking
// before the guest asks for them.
max_zcull_delay_us = 100, // Delay before a report update operation is forced to retire
min_zcull_tick_us = 25, // Default tick duration. To avoid hardware spam, we schedule peeks in multiples of this.
#else
max_zcull_delay_us = 300, // Delay before a report update operation is forced to retire
min_zcull_tick_us = 100, // Default tick duration. To avoid hardware spam, we schedule peeks in multiples of this.
#endif
occlusion_query_count = 2048, // Number of occlusion query slots available. Real hardware actually has far fewer units before choking
max_safe_queue_depth = 1792, // Number of in-flight queries before we start forcefully flushing data from the GPU device.
max_stat_registers = 8192 // Size of the statistics cache
@@ -214,11 +203,6 @@ namespace rsx
virtual void end_occlusion_query(occlusion_query_info* /*query*/) {}
virtual bool check_occlusion_query_status(occlusion_query_info* /*query*/) { return true; }
virtual void get_occlusion_query_result(occlusion_query_info* query) { query->result = -1; }
// Optional batch hint: the backend may fetch every result this drain is about to read in
// one round trip and prime its per-query cache, so the reads below are then free. Purely
// an optimization -- the default does nothing and the per-query path still works.
virtual void prefetch_occlusion_query_results(const std::vector<occlusion_query_info*>&) {}
virtual void discard_occlusion_query(occlusion_query_info* /*query*/) {}
};
+10 -137
View File
@@ -46,32 +46,6 @@ namespace vk
}
}
// The render target this sampled image is, if it is one and if the sample park applies to it.
// Null on every other target, on every non-RTT, and whenever the tunable is off, so all the
// gating lives in one place and the switch below reads as a plain "park or do what we did".
static vk::render_target* sample_park_candidate(vk::image* raw, const rsx::sampled_image_descriptor_base* sampler_state)
{
#ifdef __ANDROID__
if constexpr (vk::s_sample_park_frames == 0)
{
return nullptr;
}
if (sampler_state->upload_context != rsx::texture_upload_context::framebuffer_storage)
{
return nullptr;
}
// dynamic_cast rather than vk::as_rtt: as_rtt asserts, and a framebuffer_storage
// descriptor can still hand over a temporary subresource that is a plain viewable_image.
return dynamic_cast<vk::render_target*>(raw);
#else
static_cast<void>(raw);
static_cast<void>(sampler_state);
return nullptr;
#endif
}
void validate_image_layout_for_read_access(
vk::command_buffer& cmd,
vk::image_view* view,
@@ -100,33 +74,6 @@ namespace vk
break;
}
// A sample-parked surface arrives here on every sample after the first, because it
// was left in GENERAL instead of being moved to SHADER_READ_ONLY_OPTIMAL.
//
// It still needs a write -> read barrier whenever it has been written since the last
// one, and that barrier still has to end the open pass: an unbound RTT is not an
// attachment of the pass in flight, so VUID-vkCmdPipelineBarrier-image-04073 rules
// out keeping it. What it must NOT do is move the layout, or the surface un-parks and
// pays the return trip after all.
//
// The "has been written since" test is the load-bearing part. Without parking the
// layout itself answered it - already in SHADER_READ_ONLY_OPTIMAL meant already
// synchronized, so the second through five-hundredth draw sampling a shadow map fell
// through the default case and issued nothing. Leaving the surface in GENERAL erases
// that signal, and issuing a barrier per sampling draw instead of per write would be
// a catastrophic regression rather than a win. render_target carries the answer
// explicitly now; see sample_park_needs_read_barrier.
if (auto parked = sample_park_candidate(raw, sampler_state);
parked && raw->current_layout == vk::render_target::get_sample_park_layout())
{
if (!parked->sample_park_needs_read_barrier())
{
// Already synchronized for reading, and already in a layout that is legal to
// sample from. Nothing at all to record.
break;
}
}
// This was used in a cyclic ref before, but is missing a barrier
// No need for a full stall, use a custom barrier instead
VkPipelineStageFlags src_stage;
@@ -146,74 +93,22 @@ namespace vk
dst_stage |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
}
{
// Stay in the park layout if this surface is parked, otherwise the historical
// move to SHADER_READ_ONLY_OPTIMAL. Note this keeps oldLayout == newLayout in
// the parked case, which is the only form a barrier is allowed to take inside a
// render pass - not that it can stay inside one here, but it means the barrier
// carries no transition cost of its own either.
auto parked = sample_park_candidate(raw, sampler_state);
const bool stay_parked = parked && parked->try_arm_sample_park() &&
raw->current_layout == vk::render_target::get_sample_park_layout();
vk::insert_image_memory_barrier(
cmd,
raw->value,
raw->current_layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
src_stage, dst_stage,
src_access, dst_access,
{ raw->aspect(), 0, 1, 0, 1 });
const VkImageLayout target_layout = stay_parked
? vk::render_target::get_sample_park_layout()
: VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
vk::insert_image_memory_barrier(
cmd,
raw->value,
raw->current_layout, target_layout,
src_stage, dst_stage,
src_access, dst_access,
{ raw->aspect(), 0, 1, 0, 1 });
raw->current_layout = target_layout;
if (stay_parked)
{
parked->on_sample_park_synced();
}
else if (parked)
{
parked->clear_sample_park();
}
}
raw->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
break;
case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:
case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
ensure(sampler_state->upload_context == rsx::texture_upload_context::framebuffer_storage);
if (!sampler_state->is_cyclic_reference) [[ likely ]]
{
// Standard pre-read barrier, and the first half of the sample/rebind cycle.
//
// This transition is the floor. The surface was just rendered to, so the read
// needs a write -> read barrier, and a barrier cannot stay inside a pass for an
// image the pass does not have attached. What the park changes is the
// destination: GENERAL is legal to sample from and legal to attach, so the next
// bind has nothing to undo, whereas SHADER_READ_ONLY_OPTIMAL is not a legal
// attachment layout and forces a second teardown to leave it.
//
// The transition itself is issued through the same change_layout as before, so
// the access/stage scopes image_helpers derives for it are unchanged apart from
// the destination, and it is still charged to ImgHelper:43.
if (auto parked = sample_park_candidate(raw, sampler_state))
{
if (parked->try_arm_sample_park())
{
raw->change_layout(cmd, vk::render_target::get_sample_park_layout());
parked->on_sample_park_synced();
break;
}
// Declined - bound, multisampled, or no renderer. Drop any window left over
// from an earlier park so the deadline can never outlive the layout it
// describes. Both readers already re-check current_layout, so this is
// tidiness rather than a fix, but it keeps the state machine to one rule:
// a live deadline always means the surface is in the park layout.
parked->clear_sample_park();
}
// Standard pre-read barrier.
raw->change_layout(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
break;
}
@@ -235,11 +130,6 @@ void VKGSRender::begin_render_pass()
get_render_pass(),
m_draw_fbo->value,
{ positionu{0u, 0u}, sizeu{m_draw_fbo->width(), m_draw_fbo->height()} });
// Publish what this pass actually has attached, so a barrier that wants to stay inside it can
// check VUID-vkCmdPipelineBarrier-image-04073 rather than trust the caller. m_fbo_images is
// the exact list the framebuffer was built from a few lines up the call chain in prepare_rtts.
vk::set_renderpass_attachments(*m_current_command_buffer, m_fbo_images);
}
void VKGSRender::close_render_pass()
@@ -1208,24 +1098,7 @@ void VKGSRender::emit_geometry(u32 sub_index)
if (pass)
{
// Subpass mismatch, end it before proceeding
if (rsx::prof::enabled()) [[unlikely]]
{
rsx::prof::g_rp_sites[1]++;
// Which of the two mismatches fired. The framebuffer changing is the game
// switching render target and nothing here can avoid it; the render pass handle
// changing under an unchanged framebuffer can only be the attachment layouts,
// since format and sample count cannot move without the framebuffer moving too.
//
// This site only became loud once parking stopped the bind-time layout change
// from ending the pass earlier in the draw, which left the previous draw's pass
// open to be torn down here instead. Whether that is the same teardown relocated
// or an extra one caused by layout churn is exactly what these two counters
// separate, and it decides whether parking longer is worth anything.
rsx::prof::g_rp_sites[(m_draw_fbo->value != fbo) ? 19 : 18]++;
}
vk::end_renderpass(cmd);
if (rsx::prof::enabled()) [[unlikely]] rsx::prof::g_rp_sites[1]++; vk::end_renderpass(cmd);
}
// Starting a new renderpass should clobber dynamic state
+2 -191
View File
@@ -483,13 +483,6 @@ VKGSRender::VKGSRender(utils::serial* ar) noexcept : GSRender(ar)
if (!m_swapchain->init(m_swapchain_dims.width, m_swapchain_dims.height))
{
swapchain_unavailable = true;
#ifdef ANDROID
// The VkSurfaceKHR is bound to the ANativeWindow captured at create time, so a surface
// lost during boot-time init stays lost however often we re-query it. Flag it so the first
// reinitialize_swapchain() takes the recreate branch with the new window instead of
// soft-looping forever against a dead surface.
m_surface_lost = true;
#endif
}
else
{
@@ -522,37 +515,9 @@ VKGSRender::VKGSRender(utils::serial* ar) noexcept : GSRender(ar)
m_occlusion_query_manager->set_control_flags(VK_QUERY_CONTROL_PRECISE_BIT, 0);
}
// The vertex cache can only retain an entry while the ring memory naming it survives, so
// retention engages only once the ring holds the in-flight headroom plus another frame of
// geometry -- four frames at the current headroom. Arkham City peaks near 31MB of geometry a
// frame, needing ~124MB, so the 64MB default leaves retention permanently disengaged in
// exactly the heavy scenes that stand to gain from it. The heap is growable, but it grows on
// allocation pressure and wrapping relieves that pressure, so it never reaches a size that
// would let entries live: the initial size is the only lever. Scale it against the memory
// budget rather than taking a fixed 192MB, because that is a fifth of the floor budget we
// hand a 4GB phone.
u32 attrib_ring_size_m = VK_ATTRIB_RING_BUFFER_SIZE_M;
#ifdef __ANDROID__
{
// Flat, deliberately. This was first scaled off get_budgetable_device_memory, which was
// wrong twice over: that figure is a texture-cache quota the ring does not draw from, and
// it is derived from free memory *after* the emulator has taken its RAM, so it floors at
// 1024M on an 8GB device and every tier collapsed back to 64M.
//
// 192M covers a peak frame up to 48M against the 4x rule; measured peaks here run 11-32M.
// 128M would only just clear the observed 31.8M peak and would flap on anything heavier.
attrib_ring_size_m = 192u;
// Named because the retention log reports the ring size it had to work with, and a reader
// otherwise cannot tell a ring that was sized down from one that was never sized up.
rsx_log.notice("Attribute ring sized to %uM.", attrib_ring_size_m);
}
#endif
// VRAM allocation
// This first set is bound persistently, so grow notifications are enabled.
m_attrib_ring_info.create(VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT, attrib_ring_size_m * 0x100000, vk::heap_pool_default, "attrib buffer", 0x400000, VK_TRUE);
m_attrib_ring_info.create(VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT, VK_ATTRIB_RING_BUFFER_SIZE_M * 0x100000, vk::heap_pool_default, "attrib buffer", 0x400000, VK_TRUE);
m_fragment_env_ring_info.create(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_UBO_RING_BUFFER_SIZE_M * 0x100000, vk::heap_pool_low_latency, "fragment env buffer", 0x10000, VK_TRUE);
m_vertex_env_ring_info.create(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_UBO_RING_BUFFER_SIZE_M * 0x100000, vk::heap_pool_default, "vertex env buffer", 0x10000, VK_TRUE);
m_fragment_texture_params_ring_info.create(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_UBO_RING_BUFFER_SIZE_M * 0x100000, vk::heap_pool_low_latency, "fragment texture params buffer", 0x10000, VK_TRUE);
@@ -606,9 +571,7 @@ VKGSRender::VKGSRender(utils::serial* ar) noexcept : GSRender(ar)
m_fragment_constants_buffer_info = { *m_fragment_constants_ring_info.heap, 0, VK_WHOLE_SIZE };
const auto& limits = m_device->gpu().get_limits();
// Clamped to the device limit, not the ring size: views window across the ring (see
// upload_vertex_data), so a ring larger than one view is fine and is the normal case now.
m_texbuffer_view_size = std::min(limits.maxTexelBufferElements, attrib_ring_size_m * 0x100000u);
m_texbuffer_view_size = std::min(limits.maxTexelBufferElements, VK_ATTRIB_RING_BUFFER_SIZE_M * 0x100000u);
// Initialize bulk allocators
m_vertex_env_allocator = std::make_unique<rsx::data_heap::bulk_allocator<256, 96>>(
@@ -2926,85 +2889,6 @@ bool VKGSRender::check_occlusion_query_status(rsx::reports::occlusion_query_info
return m_occlusion_query_manager->check_query_status(oldest);
}
// Collapse the drain's N blocking per-query reads into one GPU copy plus a single fence wait,
// then prime the per-query cache so the reads that follow cost nothing. On a tiler each
// individual read is a full round trip, so N of them back to back is most of the ZCULL cost.
// Best-effort: on any bail the caller's per-query path still runs unchanged.
// Ported from ouroboros420/rpcsx (7ed3365bc).
void VKGSRender::prefetch_occlusion_query_results(const std::vector<rsx::reports::occlusion_query_info*>& queries)
{
if (queries.size() < 2)
{
// Not worth a batch round trip; the per-query loop handles it with no extra hard sync.
return;
}
std::vector<u32> indices;
indices.reserve(queries.size() * 4);
bool needs_hard_sync = false;
for (auto* query : queries)
{
if (!query) continue;
auto& data = m_occlusion_map[query->driver_handle];
if (data.indices.empty()) continue;
// A query begun in the current command buffer has not been ENDED yet, so copying it
// would need a hard sync -- exactly what the per-query path deliberately avoids. Skip
// the whole batch rather than force one.
if (data.is_current(m_current_command_buffer))
{
needs_hard_sync = true;
break;
}
for (const auto id : data.indices)
{
indices.push_back(id);
}
}
if (needs_hard_sync || indices.size() < 2)
{
return;
}
// Sorted so the pool-aware coalescing in copy_query_results actually finds runs.
std::sort(indices.begin(), indices.end());
indices.erase(std::unique(indices.begin(), indices.end()), indices.end());
const u64 required = indices.size() * 4ull;
if (!m_occlusion_readback_buffer || m_occlusion_readback_buffer->size() < required)
{
const u64 alloc_size = std::max<u64>(required, 4096);
m_occlusion_readback_buffer = std::make_unique<vk::buffer>(*m_device,
alloc_size,
m_device->get_memory_mapping().host_visible_coherent, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,
VK_BUFFER_USAGE_TRANSFER_DST_BIT, 0,
VMM_ALLOCATION_POOL_SYSTEM);
}
m_occlusion_query_manager->copy_query_results(*m_current_command_buffer, indices, m_occlusion_readback_buffer->value);
// One fence wait drains the whole batch. flush_command_queue(true) submits and waits, which
// is the single round trip this exists to pay instead of N.
if (rsx::prof::enabled()) [[unlikely]] rsx::prof::g_flush_sites[11]++;
flush_command_queue(true);
if (auto* mapped = static_cast<u32*>(m_occlusion_readback_buffer->map(0, required)))
{
for (usz i = 0; i < indices.size(); ++i)
{
m_occlusion_query_manager->prime_query_result(indices[i], mapped[i]);
}
m_occlusion_readback_buffer->unmap();
}
}
void VKGSRender::get_occlusion_query_result(rsx::reports::occlusion_query_info* query)
{
auto &data = m_occlusion_map[query->driver_handle];
@@ -3029,82 +2913,9 @@ void VKGSRender::get_occlusion_query_result(rsx::reports::occlusion_query_info*
data.sync();
// On a tile-based renderer the result is usually a whole tiling pass away, so this wait
// is long -- and it must stay interruptible. get_query_result() blocks without ever
// checking external_interrupt_lock, and a PPU thread that faults on RSX-guarded memory
// during that window spins in on_access_violation() waiting for an ack this thread can no
// longer give: PPU pinned in sched_yield, RSX parked in the query wait, presenting as a
// freeze. Wait here instead, servicing external interrupts the way the FIFO
// semaphore_acquire loop does, and only call get_query_result() once the value is ready.
// Ported from rfandango/rpcsx (a560768ce).
static const bool needs_interruptible_wait = vk::is_tile_based_renderer(vk::get_driver_vendor());
bool aborted = false;
// Gather data
for (const auto occlusion_id : data.indices)
{
if (needs_interruptible_wait)
{
u32 wait_iterations = 0;
bool rescued = false;
while (!m_occlusion_query_manager->check_query_status(occlusion_id))
{
// Rescue path. A query begun in the current command buffer is not even ENDED
// until close_and_submit_command_buffer(), so if the is_current() bookkeeping
// above mis-reported, the value can never arrive without a flush. Rather than
// pay a hard sync on every ZCULL read, give it a generous window then force the
// flush once. If the query still never readies after this fires, the GPU is not
// retiring work at all -- a driver hang, not a bookkeeping bug -- and this
// warning is the breadcrumb that tells the two apart.
if (!rescued && ++wait_iterations >= 4096)
{
rescued = true;
rsx_log.warning("ZCULL result did not arrive; forcing command flush (query=%d)", occlusion_id);
std::lock_guard lock(m_flush_queue_mutex);
if (rsx::prof::enabled()) [[unlikely]] rsx::prof::g_flush_sites[11]++; flush_command_queue();
if (m_flush_requests.pending())
{
m_flush_requests.clear_pending_flag();
}
continue;
}
// Not optional: a PPU thread that faults on RSX-guarded memory posts a flush
// request in on_access_violation() and spins in producer_wait() until this
// thread consumes it. Servicing only external_interrupt_lock is not enough --
// that was a measured deadlock. The FIFO semaphore_acquire wait survives the
// same situation precisely because cpu_wait() makes this call.
on_semaphore_acquire_wait();
if (external_interrupt_lock)
{
wait_pause();
}
else if (state & cpu_flag::exit)
{
// The result may never arrive during shutdown, so do not read it.
aborted = true;
break;
}
else
{
// Park at near-zero power; the architected event stream bounds the poll
// period to tens of microseconds.
utils::wait_for_event();
}
}
if (aborted)
{
break;
}
}
query->result += m_occlusion_query_manager->get_query_result(occlusion_id);
if (query->result && !g_cfg.video.precise_zpass_count)
{
-28
View File
@@ -70,8 +70,6 @@ private:
output_scaling_mode m_output_scaling{output_scaling_mode::bilinear};
std::unique_ptr<vk::buffer> m_cond_render_buffer;
// Host-visible scratch for the batched ZCULL readback. Allocated on first use.
std::unique_ptr<vk::buffer> m_occlusion_readback_buffer;
u64 m_cond_render_sync_tag = 0;
shared_mutex m_sampler_mutex;
@@ -104,8 +102,6 @@ private:
bool m_surface_lost = false;
vk::instance m_instance;
vk::render_device *m_device;
// Timestamp of the last periodic pipeline-cache serialize (see flip()).
u64 m_last_pipeline_cache_save_time = 0;
//Vulkan internals
std::unique_ptr<vk::query_pool_manager> m_occlusion_query_manager;
@@ -148,23 +144,6 @@ private:
rsx::simple_array<vk::data_heap*> m_flushable_data_heaps; // List of heaps that can be 'dirty' and need manual flush
#ifdef __ANDROID__
// Cross-frame vertex cache retention. m_attrib_ring_info is a ring, so a cached
// offset_in_heap stays meaningful only until the ring laps back over it; the cache used to be
// purged every frame because nothing tracked that. See vertex_cache_on_frame_end().
u64 m_vtx_heap_cursor = 0; // Monotonic byte count ever handed out by m_attrib_ring_info
usz m_vtx_heap_put = 0; // Ring PUT at the last cursor sample
u64 m_vtx_retain_floor = 0; // Entries stamped below this cursor value are evicted
u64 m_vtx_frame_base = 0; // Cursor as of the start of the current frame
u64 m_vtx_frame_peak = 0; // Largest single-frame consumption in the recent window
u32 m_vtx_peak_ttl = 0; // Frames left before the peak is allowed to decay
u64 m_vtx_reserve_bytes = 0; // Ring bytes withheld from the allocator (0 = retention off)
u64 m_vtx_frame_marks[8]{}; // Cursor at the end of each of the last 8 frames
u32 m_vtx_frame_index = 0;
bool m_vtx_retention_locked_out = false;
VkBuffer m_vtx_heap_handle = VK_NULL_HANDLE; // Detects a grow(), which discards the whole heap
#endif
VkDescriptorBufferInfoEx m_vertex_env_buffer_info {};
VkDescriptorBufferInfoEx m_fragment_env_buffer_info {};
VkDescriptorBufferInfoEx m_vertex_layout_stream_info {};
@@ -268,12 +247,6 @@ private:
vk::vertex_upload_info upload_vertex_data();
rsx::simple_array<u8> m_scratch_mem;
#ifdef __ANDROID__
void vertex_cache_sample_heap(); // Advance the ring cursor, and detect a heap reallocation
void vertex_cache_on_heap_reset(); // Drop everything; the heap the offsets named is gone
void vertex_cache_on_frame_end(); // Size the reservation and evict what it cannot cover
#endif
bool load_program();
void load_program_env();
void update_vertex_env(u32 id, const vk::vertex_upload_info& vertex_info);
@@ -298,7 +271,6 @@ public:
void end_occlusion_query(rsx::reports::occlusion_query_info* query) override;
bool check_occlusion_query_status(rsx::reports::occlusion_query_info* query) override;
void get_occlusion_query_result(rsx::reports::occlusion_query_info* query) override;
void prefetch_occlusion_query_results(const std::vector<rsx::reports::occlusion_query_info*>& queries) override;
void discard_occlusion_query(rsx::reports::occlusion_query_info* query) override;
// External callback in case we need to suddenly submit a commandlist unexpectedly, e.g in a violation handler
-42
View File
@@ -1,6 +1,5 @@
#include "stdafx.h"
#include "VKGSRender.h"
#include "Emu/Cell/timers.hpp"
#include "vkutils/buffer_object.h"
#include "vkutils/memory.h"
#include "Emu/RSX/Overlays/overlay_manager.h"
@@ -244,35 +243,9 @@ void VKGSRender::advance_queued_frames()
vk::remove_unused_framebuffers();
#ifdef __ANDROID__
// Was an unconditional m_vertex_cache->purge(). Arkham City lands 10 cache hits in 1386
// requests because of it: every entry is thrown away at the frame boundary, so the 10 are
// within-frame duplicates and the other 1376 draws re-upload geometry that never changed,
// 2499us of it a frame, and hand a tile-based GPU 1.35M vertices in one pass to re-bin.
//
// Entries can only survive if the ring memory they name survives with them, so this sizes a
// reservation and evicts anything it cannot cover. See vertex_cache_on_frame_end().
vertex_cache_on_frame_end();
#else
m_vertex_cache->purge();
#endif
m_current_frame->tag_frame_end();
#ifdef __ANDROID__
// tag_frame_end() records PUT-1 for every managed heap, which is what frame_context_cleanup
// later publishes as that heap's GET once this frame retires - i.e. "the GPU is done with
// everything before here, reuse it". For the attribute ring that is exactly the statement that
// makes a retained entry unsafe, so publish the retention floor instead. It is always at or
// behind the value this would otherwise carry, so the allocator ends up strictly more
// conservative than before and in-flight frames stay protected as they already were.
if (m_vtx_reserve_bytes && m_attrib_ring_info.size())
{
m_current_frame->heap_snapshot[&m_attrib_ring_info] =
static_cast<s64>(m_vtx_retain_floor % m_attrib_ring_info.size());
}
#endif
// Throttle here rather than by accident.
//
// The queue used to stay at one entry because the poll above blocked until the oldest
@@ -1247,19 +1220,4 @@ void VKGSRender::flip(const rsx::display_flip_info_t& info)
if (rsx::prof::enabled()) [[unlikely]] rsx::prof::g_flush_sites[20]++; flush_command_queue(true);
}
}
// Crash-resilient pipeline-cache persistence. The blob is otherwise only written on
// clean teardown, so a crash or an Android LMK kill mid-session loses the entire
// warmup -- and a cold-boot compile-burst crash then keeps every later boot cold too.
// save_pipeline_cache() skips the write when the cache size has not moved, so steady
// state costs one size query per interval.
if (const u64 now = get_system_time(); now >= m_last_pipeline_cache_save_time + 120'000'000)
{
m_last_pipeline_cache_save_time = now;
if (m_device)
{
m_device->save_pipeline_cache();
}
}
}
+2 -6
View File
@@ -269,17 +269,13 @@ namespace vk
{
VkGraphicsPipelineCreateInfo create_info = *p_graphics_info;
create_info.layout = m_pipeline_layout;
// Shared driver cache, so the SPIR-V -> ISA compile is not redone every cold boot.
// Null when unavailable, which is exactly what was passed here before.
const VkPipelineCache pipe_cache = g_render_device ? g_render_device->get_pipeline_cache() : VK_NULL_HANDLE;
CHECK_RESULT(vkCreateGraphicsPipelines(m_device, pipe_cache, 1, &create_info, nullptr, &m_pipeline));
CHECK_RESULT(vkCreateGraphicsPipelines(m_device, nullptr, 1, &create_info, nullptr, &m_pipeline));
}
else
{
VkComputePipelineCreateInfo create_info = *p_compute_info;
create_info.layout = m_pipeline_layout;
const VkPipelineCache pipe_cache = g_render_device ? g_render_device->get_pipeline_cache() : VK_NULL_HANDLE;
CHECK_RESULT(vkCreateComputePipelines(m_device, pipe_cache, 1, &create_info, nullptr, &m_pipeline));
CHECK_RESULT(vkCreateComputePipelines(m_device, nullptr, 1, &create_info, nullptr, &m_pipeline));
}
m_linked = true;
+35 -99
View File
@@ -61,7 +61,6 @@ namespace vk
owner = &dev;
query_type = type;
query_slot_status.resize(num_entries, {});
tile_based_renderer = vk::is_tile_based_renderer(vk::get_driver_vendor());
for (unsigned i = 0; i < num_entries; ++i)
{
@@ -143,16 +142,6 @@ namespace vk
{
control_flags = control_;
result_flags = result_;
if (tile_based_renderer)
{
// PARTIAL_BIT exists so the poll loop can finish early the moment any sample is known
// to have passed. That can only happen on an immediate-mode renderer, where fragments
// rasterize as they arrive. A TBDR has nothing to report until the tiling pass ends, so
// the flag can never pay off here and only asks the driver for extra work.
// Ported from rfandango/rpcsx (a560768ce).
result_flags &= ~VK_QUERY_RESULT_PARTIAL_BIT;
}
}
void query_pool_manager::begin_query(vk::command_buffer& cmd, u32 index)
@@ -244,28 +233,29 @@ namespace vk
}
}
#ifdef __ANDROID__
// Spin briefly, then hand the core back.
//
// A pure pause() spin is reasonable on a desktop, where the result lands in
// microseconds and there are cores to spare. On a tiled renderer the result is
// not available until the tile pass resolves, so the wait is far longer, and a
// handheld runs eleven hot emulator threads across five usable cores. Burning
// one of them on a spin costs an SPU thread that had real work to do.
// microseconds and there are cores to spare. On a tiled mobile GPU the
// result is not available until the tile pass resolves, so the wait is far
// longer, and this device runs eleven hot emulator threads across five
// usable cores. Burning one of them on a spin costs an SPU thread that had
// real work to do.
//
// The short spin first keeps the fast case fast, since a result that is nearly
// ready still returns without a scheduler round trip.
//
// Gated on the driver vendor rather than __ANDROID__: an immediate-mode mobile
// GPU wants the desktop path, and a tiler on any other platform wants this one.
if (tile_based_renderer && spins >= 64)
{
std::this_thread::yield();
}
else
// The short spin first keeps the fast case fast, since a result that is
// nearly ready still returns without a scheduler round trip.
if (spins < 64)
{
utils::pause();
}
else
{
std::this_thread::yield();
}
#else
utils::pause();
#endif
poke_query(query_info, index, result_flags);
}
}
@@ -273,31 +263,6 @@ namespace vk
return query_info.data;
}
// Shared by both readback paths. vkCmdCopyQueryPoolResults MUST be recorded outside a render
// pass instance; inside one it is undefined behaviour, and a tiler does not tolerate what an
// IMR desktop GPU does. See the detailed history on get_query_result_indirect below.
void query_pool_manager::end_renderpass_for_readback(vk::command_buffer& cmd)
{
if (!vk::is_renderpass_open(cmd))
{
return;
}
// A query that began inside a render pass instance has to end inside that same instance.
// Ending the pass underneath an open one leaves it permanently unavailable: the driver
// never marks it ready, get_query_result spins on poke_query forever, and the RSX thread
// stops with audio and vblank still alive -- a hang, not a crash. The submit-time ensure()
// does not catch it either, because end_occlusion_query closes the query a moment later so
// the flag reads clear while the result never arrives.
if (cmd.flags & vk::command_buffer::cb_has_open_query)
{
vk::do_query_cleanup(cmd);
}
if (rsx::prof::enabled()) [[unlikely]] rsx::prof::g_rp_sites[2]++;
vk::end_renderpass(cmd);
}
void query_pool_manager::get_query_result_indirect(vk::command_buffer& cmd, u32 index, u32 count, VkBuffer dst, VkDeviceSize dst_offset)
{
// Not "technically supposed to" -- vkCmdCopyQueryPoolResults MUST be recorded outside a
@@ -316,60 +281,31 @@ namespace vk
//
// The upstream comment feared the flush cost. It is paid only when a pass is actually
// open, on a path that already stalls for a GPU result.
end_renderpass_for_readback(cmd);
vkCmdCopyQueryPoolResults(cmd, *query_slot_status[index].pool, index, count, dst, dst_offset, 4, VK_QUERY_RESULT_WAIT_BIT);
}
// Batched readback. Copies each result in `indices` order to `dst`, one 4-byte word per
// entry at its position in the list, coalescing contiguous index runs into single copies.
//
// Pool-aware on purpose: a numerically contiguous run can still span a pool reallocation,
// and copying across that boundary would read from the wrong VkQueryPool. Ported from
// ouroboros420/rpcsx (7ed3365bc), reusing our renderpass handling rather than theirs --
// theirs omits the open-query cleanup above.
void query_pool_manager::copy_query_results(vk::command_buffer& cmd, const std::vector<u32>& indices, VkBuffer dst)
{
if (indices.empty())
if (vk::is_renderpass_open(cmd))
{
return;
}
end_renderpass_for_readback(cmd);
const usz n = indices.size();
for (usz i = 0; i < n;)
{
const u32 base = indices[i];
const auto* pool = query_slot_status[base].pool;
usz j = i + 1;
while (j < n && indices[j] == indices[j - 1] + 1 && query_slot_status[indices[j]].pool == pool)
// A query that began inside a render pass instance has to end inside that same
// instance. Ending the pass underneath an open one leaves it permanently
// unavailable: the driver never marks it ready, so get_query_result spins on
// poke_query forever and the RSX thread stops with everything else still alive.
//
// Nothing catches it either. The submit-time ensure() in commands.cpp only checks
// that the query was closed, and end_occlusion_query does close it a moment later,
// so the flag is clear and the assert passes while the result never arrives. That
// cost Web of Shadows a hang here after the device loss below was fixed.
//
// Closing it first keeps begin and end within one pass, which is the ordering the
// spec asks for. The query is cut short, as it is anywhere do_query_cleanup is
// used, and a truncated occlusion result beats a stalled thread.
if (cmd.flags & vk::command_buffer::cb_has_open_query)
{
j++;
vk::do_query_cleanup(cmd);
}
const u32 count = static_cast<u32>(j - i);
// The run is contiguous in both hardware index and list position, so query[base + k]
// lands at destination word (i + k).
vkCmdCopyQueryPoolResults(cmd, *query_slot_status[base].pool, base, count, dst,
static_cast<VkDeviceSize>(i) * 4, 4, VK_QUERY_RESULT_WAIT_BIT);
i = j;
if (rsx::prof::enabled()) [[unlikely]] rsx::prof::g_rp_sites[2]++;
vk::end_renderpass(cmd);
}
}
// Seed a slot from a value already read back on the host, so the next get_query_result
// returns it without issuing another blocking read. Mirrors poke_query's VK_SUCCESS path.
// Only valid for a value that was waited on (the copy above uses WAIT_BIT).
void query_pool_manager::prime_query_result(u32 index, u32 value)
{
auto& query = query_slot_status[index];
query.ready = true;
query.data = value;
query.any_passed = (value != 0);
vkCmdCopyQueryPoolResults(cmd, *query_slot_status[index].pool, index, count, dst, dst_offset, 4, VK_QUERY_RESULT_WAIT_BIT);
}
void query_pool_manager::free_query(vk::command_buffer&/*cmd*/, u32 index)
-15
View File
@@ -1,6 +1,4 @@
#pragma once
#include <vector>
#include "VulkanAPI.h"
#include <deque>
@@ -48,13 +46,7 @@ namespace vk
vk::render_device* owner = nullptr;
std::vector<query_slot_info> query_slot_status;
// A tile-based renderer cannot produce a query result before its tiling pass resolves.
// Decided once, at construction. More precise than an __ANDROID__ check, which would
// misclassify an immediate-mode mobile part and miss a tiler on any other platform.
bool tile_based_renderer = false;
bool poke_query(query_slot_info& query, u32 index, VkQueryResultFlags flags);
void end_renderpass_for_readback(vk::command_buffer& cmd);
void allocate_new_pool(vk::command_buffer& cmd);
void reallocate_pool(vk::command_buffer& cmd);
void run_pool_cleanup();
@@ -72,13 +64,6 @@ namespace vk
u32 get_query_result(u32 index);
void get_query_result_indirect(vk::command_buffer& cmd, u32 index, u32 count, VkBuffer dst, VkDeviceSize dst_offset);
// Batched readback: copy every result in `indices` to `dst` (4-byte word per entry, at its
// position in the list), coalescing contiguous same-pool runs into single copies.
void copy_query_results(vk::command_buffer& cmd, const std::vector<u32>& indices, VkBuffer dst);
// Seed a slot from a host-read value so the next get_query_result needs no GPU round trip.
void prime_query_result(u32 index, u32 value);
u32 allocate_query(vk::command_buffer& cmd);
void free_query(vk::command_buffer&/*cmd*/, u32 index);
+3 -172
View File
@@ -15,25 +15,11 @@ namespace vk
{
VkRenderPass pass = VK_NULL_HANDLE;
VkFramebuffer fbo = VK_NULL_HANDLE;
// Images backing the attachments of this pass, published by whoever opened it.
// Empty means "unknown", which is treated as "covers nothing".
std::vector<VkImage> attachments;
};
// The last pass closed on a command buffer, for the redundancy check in begin_renderpass.
// Profiling only; never read for correctness.
struct closed_renderpass_info_t
{
VkRenderPass pass = VK_NULL_HANDLE;
VkFramebuffer fbo = VK_NULL_HANDLE;
const void* caller = nullptr;
};
atomic_t<u64> g_cached_renderpass_key = 0;
VkRenderPass g_cached_renderpass = VK_NULL_HANDLE;
rsx::unordered_map<VkCommandBuffer, active_renderpass_info_t> g_current_renderpass;
rsx::unordered_map<VkCommandBuffer, closed_renderpass_info_t> g_last_closed_renderpass;
shared_mutex g_renderpass_cache_mutex;
rsx::unordered_map<u64, VkRenderPass> g_renderpass_cache;
@@ -363,23 +349,10 @@ namespace vk
// Framebuffer-local stages only. VK_DEPENDENCY_BY_REGION_BIT is what makes the
// dependency tile-local rather than a full pipeline flush, and it requires every stage
// named here to be framebuffer-space, which rules out the vertex stage.
//
// Symmetric on purpose. The feedback case needs write -> read (draw writes the pixel, the
// next draw samples it) and the fall-out case needs read -> write (the sampling draw is
// done, the next draw writes the attachment again). Declaring only the first direction
// left render_target::post_texture_barrier with no dependency it could match, so the only
// legal thing it could do was end the pass, which is what it did.
//
// Every stage named is framebuffer-space (FRAGMENT_SHADER, EARLY/LATE_FRAGMENT_TESTS,
// COLOR_ATTACHMENT_OUTPUT), which is what VK_DEPENDENCY_BY_REGION_BIT requires of a
// self-dependency, and each access bit is one its stage supports. Widening a subpass
// dependency only adds synchronization; it is not part of render pass compatibility, so
// no cached pipeline is invalidated by this.
VkSubpassDependency self_dependency = {};
self_dependency.srcSubpass = 0;
self_dependency.dstSubpass = 0;
self_dependency.srcStageMask =
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
@@ -389,100 +362,23 @@ namespace vk
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
self_dependency.srcAccessMask =
VK_ACCESS_SHADER_READ_BIT |
VK_ACCESS_INPUT_ATTACHMENT_READ_BIT |
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
self_dependency.dstAccessMask =
VK_ACCESS_SHADER_READ_BIT |
VK_ACCESS_INPUT_ATTACHMENT_READ_BIT |
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT |
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
self_dependency.dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;
VkSubpassDependency dependencies[2] = { self_dependency, {} };
u32 dependency_count = 1;
#ifdef __ANDROID__
if constexpr (s_surface_parking_enabled)
{
// The dependency parking removed, put back where it costs nothing.
//
// Without parking, a render target that has been sampled sits in
// SHADER_READ_ONLY_OPTIMAL and is dragged back to COLOR_/DEPTH_STENCIL_ATTACHMENT_-
// OPTIMAL when it is next bound. That transition is what carried the read -> write
// dependency: image_helpers.cpp gives it srcAccess = SHADER_READ | TRANSFER_READ at
// FRAGMENT_SHADER | TRANSFER, dstAccess = COLOR_ATTACHMENT_WRITE at
// COLOR_ATTACHMENT_OUTPUT. Parking declines the transition, so that barrier is gone
// and nothing else orders the previous pass's sample against this pass's write.
//
// A render pass boundary is not a synchronization boundary in Vulkan, and the
// implicit VK_SUBPASS_EXTERNAL dependency has srcStageMask = TOP_OF_PIPE, which is
// no execution dependency on anything. So this has to be declared, or parking is
// shipping a write-after-read hazard on every RTT it touches. That class of omission
// is exactly what produced visible corruption in the first attempt at this work.
//
// Declared on the pass rather than issued per draw because it is free here: it is
// baked into the render pass object, applies once at pass start, and a tiler already
// orders passes on the same attachment. A per-draw barrier would cost a teardown and
// give back everything parking wins.
//
// Deliberately NOT VK_DEPENDENCY_BY_REGION_BIT. By-region is for tile-local
// self-dependencies; this crosses a pass boundary, where the producer's fragment may
// be anywhere in the framebuffer relative to the consumer's.
//
// Covers write-after-write too (srcAccess names the attachment writes), which the
// same removed transition used to cover for a surface written, sampled, and written
// again without changing layout.
//
// A dependency is not part of render pass compatibility (Vulkan 8.2: compatibility is
// attachment references, formats and sample counts), so adding one does not
// invalidate a single cached pipeline.
VkSubpassDependency& entry_dependency = dependencies[dependency_count++];
entry_dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
entry_dependency.dstSubpass = 0;
//
// The source scope mirrors what image_helpers.cpp put on the GENERAL ->
// ATTACHMENT_OPTIMAL transition that parking removes, plus the vertex stage: vertex
// textures reach validate_image_layout_for_read_access too (VKDraw.cpp passes
// VK_PIPELINE_STAGE_VERTEX_SHADER_BIT there), so a vertex shader sampling a parked
// RTT is a reader this has to order against. Naming a non-framebuffer-space stage is
// fine here precisely because this is not a by-region self-dependency.
entry_dependency.srcStageMask =
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT |
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_TRANSFER_BIT |
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
entry_dependency.dstStageMask =
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
entry_dependency.srcAccessMask =
VK_ACCESS_SHADER_READ_BIT |
VK_ACCESS_TRANSFER_READ_BIT |
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
entry_dependency.dstAccessMask =
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT |
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT |
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
entry_dependency.dependencyFlags = 0;
}
#endif
VkRenderPassCreateInfo rp_info = {};
rp_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
rp_info.attachmentCount = ::size32(attachments);
rp_info.pAttachments = attachments.data();
rp_info.subpassCount = 1;
rp_info.pSubpasses = &subpass;
rp_info.dependencyCount = dependency_count;
rp_info.pDependencies = dependencies;
rp_info.dependencyCount = 1;
rp_info.pDependencies = &self_dependency;
VkRenderPass result;
CHECK_RESULT(vkCreateRenderPass(dev, &rp_info, NULL, &result));
@@ -497,7 +393,6 @@ namespace vk
g_cached_renderpass_key = 0;
g_cached_renderpass = VK_NULL_HANDLE;
g_current_renderpass.clear();
g_last_closed_renderpass.clear();
// Destroy cache
for (const auto &renderpass : g_renderpass_cache)
@@ -542,28 +437,6 @@ namespace vk
rsx::prof::g_pass_width[rsx::prof::g_pass_ordinal] = static_cast<u16>(framebuffer_region.width);
rsx::prof::g_pass_height[rsx::prof::g_pass_ordinal] = static_cast<u16>(framebuffer_region.height);
}
// Reopening the identical (pass, framebuffer) that was just closed. The teardown in
// between bought nothing: the tile was stored and reloaded to record a barrier, and
// the pass came straight back.
//
// This is the number that says how much any barrier-suppression work can possibly
// win, and it is the one the site counters cannot give. A site counter says a
// teardown was charged there; it cannot say whether the pass was going to end anyway
// because the next draw switches framebuffer. Removing a teardown that was going to
// happen just relocates the charge - which is what happened to iteration 3, where
// ImgHelper:43 fell and (Draw:1093 fbo) rose by the same amount.
//
// So: total passes minus redundant passes is the floor. If redundant is small, no
// amount of barrier work moves the frame, and the honest answer is to stop.
auto& closed = g_last_closed_renderpass[cmd];
if (closed.pass == pass && closed.fbo == target)
{
rsx::prof::g_rp_reopened++;
rsx::prof::note_rp_teardown(closed.caller, 2);
}
closed = {};
}
// The draw region was declared and never recorded anywhere, so the one figure that
@@ -617,15 +490,6 @@ namespace vk
// After the pass ends, so the tile store it triggers is charged to the region.
vk::get_gpu_timer().end(cmd, vk::gpu_timer::region::draw);
// Remember what was closed and by whom, so the next begin can tell whether this teardown
// was load-bearing or whether the same pass simply came back. __builtin_return_address
// here is the teardown site itself - end_renderpass is called directly from all of them.
if (rsx::prof::enabled()) [[unlikely]]
{
const auto& active = g_current_renderpass[cmd];
g_last_closed_renderpass[cmd] = { active.pass, active.fbo, __builtin_return_address(0) };
}
g_current_renderpass[cmd] = {};
}
@@ -634,39 +498,6 @@ namespace vk
return g_current_renderpass[cmd].pass != VK_NULL_HANDLE;
}
void set_renderpass_attachments(const vk::command_buffer& cmd, const std::vector<vk::image*>& images)
{
auto& info = g_current_renderpass[cmd];
if (info.pass == VK_NULL_HANDLE)
{
// Nothing open. Do not record a set that would outlive the pass it describes.
return;
}
info.attachments.clear();
info.attachments.reserve(images.size());
for (const auto* img : images)
{
if (img)
{
info.attachments.push_back(img->value);
}
}
}
bool renderpass_covers_image(const vk::command_buffer& cmd, VkImage image)
{
const auto& info = g_current_renderpass[cmd];
if (info.pass == VK_NULL_HANDLE || image == VK_NULL_HANDLE)
{
return false;
}
// At most 5 entries (4 MRTs + depth), so a scan beats anything with a hash in it.
return std::find(info.attachments.begin(), info.attachments.end(), image) != info.attachments.end();
}
void renderpass_op(const vk::command_buffer& cmd, const renderpass_op_callback_t& op)
{
const auto& active = g_current_renderpass[cmd];
-62
View File
@@ -8,50 +8,6 @@ namespace vk
class image;
class command_buffer;
// Surface layout parking policy.
//
// "Parking" is declining to move a render target out of a layout that is legal both for
// sampling and for use as an attachment, so a sample-then-rebind cycle costs no layout
// transition at all. A transition can never be recorded inside a render pass instance
// (VUID-vkCmdPipelineBarrier-oldLayout-01181), so every transition avoided is a render pass
// teardown avoided - on a tiler, a tile store plus a full reload.
//
// Both knobs live here rather than next to vk::render_target because the render pass builder
// has to see them too. The barrier that parking removes is the one that used to carry the
// read -> write dependency across the cycle, so the pass has to declare that dependency
// itself; see the VK_SUBPASS_EXTERNAL dependency in get_renderpass. Setting these to 0 has to
// take that declaration away with them, otherwise "0 reproduces current behaviour" is a lie.
//
// Both are counted in flips, not binds. A render graph repeats per frame, so "was this
// surface used this way recently" is a per-frame question; the number of unrelated binds
// collected in between is a property of the scene's draw count and is not a useful unit.
// Two frames of slack rather than one so a surface used on alternate frames, or one that
// misses a frame to a paused or duplicate flip, does not oscillate.
// Feedback loops: a surface written and sampled by the same draw, parked in
// ATTACHMENT_FEEDBACK_LOOP_OPTIMAL_EXT where the device supports it and GENERAL otherwise.
// Iteration 3. Measured effect on Arkham City: close to zero, because the teardown it
// removes at the bind site relocates to the draw site as a framebuffer mismatch.
// Held at 0 pending the baseline `redundant` reading: iteration 3 measured close to zero here,
// and at 0 this also drops the extra pass dependency below, which is the only thing that was
// covering the read->write hazard this park had been running without.
inline constexpr u64 s_feedback_park_frames = 0;
// Non-cyclic sampling of a render target: the RTT is left in GENERAL for the read instead of
// being dragged to SHADER_READ_ONLY_OPTIMAL and back. GENERAL, not the feedback-loop layout,
// deliberately - see the note in render_target::get_sample_park_layout.
//
// This is the independent A/B knob. Set to 0 to get the previous behaviour exactly while
// leaving feedback parking alone, and vice versa.
// Held at 0 for the same reason. `redundant` in the RSXPROF line is the hard ceiling on what
// suppressing these teardowns can win; read that first, because if it is small then this trade
// (GENERAL costs UBWC while rendering, and HiZ on depth) cannot pay for itself and the answer
// is to leave both at 0.
inline constexpr u64 s_sample_park_frames = 0;
// Any parking at all. Gates the extra render pass dependency that makes parking safe.
inline constexpr bool s_surface_parking_enabled = (s_feedback_park_frames != 0) || (s_sample_park_frames != 0);
u64 get_renderpass_key(const std::vector<vk::image*>& images, const std::vector<u8>& input_attachment_ids = {});
u64 get_renderpass_key(const std::vector<vk::image*>& images, u64 previous_key);
u64 get_renderpass_key(VkFormat surface_format, u8 sample_count = 1);
@@ -67,24 +23,6 @@ namespace vk
void end_renderpass(const vk::command_buffer& cmd);
bool is_renderpass_open(const vk::command_buffer& cmd);
// Attachment set of the pass currently open on this command buffer.
//
// VUID-vkCmdPipelineBarrier-image-04073 only allows a barrier to be recorded inside a render
// pass instance when the image is an attachment of the current subpass. "The caller only ever
// does this for a bound render target" is not the same statement: the pass that is open is the
// one from the previous draw, and it is not re-created until the draw call itself, so between
// a framebuffer switch and the next draw the open pass belongs to the *previous* framebuffer.
// A surface that needs no layout change (because it is parked) does not end the pass on the
// way in, so it can reach a barrier site with a stale pass open and its own image absent from
// it. That is the corruption an earlier attempt hit; this makes the rule checkable instead of
// assumed.
//
// The set is cleared whenever a pass begins, so any pass opened by code that does not publish
// its attachments (overlays, texture cache, present) reads as covering nothing and in-pass
// barriers are refused for it. Fail-closed by construction.
void set_renderpass_attachments(const vk::command_buffer& cmd, const std::vector<vk::image*>& images);
bool renderpass_covers_image(const vk::command_buffer& cmd, VkImage image);
using renderpass_op_callback_t = std::function<void(const vk::command_buffer&, VkRenderPass, VkFramebuffer)>;
void renderpass_op(const vk::command_buffer& cmd, const renderpass_op_callback_t& op);
}
+6 -250
View File
@@ -1,6 +1,5 @@
#include "vkutils/data_heap.h"
#include "VKRenderTargets.h"
#include "VKRenderPass.h"
#include "VKResourceManager.h"
#include "Emu/RSX/rsx_methods.h"
#include "Emu/RSX/RSXThread.h"
@@ -652,14 +651,6 @@ namespace vk
void render_target::unspill(vk::command_buffer& cmd)
{
// The image below is a different VkImage with different contents, so any park describing
// the old one is meaningless. Both readers already re-check current_layout and unspill
// leaves it in an attachment or transfer layout rather than GENERAL, so this is belt and
// braces - but the read-synchronization tag is keyed on last_use_tag, which the recreate
// does not touch, and that is a hazard rather than a missed optimization if it ever
// lines up.
clear_sample_park();
// Recreate the image
const auto pdev = vk::get_current_renderer();
create_impl(*pdev, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, pdev->get_memory_mapping().device_local, VMM_ALLOCATION_POOL_SURFACE_CACHE);
@@ -856,182 +847,12 @@ namespace vk
return (scaled_w == width()) && (scaled_h == height());
}
VkImageLayout render_target::get_feedback_loop_layout(const vk::command_buffer& cmd)
{
// The layout a surface is parked in while it is both written and sampled by the same
// draw. Both of these are legal attachment layouts as well as legal sampling layouts,
// which is the property try_park_in_feedback_loop relies on: an attachment left here is
// still a valid attachment, and the render pass key encoder accepts both.
return cmd.get_command_pool().get_owner().get_framebuffer_loops_support()
? VK_IMAGE_LAYOUT_ATTACHMENT_FEEDBACK_LOOP_OPTIMAL_EXT
: VK_IMAGE_LAYOUT_GENERAL;
}
// The sample park.
//
// Iterations 1-3 attacked the feedback loop, which turned out to be a rounding error: the
// measured split of Draw:1093 came back 100% framebuffer mismatch and 0% render pass key, so
// layout churn was contributing nothing there. What did not move across any of the three was
// ImgHelper:43, which is change_image_layout ending the open pass, and the largest single
// contributor to it is the ordinary render-to-texture cycle:
//
// validate_image_layout_for_read_access drags an unbound RTT from
// COLOR_/DEPTH_STENCIL_ATTACHMENT_OPTIMAL to SHADER_READ_ONLY_OPTIMAL so it can be sampled,
// and prepare_surface_for_drawing drags it straight back the next time the game renders to
// it. Two layout changes, therefore two teardowns, per sample-then-rebind cycle.
//
// Only the first is a floor. The read genuinely needs a write -> read barrier and a barrier
// cannot be recorded inside a pass unless the image is an attachment of it, which a sampled
// unbound RTT is not. The return trip is pure overhead: GENERAL is a legal sampling layout
// AND a legal attachment layout, the descriptor path takes imageLayout from
// image->current_layout at bind time (vkutils/ex.cpp), and the render pass key encoder
// already accepts GENERAL. So the surface can simply be left where it is.
//
// What this does NOT do is make the first half free, and the honest read of the win is
// "half of the sample/rebind pairs, if and only if those teardowns were not going to happen
// anyway". That last clause is why g_rp_reopened exists: see rsx_profiler.h.
bool render_target::try_arm_sample_park()
{
if constexpr (s_sample_park_frames == 0)
{
return false;
}
// A bound attachment must never be parked from here. Its layout is baked into
// m_current_renderpass_key, which prepare_rtts computed at the end of the bind and which
// nothing recomputes between there and the draw. Moving a bound surface to GENERAL would
// leave the pass declaring initialLayout = ATTACHMENT_OPTIMAL for an image that is not in
// it, which is undefined contents rather than a validation error.
//
// Today the same mistake is caught loudly - renderpass_key_blob::set_layout throws on
// SHADER_READ_ONLY_OPTIMAL - and GENERAL would encode silently, so this check is what
// replaces the throw. The sampled-while-bound case is a feedback loop and belongs to
// texture_barrier regardless.
if (is_bound)
{
return false;
}
// MSAA surfaces sample through a resolve target and carry a second image whose contents
// are produced by resolve/unresolve passes that move layouts on their own schedule.
// Not worth the reasoning for the volume involved.
if (samples() > 1)
{
return false;
}
const auto renderer = rsx::get_current_renderer();
if (!renderer)
{
return false;
}
m_sample_park_deadline = renderer->int_flip_index + s_sample_park_frames + 1;
return true;
}
bool render_target::try_park_for_sampling()
{
if (!m_sample_park_deadline)
{
return false;
}
// Same honesty check the feedback park makes: parking never suppresses a transition, it
// only declines to start one. If anything has moved the layout since - a blit, a spill,
// an unspill that rebuilt the image as UNDEFINED, the zeta fall-out valve - the park is
// over and the normal path runs.
if (current_layout != get_sample_park_layout())
{
clear_sample_park();
return false;
}
const auto renderer = rsx::get_current_renderer();
if (renderer && renderer->int_flip_index >= m_sample_park_deadline)
{
// Expired. Let the surface go back to the attachment-optimal layout and get its
// compression back. On Adreno that is UBWC on a full-size render target, which is
// not something to hold hostage to a surface that has stopped being sampled.
clear_sample_park();
return false;
}
return true;
}
bool render_target::sample_park_needs_read_barrier() const
{
// last_use_tag advances on every write: on_write / on_write_fast at the end of each draw
// that had this surface bound with writes enabled, on_write after a clear or a memory
// initialize, on_write_copy after a blit. Equal tags therefore mean "nothing has written
// this since the barrier that made the previous read safe".
return !m_sample_park_sync_tag || m_sample_park_sync_tag != last_use_tag;
}
void render_target::on_sample_park_synced()
{
m_sample_park_sync_tag = last_use_tag;
}
void render_target::clear_sample_park()
{
m_sample_park_deadline = 0;
m_sample_park_sync_tag = 0;
}
void render_target::arm_feedback_park()
{
if constexpr (s_feedback_park_frames == 0)
{
return;
}
const auto renderer = rsx::get_current_renderer();
if (!renderer)
{
return;
}
// Deadline is exclusive, so the arming frame itself is always covered even when the
// window is one flip. int_flip_index only moves on the RSX thread, which is also the
// only thread that reaches this, so no synchronization is needed on it.
m_feedback_park_deadline = renderer->int_flip_index + s_feedback_park_frames + 1;
}
bool render_target::try_park_in_feedback_loop(const vk::command_buffer& cmd)
{
// The layout test is what keeps this honest: parking never suppresses a transition, it
// only declines to start one. A surface that is not already sitting in the loop layout
// takes the normal path, so nothing here can produce a barrier with old != new, and
// nothing here can leave an attachment in a layout the render pass key does not encode.
if (!m_feedback_park_deadline || current_layout != get_feedback_loop_layout(cmd))
{
// Either never armed, or something outside parking has already moved the layout -
// a non-cyclic sample dragging it to SHADER_READ_ONLY_OPTIMAL, the zeta fall-out
// valve in VKDraw restoring HiZ, a blit, a spill. The park is over either way, and
// leaving the deadline set would let a later unrelated visit to the loop layout
// re-park a surface that never asked for it.
m_feedback_park_deadline = 0;
return false;
}
const auto renderer = rsx::get_current_renderer();
if (renderer && renderer->int_flip_index >= m_feedback_park_deadline)
{
// Expired. Fall through to the normal path, which puts the surface back in the
// attachment-optimal layout and hands its compression back.
m_feedback_park_deadline = 0;
return false;
}
return true;
}
void render_target::texture_barrier(vk::command_buffer& cmd)
{
const auto is_framebuffer_read_only = is_depth_surface() && !rsx::method_registers.depth_write_enabled();
const auto optimal_layout = get_feedback_loop_layout(cmd);
const auto supports_fbo_loops = cmd.get_command_pool().get_owner().get_framebuffer_loops_support();
const auto optimal_layout = supports_fbo_loops ? VK_IMAGE_LAYOUT_ATTACHMENT_FEEDBACK_LOOP_OPTIMAL_EXT
: VK_IMAGE_LAYOUT_GENERAL;
if (m_cyclic_ref_tracker.can_skip() && current_layout == optimal_layout && is_framebuffer_read_only)
{
@@ -1058,35 +879,6 @@ namespace vk
vk::insert_texture_barrier(cmd, this, optimal_layout, preserve_renderpass);
m_cyclic_ref_tracker.on_insert_texture_barrier();
// Open (or re-open) the park window. This is the only writer.
//
// The first attempt keyed parking off m_cyclic_ref_tracker.is_enabled() and it could not
// hold for more than a single bind, because the tracker is a within-loop signal that
// three separate paths clear before the next bind ever runs:
// - prepare_surface_for_drawing calls reset_surface_counters() at every bind;
// - post_texture_barrier() resets it, and that is the *normal* end of an episode - the
// first draw after the loop with the surface still bound trips
// requires_post_loop_barrier() and resets before the next bind is reached;
// - memory_barrier() resets it when the layout moved elsewhere.
// So the predicate was true only on a bind directly preceded by a feedback draw, and the
// surface was dragged out of the loop layout on the very next bind and back in on the one
// after. Measured on Arkham City: the transitions moved from the bind site to the barrier
// site and the pass count did not change.
//
// A window rather than a flag because the useful question is "has this surface been in a
// loop recently", and an episode that recurs within a few binds should not pay to leave
// and re-enter the layout in between. It decays so a surface that has genuinely stopped
// looping goes back to the attachment-optimal layout and gets its compression back; the
// depth case is the one that matters, HiZ is not free to give up indefinitely.
//
// The second attempt made that window four binds and that was still the wrong unit: see
// s_feedback_park_frames. It is counted in flips now, so an episode that recurs every
// frame - which is what a render graph does - keeps the surface parked continuously
// instead of dropping it between episodes.
#ifdef __ANDROID__
arm_feedback_park();
#endif
if (is_framebuffer_read_only)
{
m_cyclic_ref_tracker.allow_skip();
@@ -1108,47 +900,23 @@ namespace vk
VkPipelineStageFlags src_stage, dst_stage;
VkAccessFlags src_access, dst_access;
// This barrier does not move the layout - it is the read-completes-before-write half of a
// feedback loop on a surface that is still the bound attachment. Both of the conditions
// that let a barrier stay inside the pass therefore hold, and it was still ending the
// pass on every fall-out purely because it never asked not to. The render pass now
// declares the read -> write direction of the self-dependency, so it can ask.
//
// The vertex stage has to come off the source scope when the barrier lands inside the
// pass: VK_DEPENDENCY_BY_REGION_BIT admits framebuffer-space stages only, and a vertex
// shader sampling a live attachment is not a tile-local dependency. That is the same
// trade texture_barrier already makes on this path, and it is sound for what this
// synchronizes - the fall-out is a fragment-shader read of the attachment the next draw
// writes. Outside the pass the wider scope is kept.
#ifdef __ANDROID__
const bool preserve_renderpass = vk::renderpass_covers_image(cmd, value);
#else
constexpr bool preserve_renderpass = false;
#endif
if (!is_depth_surface()) [[likely]]
{
src_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
src_stage = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dst_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
src_access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
dst_access = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
}
else
{
src_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
src_stage = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
dst_stage = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
src_access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
dst_access = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
}
if (!preserve_renderpass)
{
src_stage |= VK_PIPELINE_STAGE_VERTEX_SHADER_BIT;
}
vk::insert_image_memory_barrier(cmd, value, current_layout, current_layout,
src_stage, dst_stage, src_access, dst_access, { aspect(), 0, 1, 0, 1 },
preserve_renderpass);
src_stage, dst_stage, src_access, dst_access, { aspect(), 0, 1, 0, 1 });
m_cyclic_ref_tracker.reset();
}
@@ -1157,18 +925,6 @@ namespace vk
{
frame_tag = 0;
m_cyclic_ref_tracker.reset();
// Belt and braces for the sample park's read-synchronization tag. This is called from
// prepare_surface_for_drawing and from the zeta fall-out valve, both of which mean "this
// surface is or was just an attachment", so the next sample must re-establish a
// write -> read barrier. last_use_tag alone would already say so for anything that
// reaches on_write; this covers the paths that write pixels through some route that does
// not. Costs at most one extra barrier on a surface that was bound and never written.
//
// Deliberately does NOT clear m_sample_park_deadline. The park has to outlive the bind
// that resets the counters or it can never hold for more than one bind - the same
// mistake the first feedback park made.
m_sample_park_sync_tag = 0;
}
image_view* render_target::get_view(const rsx::texture_channel_remap_t& remap, VkImageAspectFlags mask)
+6 -133
View File
@@ -5,7 +5,6 @@
#include "VKFormats.h"
#include "VKHelpers.h"
#include "VKRenderPass.h"
#include "vkutils/barriers.h"
#include "vkutils/buffer_object.h"
#include "vkutils/device.h"
@@ -73,35 +72,6 @@ namespace vk
// Cyclic reference hazard tracking
image_reference_sync_barrier m_cyclic_ref_tracker;
// Flip index this surface stops being left in the feedback-loop layout at. 0 = not parked.
//
// Deliberately NOT part of m_cyclic_ref_tracker, and deliberately not cleared by
// reset_surface_counters. The tracker answers "is a loop in progress right now", which is
// a within-draw question, and every bind resets it. What parking needs is "was this
// surface in a loop recently", which has to outlive the bind that resets the tracker or
// it can never hold for more than one bind. See the comment on texture_barrier.
u64 m_feedback_park_deadline = 0;
// Same idea, for the far more common case: a render target sampled while it is NOT bound.
// Flip index the sample park expires at; 0 = not parked.
u64 m_sample_park_deadline = 0;
// last_use_tag as of the last read barrier issued for this surface while parked. The
// surface is safe to sample with no barrier at all exactly while this still matches
// last_use_tag, because that tag advances on every write (surface_store::on_write and
// on_write_fast at end of draw, on_write_copy after a blit).
//
// This is the piece the layout used to carry implicitly. Without parking, "already in
// SHADER_READ_ONLY_OPTIMAL" meant "already synchronized for reading", so the second and
// later draws sampling the same texture fell through validate_image_layout_for_read_access
// with no barrier. Leave the surface in GENERAL and that signal is gone: every sampling
// draw would look identical to the first and issue its own barrier, which for a shadow
// map sampled by hundreds of draws would turn one teardown into hundreds. The tag makes
// the same statement explicitly and does not depend on the layout.
//
// 0 = never synchronized, always barrier.
u64 m_sample_park_sync_tag = 0;
// Memory spilling support
std::unique_ptr<vk::buffer> m_spilled_mem;
@@ -152,44 +122,6 @@ namespace vk
void memory_barrier(vk::command_buffer& cmd, rsx::surface_access access);
void read_barrier(vk::command_buffer& cmd) { memory_barrier(cmd, rsx::surface_access::shader_read); }
void write_barrier(vk::command_buffer& cmd) { memory_barrier(cmd, rsx::surface_access::shader_write); }
// Layout a surface is parked in while it is written and sampled by the same draw.
// See vk::s_feedback_park_frames in VKRenderPass.h for the window and the A/B knob.
static VkImageLayout get_feedback_loop_layout(const vk::command_buffer& cmd);
// True when this bind may leave the surface in the feedback-loop layout instead of
// dragging it back to the attachment-optimal one.
bool try_park_in_feedback_loop(const vk::command_buffer& cmd);
// Arms the park window. Called from the barrier sites that establish a feedback loop.
void arm_feedback_park();
// Layout a surface is parked in while it is sampled without being bound.
//
// Plain GENERAL, never ATTACHMENT_FEEDBACK_LOOP_OPTIMAL_EXT, and that is a deliberate
// narrowing rather than an oversight. The feedback-loop layout is only meaningful for an
// image that is simultaneously an attachment and a sampled texture, and using it legally
// requires the pipelines and framebuffer to be created with the matching
// VK_PIPELINE_CREATE_COLOR_ATTACHMENT_FEEDBACK_LOOP_BIT_EXT /
// VK_IMAGE_VIEW_CREATE_..._EXT flags, which nothing in this backend sets. The existing
// feedback park already sits on that exposure; there is no reason to enlarge it for a
// case that is not a feedback loop at all. GENERAL is unconditionally legal both as an
// attachment layout and as a sampled layout, and the render pass key encoder accepts it
// (VKRenderPass.cpp set_layout), which is everything this needs.
static constexpr VkImageLayout get_sample_park_layout() { return VK_IMAGE_LAYOUT_GENERAL; }
// True when a non-cyclic sample of this surface should leave it in GENERAL rather than
// dragging it to SHADER_READ_ONLY_OPTIMAL. Arms the park window as a side effect.
// Takes no command buffer: unlike the feedback layout, the sample park layout is a
// constant and needs no device query.
bool try_arm_sample_park();
// True when this bind may leave a sample-parked surface where it is.
bool try_park_for_sampling();
// True when a parked surface has been written since its last read barrier and therefore
// needs another one. False means the sample can be issued with no barrier at all.
bool sample_park_needs_read_barrier() const;
// Records that a read barrier covering the current contents has just been issued.
void on_sample_park_synced();
// Drops the park. Any path that moves the layout out from under it lands here.
void clear_sample_park();
};
static inline vk::render_target* as_rtt(vk::image* t)
@@ -507,72 +439,13 @@ namespace vk
// Special case barrier
surface->memory_barrier(cmd, rsx::surface_access::gpu_reference);
// A surface in a feedback loop is dragged back and forth for no reason. The loop
// parks it in GENERAL (or ATTACHMENT_FEEDBACK_LOOP_OPTIMAL), this snapped it back to
// COLOR_/DEPTH_STENCIL_ATTACHMENT_OPTIMAL on the next bind, and the next draw's
// texture barrier moved it straight back. Both halves of that round trip are layout
// changes, and a layout change cannot happen inside a render pass, so each one ends
// the pass and forces a tile store plus a full reload.
//
// It shows up three times in the same profile of Arkham City, which draws 93 to 113
// render passes a frame for 6 logical ones:
// - change_image_layout from bind_surface_address<depth> ends 34.6 passes a frame,
// and <color> another 11.0. This is that snap-back.
// - the return trip in texture_barrier is a second layout change. Android asks it
// to keep the pass open, which was recorded as an in-pass layout change and is
// undefined; with the surface already parked the barrier no longer moves the
// layout and becomes legal.
// - the layout is part of the render pass key, so every flip also invalidates the
// cached render pass and costs a restart at the draw site (24.3 a frame).
//
// Both parking layouts are legal for an attachment as well as for sampling, and
// VKRenderPass.cpp's key encoder already accepts both, so the surface can simply be
// left where it is. This is not suppressing a required pass break: there is no
// transition left to require one.
//
// The park has to outlive reset_surface_counters() below, which is why it is a
// separate window on the surface rather than a read of m_cyclic_ref_tracker. Keyed
// off the tracker it survived exactly one bind, which bought nothing: the round trip
// moved from this site to the barrier site and the frame's pass count did not move.
//
// Note what this site is and is not. Declining the change_layout here is what leaves
// the previous draw's render pass open into the rest of prepare_rtts, so a teardown
// that used to be charged to this site is now charged to whichever site first finds
// a mismatch - usually the draw call. That relocation is not a cost; the win is only
// real where the surface is still parked on the next bind and there is no round trip
// to pay for at all, which is what the frames-not-binds window is for.
//
// Android only. This is where the tile traffic was measured, and a desktop GPU pays
// for GENERAL attachments (lost depth/colour compression) without the tile store to
// win back.
//
// Two independent parks meet here, and the second is the one this iteration adds.
// The feedback park covers a surface written and sampled by the same draw, which is
// rare; the sample park covers a surface sampled while unbound and then rendered to
// again, which is the ordinary render-to-texture cycle and is where the volume is.
// Either one may hold, and both leave the surface in a layout that is legal as an
// attachment and encodable in the render pass key.
//
// Order matters only for the side effects: try_park_in_feedback_loop clears its own
// deadline when the layout has moved, so it must still run even when the sample park
// would answer first. || would short-circuit it, so both are evaluated.
bool keep_current_layout = false;
#ifdef __ANDROID__
const bool feedback_parked = surface->try_park_in_feedback_loop(cmd);
const bool sample_parked = surface->try_park_for_sampling();
keep_current_layout = feedback_parked || sample_parked;
#endif
if (!keep_current_layout)
if (surface->aspect() == VK_IMAGE_ASPECT_COLOR_BIT)
{
if (surface->aspect() == VK_IMAGE_ASPECT_COLOR_BIT)
{
surface->change_layout(cmd, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
}
else
{
surface->change_layout(cmd, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
}
surface->change_layout(cmd, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
}
else
{
surface->change_layout(cmd, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);
}
surface->reset_surface_counters();
+2 -211
View File
@@ -214,195 +214,8 @@ namespace
};
}
#ifdef __ANDROID__
namespace
{
// Never withhold more than this fraction of the attribute ring from the allocator.
constexpr u32 VTX_RETAIN_MAX_SHIFT = 1; // <= half the ring
// Free space kept available, as a multiple of the recent per-frame peak. Two frames can be in
// flight at once (see advance_queued_frames), so anything under 2 would force the ring to grow;
// 3 leaves a frame of slack on top of that.
constexpr u64 VTX_RETAIN_HEADROOM_FRAMES = 3;
// Hard ceiling on how long an entry may live regardless of how much room the ring has.
// find_vertex_range only fingerprints the first 8 bytes of the guest source, so geometry that
// mutates without touching its first 8 bytes reads as unchanged. That was near-harmless when
// entries lived for one frame; capping the age bounds how long such an entry can be wrong.
constexpr u32 VTX_RETAIN_MAX_FRAMES = 8;
}
// Bring the monotonic ring cursor up to date, and notice if the heap was thrown away underneath us.
//
// m_attrib_ring_info is the only consumer of this ring (upload_vertex_data holds the sole alloc
// sites), so sampling PUT here accounts for every byte it hands out. The cursor is what makes a
// cached offset_in_heap checkable at all: the ring wraps, so an offset on its own cannot say
// whether it still names the data that was written there or the data that lapped over it.
void VKGSRender::vertex_cache_sample_heap()
{
const VkBuffer handle = m_attrib_ring_info.heap ? m_attrib_ring_info.heap->value : VK_NULL_HANDLE;
if (handle != m_vtx_heap_handle)
{
vertex_cache_on_heap_reset();
return;
}
const usz heap_size = m_attrib_ring_info.size();
if (!heap_size)
{
return;
}
// get_current_put_pos_minus_one() reports PUT - 1, wrapping to size - 1 when PUT is zero.
const usz put = (m_attrib_ring_info.get_current_put_pos_minus_one() + 1) % heap_size;
// Sampled at the head of every upload and once per frame, so at most one draw's worth of
// allocation separates two samples and the delta can never hide a full lap.
m_vtx_heap_cursor += (put >= m_vtx_heap_put)
? (put - m_vtx_heap_put)
: (heap_size - m_vtx_heap_put + put);
m_vtx_heap_put = put;
}
// vk::data_heap::grow() disposes the old VkBuffer and creates a new one without copying anything
// across, and re-inits the ring to PUT=0. Every offset the cache is holding names memory in a
// buffer that no longer exists, so nothing survives this.
void VKGSRender::vertex_cache_on_heap_reset()
{
m_vertex_cache->purge();
if (m_vtx_reserve_bytes)
{
// The ring grew while we were holding part of it back, which means the reservation was
// sized too optimistically for this title. Growing the attribute ring on this platform is
// expensive and has run away before (see the note in flush_command_queue), so stop
// reserving for the rest of the session rather than risk a second one.
rsx_log.notice("Vertex cache retention disabled: attribute ring was reallocated while %uK was reserved.",
static_cast<u32>(m_vtx_reserve_bytes / 1024));
m_vtx_retention_locked_out = true;
}
m_vtx_heap_handle = m_attrib_ring_info.heap ? m_attrib_ring_info.heap->value : VK_NULL_HANDLE;
m_vtx_heap_cursor = 0;
m_vtx_heap_put = 0;
m_vtx_retain_floor = 0;
m_vtx_frame_base = 0;
m_vtx_frame_peak = 0;
m_vtx_peak_ttl = 0;
m_vtx_reserve_bytes = 0;
m_vtx_frame_index = 0;
std::fill(std::begin(m_vtx_frame_marks), std::end(m_vtx_frame_marks), 0ull);
m_vertex_cache->set_epoch(0);
}
// Decide how much of the ring to withhold from the allocator, then evict everything the withheld
// window does not cover. Called where the unconditional purge() used to be, at frame end.
//
// Why any of this exists: Arkham City reports 10 vertex cache hits out of 1386 requests, so 2499us
// of vertex upload and 1.35M vertices in a single pass are re-uploaded and re-binned every frame
// even though the geometry is unchanged. The cache was purged every frame because offset_in_heap
// points into a ring, and a ring recycles.
//
// The guarantee: the allocator already refuses to allocate across GET (rsx::data_heap::can_alloc),
// which is how in-flight frames are protected from being overwritten. Publishing GET as the
// retention floor rather than the frame's PUT puts retained entries inside that same protected
// window, so the ring physically cannot lap onto them - it grows instead. The floor only ever moves
// forward, and it moves at exactly the moment the matching entries are evicted, so a live entry is
// never below GET.
void VKGSRender::vertex_cache_on_frame_end()
{
vertex_cache_sample_heap();
const u64 heap_size = m_attrib_ring_info.size();
const u64 consumed = m_vtx_heap_cursor - m_vtx_frame_base;
m_vtx_frame_base = m_vtx_heap_cursor;
m_vtx_frame_marks[m_vtx_frame_index % VTX_RETAIN_MAX_FRAMES] = m_vtx_heap_cursor;
m_vtx_frame_index++;
// Hold the peak for a short window so one quiet frame cannot shrink the margin right before a
// busy one lands on it.
if (consumed >= m_vtx_frame_peak || !m_vtx_peak_ttl)
{
m_vtx_frame_peak = consumed;
m_vtx_peak_ttl = VTX_RETAIN_MAX_FRAMES;
}
else
{
m_vtx_peak_ttl--;
}
u64 reserve = 0;
// Nothing to retain when the cache is a null_vertex_cache, and reserving part of the ring for
// it would shrink the allocator's working set for no reason at all.
const bool cache_active = !g_cfg.video.disable_vertex_cache;
if (cache_active && !m_vtx_retention_locked_out && m_vtx_frame_peak && heap_size)
{
const u64 headroom = VTX_RETAIN_HEADROOM_FRAMES * m_vtx_frame_peak;
// Retention is only worth anything if what is left over can hold at least one frame of
// geometry; below that every entry dies before it can be hit and the ring would just be
// smaller for nothing. That threshold is heap_size >= (headroom + 1 frame).
if (heap_size >= headroom + m_vtx_frame_peak)
{
reserve = std::min<u64>(heap_size >> VTX_RETAIN_MAX_SHIFT, heap_size - headroom);
}
}
// Say when this engages or drops out, and on what measurement. Otherwise a hit rate that stays
// at zero cannot be told apart from a reservation that was never sized above zero to begin
// with, which is the whole question when reading the counters back off a device.
if (!!reserve != !!m_vtx_reserve_bytes)
{
rsx_log.notice("Vertex cache retention %s (reserve=%uK, peak frame=%uK, ring=%uM).",
reserve ? "engaged" : "disengaged",
static_cast<u32>(reserve / 1024),
static_cast<u32>(m_vtx_frame_peak / 1024),
static_cast<u32>(heap_size / 0x100000));
}
if (!reserve)
{
// No room to promise anything. Behave exactly as this call site did before.
m_vtx_reserve_bytes = 0;
m_vtx_retain_floor = m_vtx_heap_cursor;
m_vertex_cache->purge();
return;
}
m_vtx_reserve_bytes = reserve;
u64 floor = (m_vtx_heap_cursor > reserve) ? (m_vtx_heap_cursor - reserve) : 0;
// Apply the age cap on top of the byte budget, taking whichever is stricter.
if (m_vtx_frame_index > VTX_RETAIN_MAX_FRAMES)
{
floor = std::max(floor, m_vtx_frame_marks[m_vtx_frame_index % VTX_RETAIN_MAX_FRAMES]);
}
m_vtx_retain_floor = floor;
m_vertex_cache->evict_before(floor);
}
#endif
vk::vertex_upload_info VKGSRender::upload_vertex_data()
{
#ifdef __ANDROID__
// Before the lookup below: a heap reallocation since the last draw invalidates every entry.
vertex_cache_sample_heap();
// Stamp entries with the cursor as of the start of this call, which is at or below where their
// own bytes actually land. Erring low can only retire an entry early, never late.
m_vertex_cache->set_epoch(m_vtx_heap_cursor);
#endif
draw_command_visitor visitor(m_index_buffer_ring_info, m_vertex_layout);
auto result = std::visit(visitor, m_draw_processor.get_draw_command(rsx::method_registers));
@@ -421,17 +234,12 @@ vk::vertex_upload_info VKGSRender::upload_vertex_data()
u32 persistent_range_base = -1, volatile_range_base = -1;
usz persistent_offset = -1, volatile_offset = -1;
// Hoisted out of the block below so the allocations that follow can retract a cache hit if they
// end up reallocating the heap the hit points into.
bool in_cache = false;
if (required.first > 0)
{
//Check if cacheable
//Only data in the 'persistent' block may be cached
//TODO: hook the notify command. Entries now outlive the frame they were written in (see
//vertex_cache_on_frame_end), but find_vertex_range still detects guest-side edits only by
//fingerprinting the first 8 bytes of the source, not by locking the memory range.
//TODO: make vertex cache keep local data beyond frame boundaries and hook notify command
bool in_cache = false;
bool to_store = false;
u32 storage_address = -1;
@@ -477,23 +285,6 @@ vk::vertex_upload_info VKGSRender::upload_vertex_data()
volatile_range_base = static_cast<u32>(volatile_offset);
}
#ifdef __ANDROID__
// An allocation above may have grown the heap, which replaces the VkBuffer outright without
// copying the old contents over. Offsets returned by alloc() are already relative to the new
// buffer, but one taken from the cache before that point is not: it names a buffer that has
// just been disposed. Retract the hit and upload the data normally.
if (in_cache && m_attrib_ring_info.heap && m_attrib_ring_info.heap->value != m_vtx_heap_handle)
{
vertex_cache_on_heap_reset();
in_cache = false;
m_frame_stats.vertex_cache_miss_count++;
persistent_offset = static_cast<u32>(m_attrib_ring_info.alloc<256>(required.first));
persistent_range_base = static_cast<u32>(persistent_offset);
}
#endif
//Write all the data once if possible
if (required.first && required.second && volatile_offset > persistent_offset)
{
-4
View File
@@ -96,7 +96,6 @@ extern "C"
PFN_vkDestroyInstance vkDestroyInstance = nullptr;
PFN_vkDestroyPipeline vkDestroyPipeline = nullptr;
PFN_vkDestroyPipelineCache vkDestroyPipelineCache = nullptr;
PFN_vkGetPipelineCacheData vkGetPipelineCacheData = nullptr;
PFN_vkDestroyPipelineLayout vkDestroyPipelineLayout = nullptr;
PFN_vkDestroyQueryPool vkDestroyQueryPool = nullptr;
PFN_vkDestroyRenderPass vkDestroyRenderPass = nullptr;
@@ -340,8 +339,6 @@ namespace vk::android
if (!vkDestroyPipeline) vkDestroyPipeline = reinterpret_cast<PFN_vkDestroyPipeline>(dlsym(handle, "vkDestroyPipeline"));
vkDestroyPipelineCache = reinterpret_cast<PFN_vkDestroyPipelineCache>(vkGetInstanceProcAddr(nullptr, "vkDestroyPipelineCache"));
if (!vkDestroyPipelineCache) vkDestroyPipelineCache = reinterpret_cast<PFN_vkDestroyPipelineCache>(dlsym(handle, "vkDestroyPipelineCache"));
vkGetPipelineCacheData = reinterpret_cast<PFN_vkGetPipelineCacheData>(vkGetInstanceProcAddr(nullptr, "vkGetPipelineCacheData"));
if (!vkGetPipelineCacheData) vkGetPipelineCacheData = reinterpret_cast<PFN_vkGetPipelineCacheData>(dlsym(handle, "vkGetPipelineCacheData"));
vkDestroyPipelineLayout = reinterpret_cast<PFN_vkDestroyPipelineLayout>(vkGetInstanceProcAddr(nullptr, "vkDestroyPipelineLayout"));
if (!vkDestroyPipelineLayout) vkDestroyPipelineLayout = reinterpret_cast<PFN_vkDestroyPipelineLayout>(dlsym(handle, "vkDestroyPipelineLayout"));
vkDestroyQueryPool = reinterpret_cast<PFN_vkDestroyQueryPool>(vkGetInstanceProcAddr(nullptr, "vkDestroyQueryPool"));
@@ -535,7 +532,6 @@ namespace vk::android
if (auto p = vkGetInstanceProcAddr(instance, "vkDestroyInstance")) vkDestroyInstance = reinterpret_cast<PFN_vkDestroyInstance>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkDestroyPipeline")) vkDestroyPipeline = reinterpret_cast<PFN_vkDestroyPipeline>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkDestroyPipelineCache")) vkDestroyPipelineCache = reinterpret_cast<PFN_vkDestroyPipelineCache>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkGetPipelineCacheData")) vkGetPipelineCacheData = reinterpret_cast<PFN_vkGetPipelineCacheData>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkDestroyPipelineLayout")) vkDestroyPipelineLayout = reinterpret_cast<PFN_vkDestroyPipelineLayout>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkDestroyQueryPool")) vkDestroyQueryPool = reinterpret_cast<PFN_vkDestroyQueryPool>(p);
if (auto p = vkGetInstanceProcAddr(instance, "vkDestroyRenderPass")) vkDestroyRenderPass = reinterpret_cast<PFN_vkDestroyRenderPass>(p);
-1
View File
@@ -117,7 +117,6 @@ extern "C"
extern PFN_vkDestroyInstance vkDestroyInstance;
extern PFN_vkDestroyPipeline vkDestroyPipeline;
extern PFN_vkDestroyPipelineCache vkDestroyPipelineCache;
extern PFN_vkGetPipelineCacheData vkGetPipelineCacheData;
extern PFN_vkDestroyPipelineLayout vkDestroyPipelineLayout;
extern PFN_vkDestroyQueryPool vkDestroyQueryPool;
extern PFN_vkDestroyRenderPass vkDestroyRenderPass;

Some files were not shown because too many files have changed in this diff Show More