Files
jpolo1224 13dcec9e66 Merge RPCS3 upstream, excluding the ISO timestamp change
14 of the 15 upstream commits since bab81aa23. The fifteenth, 3aea3b15d 'Fix
ISO timestamps', is deliberately left out: it touches rpcs3/Loader/ISO.cpp,
and the Aug-2026 upstream ISO refactor is already reverted here because it
breaks some images (region_count reads 0 and the disc will not mount). It is
the tip commit, so merging its parent excluded it exactly, with no surgery.

Four conflicts, all of them ours-and-theirs rather than either-or:

nv4097.cpp conflicted whole-file. Took ours and applied upstream 071c9f10f's
set_shading_mode by hand -- an earlier merge of this same file lost two hunks
by resolving it wholesale, and that is recorded in c6a0878a9.

VKPipelineCompiler.cpp: ours has the mobile dynamic-state work (topology-class
collapsing, normalize_dynamic_pipeline_state, compiler thread affinity),
upstream adds a provoking-vertex chain for flat shading. They are independent,
so both are in, with the rasterization state rebased as upstream needs.

device.cpp: three hunks, all parallel feature queries -- extended dynamic
state and the Android LSFG feature bits on our side, provoking vertex on
theirs. All kept; ours' extension push needed its own closing brace.

BUILDING.md stays deleted: 30fc4e566 folded it into the README, and upstream
merely edited it.

Core builds and links.
2026-08-24 16:13:52 -04:00

4714 lines
150 KiB
C++

#include "stdafx.h"
#include "RSXThread.h"
#include "Capture/rsx_capture.h"
#include "Common/surface_store.h"
#include "Core/RSXReservationLock.hpp"
#include "Core/RSXEngLock.hpp"
#include "Host/MM.h"
#include "Host/RSXDMAWriter.h"
#include "NV47/HW/context.h"
#include "Program/GLSLCommon.h"
#include "rsx_methods.h"
#include "rsx_profiler.h"
#include "gcm_printing.h"
#include "RSXDisAsm.h"
#ifdef __ANDROID__
#include <unistd.h> // ::gettid() for the ADPF feed
#endif
#include "Emu/Memory/vm_reservation.h"
#include "Emu/System.h"
#include "Emu/system_utils.hpp"
#include "Emu/Cell/PPUThread.h"
#include "Emu/Cell/PPUDisAsm.h"
#include "Emu/Cell/SPUDisAsm.h"
#include "Emu/Cell/SPUThread.h"
#include "Emu/Cell/timers.hpp"
#include "Emu/Cell/lv2/sys_event.h"
#include "Emu/Cell/lv2/sys_spu.h"
#include "Emu/Cell/lv2/sys_time.h"
#include "Emu/Cell/Modules/cellGcmSys.h"
#include "util/serialization_ext.hpp"
#include "Overlays/overlay_perf_metrics.h"
#include "Overlays/overlay_debug_overlay.h"
#include "Overlays/overlay_manager.h"
#include "Overlays/overlay_message.h"
#include "Emu/system_progress.hpp"
#include "Utilities/date_time.h"
#include "util/asm.hpp"
#include <span>
#include <thread>
#include <unordered_set>
class GSRender;
#define CMD_DEBUG 0
atomic_t<bool> g_user_asked_for_recording = false;
atomic_t<bool> g_user_asked_for_screenshot = false;
atomic_t<bool> g_user_asked_for_frame_capture = false;
atomic_t<bool> g_disable_frame_limit = false;
rsx::frame_trace_data frame_debug;
rsx::frame_capture_data frame_capture;
extern CellGcmOffsetTable offsetTable;
extern thread_local std::string(*g_tls_log_prefix)();
extern atomic_t<u32> g_lv2_preempts_taken;
LOG_CHANNEL(perf_log, "PERF");
template <>
bool serialize<rsx::rsx_state>(utils::serial& ar, rsx::rsx_state& o)
{
ar(o.transform_program);
// Work around for old RSX captures.
// RSX capture and savestates both call this method.
// We do not want to grab transform constants if it is not savestate capture.
const bool is_savestate_capture = thread_ctrl::get_current() && thread_ctrl::get_name() == "Emu State Capture Thread";
if (GET_SERIALIZATION_VERSION(global_version) || is_savestate_capture)
{
ar(o.transform_constants);
}
return ar(o.registers);
}
template <>
bool serialize<rsx::frame_capture_data>(utils::serial& ar, rsx::frame_capture_data& o)
{
ar(o.magic, o.version, o.LE_format);
if (o.magic != rsx::c_fc_magic || o.version != rsx::c_fc_version || o.LE_format != u32{std::endian::little == std::endian::native})
{
return false;
}
return ar(o.tile_map, o.memory_map, o.memory_data_map, o.display_buffers_map, o.replay_commands, o.reg_state);
}
template <>
bool serialize<rsx::frame_capture_data::memory_block_data>(utils::serial& ar, rsx::frame_capture_data::memory_block_data& o)
{
return ar(o.data);
}
template <>
bool serialize<rsx::frame_capture_data::replay_command>(utils::serial& ar, rsx::frame_capture_data::replay_command& o)
{
return ar(o.rsx_command, o.memory_state, o.tile_state, o.display_buffer_state);
}
template <>
bool serialize<rsx::rsx_iomap_table>(utils::serial& ar, rsx::rsx_iomap_table& o)
{
// We do not need more than that
ar(std::span(o.ea.data(), 512));
if (!ar.is_writing())
{
// Populate o.io
for (const atomic_t<u32>& ea_addr : o.ea)
{
const u32& addr = ea_addr.raw();
if (addr != umax)
{
o.io[addr >> 20].raw() = static_cast<u32>(&ea_addr - o.ea.data()) << 20;
}
}
}
return true;
}
namespace rsx
{
std::function<bool(u32 addr, bool is_writing)> g_access_violation_handler;
// TODO: Proper context manager
static rsx::context s_ctx{ .rsxthr = nullptr, .register_state = &method_registers };
rsx_iomap_table::rsx_iomap_table() noexcept
: ea(fill_array(-1))
, io(fill_array(-1))
{
}
u32 get_address(u32 offset, u32 location, u32 size_to_check, std::source_location src_loc)
{
const auto render = get_current_renderer();
std::string_view msg;
switch (location)
{
case CELL_GCM_CONTEXT_DMA_MEMORY_FRAME_BUFFER:
case CELL_GCM_LOCATION_LOCAL:
{
if (offset < render->local_mem_size && render->local_mem_size - offset >= size_to_check)
{
return rsx::constants::local_mem_base + offset;
}
msg = "Local RSX offset out of range!"sv;
break;
}
case CELL_GCM_CONTEXT_DMA_MEMORY_HOST_BUFFER:
case CELL_GCM_LOCATION_MAIN:
{
if (const u32 ea = render->iomap_table.get_addr(offset); ea != umax)
{
if (size_to_check <= 1 || (offset < render->main_mem_size && render->main_mem_size - offset >= size_to_check))
{
bool ok = true;
for (u32 offs_index = 0x100000; offs_index < size_to_check + (offset & 0xfffff); offs_index += 0x100000)
{
// This check does not check continuity but rather that it's mapped at all
if (render->iomap_table.get_addr(offset + offs_index) == umax)
{
ok = false;
}
}
if (ok)
{
if (!size_to_check || vm::check_addr(ea, 0, size_to_check))
{
return ea;
}
}
}
}
msg = "RSXIO memory not mapped!"sv;
break;
}
case CELL_GCM_CONTEXT_DMA_REPORT_LOCATION_LOCAL:
{
if (offset < sizeof(RsxReports::report) /*&& (offset % 0x10) == 0*/)
{
return render->label_addr + ::offset32(&RsxReports::report) + offset;
}
msg = "Local RSX REPORT offset out of range!"sv;
break;
}
case CELL_GCM_CONTEXT_DMA_REPORT_LOCATION_MAIN:
{
if (const u32 ea = offset < 0x1000000 ? render->iomap_table.get_addr(0x0e000000 + offset) : -1; ea != umax)
{
if (!size_to_check || vm::check_addr(ea, 0, size_to_check))
{
return ea;
}
}
msg = "RSXIO REPORT memory not mapped!"sv;
break;
}
// They are handled elsewhere for targeted methods, so it's unexpected for them to be passed here
case CELL_GCM_CONTEXT_DMA_TO_MEMORY_GET_NOTIFY0:
case CELL_GCM_CONTEXT_DMA_TO_MEMORY_GET_NOTIFY1:
case CELL_GCM_CONTEXT_DMA_TO_MEMORY_GET_NOTIFY2:
case CELL_GCM_CONTEXT_DMA_TO_MEMORY_GET_NOTIFY3:
case CELL_GCM_CONTEXT_DMA_TO_MEMORY_GET_NOTIFY4:
case CELL_GCM_CONTEXT_DMA_TO_MEMORY_GET_NOTIFY5:
case CELL_GCM_CONTEXT_DMA_TO_MEMORY_GET_NOTIFY6:
case CELL_GCM_CONTEXT_DMA_TO_MEMORY_GET_NOTIFY7:
msg = "CELL_GCM_CONTEXT_DMA_TO_MEMORY_GET_NOTIFYx"sv; break;
case CELL_GCM_CONTEXT_DMA_NOTIFY_MAIN_0:
case CELL_GCM_CONTEXT_DMA_NOTIFY_MAIN_1:
case CELL_GCM_CONTEXT_DMA_NOTIFY_MAIN_2:
case CELL_GCM_CONTEXT_DMA_NOTIFY_MAIN_3:
case CELL_GCM_CONTEXT_DMA_NOTIFY_MAIN_4:
case CELL_GCM_CONTEXT_DMA_NOTIFY_MAIN_5:
case CELL_GCM_CONTEXT_DMA_NOTIFY_MAIN_6:
case CELL_GCM_CONTEXT_DMA_NOTIFY_MAIN_7:
msg = "CELL_GCM_CONTEXT_DMA_NOTIFY_MAIN_x"sv; break;
case CELL_GCM_CONTEXT_DMA_SEMAPHORE_RW:
case CELL_GCM_CONTEXT_DMA_SEMAPHORE_R:
{
if (offset < sizeof(RsxReports::semaphore) /*&& (offset % 0x10) == 0*/)
{
return render->label_addr + offset;
}
msg = "DMA SEMAPHORE offset out of range!"sv;
break;
}
case CELL_GCM_CONTEXT_DMA_DEVICE_RW:
case CELL_GCM_CONTEXT_DMA_DEVICE_R:
{
if (offset < 0x100000 /*&& (offset % 0x10) == 0*/)
{
return render->device_addr + offset;
}
// TODO: What happens here? It could wrap around or access other segments of rsx internal memory etc
// Or can simply throw access violation error
msg = "DMA DEVICE offset out of range!"sv;
break;
}
default:
{
msg = "Invalid location!"sv;
break;
}
}
if (size_to_check)
{
// Allow failure if specified size
// This is to allow accurate recovery for failures
rsx_log.warning("rsx::get_address(offset=0x%x, location=0x%x, size=0x%x): %s%s", offset, location, size_to_check, msg, src_loc);
return 0;
}
fmt::throw_exception("rsx::get_address(offset=0x%x, location=0x%x): %s%s", offset, location, msg, src_loc);
}
extern void set_rsx_yield_flag() noexcept
{
if (auto rsx = get_current_renderer())
{
if (g_cfg.core.allow_rsx_cpu_preempt)
{
rsx->state += cpu_flag::yield;
}
}
}
extern void set_native_ui_flip()
{
if (auto rsxthr = rsx::get_current_renderer())
{
rsxthr->async_flip_requested |= rsx::thread::flip_request::native_ui;
}
}
std::pair<u32, u32> interleaved_range_info::calculate_required_range(u32 first, u32 count)
{
if (vertex_range.second)
{
// Cached result
return vertex_range;
}
if (single_vertex)
{
return { 0, 1 };
}
const u32 max_index = (first + count) - 1;
u32 _max_index = 0;
u32 _min_index = first;
u32 frequencies[rsx::limits::vertex_count];
u32 freq_count = rsx::method_registers.current_draw_clause.command == rsx::draw_command::indexed ? 0 : u32{umax};
u32 max_result_by_division = 0; // Guaranteed maximum
for (const auto &attrib : locations)
{
if (attrib.frequency <= 1) [[likely]]
{
freq_count = umax;
_max_index = max_index;
}
else
{
if (attrib.modulo)
{
if (max_index >= attrib.frequency)
{
// Actually uses the modulo operator
_min_index = 0;
_max_index = std::max<u32>(_max_index, attrib.frequency - 1);
if (max_result_by_division < _max_index)
{
if (freq_count != umax)
{
if (std::find(frequencies, frequencies + freq_count, attrib.frequency) == frequencies + freq_count)
{
frequencies[freq_count++] = attrib.frequency;
}
}
}
}
else
{
// Same as having no modulo
_max_index = max_index;
freq_count = umax;
}
}
else
{
// Division operator
_min_index = std::min(_min_index, first / attrib.frequency);
_max_index = std::max<u32>(_max_index, utils::aligned_div(max_index, attrib.frequency));
if (freq_count > 0 && freq_count != umax)
{
const u32 max = utils::aligned_div(max_index, attrib.frequency);
max_result_by_division = std::max<u32>(max_result_by_division, max);
// Discard lower frequencies because it has been proven that there are indices higher than them
const usz discard_cnt = frequencies + freq_count - std::remove_if(frequencies, frequencies + freq_count, [&max_result_by_division](u32 freq)
{
return freq <= max_result_by_division;
});
freq_count -= static_cast<u32>(discard_cnt);
}
}
}
}
while (freq_count > 0 && freq_count != umax)
{
const rsx::index_array_type index_type = rsx::method_registers.current_draw_clause.is_immediate_draw ?
rsx::index_array_type::u32 :
rsx::method_registers.index_type();
const u32 index_size = index_type == rsx::index_array_type::u32 ? 4 : 2;
const auto render = rsx::get_current_renderer();
// If we can access a bit a more memory than required - do it
// The alternative would be re-iterating again over all of them
if (get_location(real_offset_address) == CELL_GCM_LOCATION_LOCAL)
{
if (utils::add_saturate<u32>(real_offset_address - rsx::constants::local_mem_base, (_max_index + 1) * attribute_stride) <= render->local_mem_size)
{
break;
}
}
else if (real_offset_address % 0x100000 + (_max_index + 1) * attribute_stride <= 0x100000)//(vm::check_addr(real_offset_address, vm::page_readable, (_max_index + 1) * attribute_stride))
{
break;
}
_max_index = 0;
const auto re_evaluate = [&] <typename T> (const std::byte* ptr, T)
{
const u64 restart = rsx::method_registers.restart_index_enabled() ? rsx::method_registers.restart_index() : u64{umax};
for (u32 _index = first; _index < first + count; _index++)
{
const auto value = read_from_ptr_unsafe<be_t<T>>(ptr, _index * sizeof(T));
if (value == restart)
{
continue;
}
for (u32 freq_it = 0; freq_it < freq_count; freq_it++)
{
const auto res = value % frequencies[freq_it];
if (res > _max_index)
{
_max_index = res;
}
}
}
};
const auto element_push_buffer = render->draw_processor()->element_push_buffer();
if (index_size == 4)
{
if (!element_push_buffer.empty()) [[unlikely]]
{
// Indices provided via immediate mode
re_evaluate(reinterpret_cast<const std::byte*>(element_push_buffer.data()), u32{});
}
else
{
const u32 address = (0 - index_size) & get_address(rsx::method_registers.index_array_address(), rsx::method_registers.index_array_location());
re_evaluate(vm::get_super_ptr<std::byte>(address), u32{});
}
}
else
{
if (!element_push_buffer.empty()) [[unlikely]]
{
// Indices provided via immediate mode
re_evaluate(reinterpret_cast<const std::byte*>(element_push_buffer.data()), u16{});
}
else
{
const u32 address = (0 - index_size) & get_address(rsx::method_registers.index_array_address(), rsx::method_registers.index_array_location());
re_evaluate(vm::get_super_ptr<std::byte>(address), u16{});
}
}
break;
}
ensure(_max_index >= _min_index);
vertex_range = { _min_index, (_max_index - _min_index) + 1 };
return vertex_range;
}
u32 get_vertex_type_size_on_host(vertex_base_type type, u32 size)
{
switch (type)
{
case vertex_base_type::s1:
case vertex_base_type::s32k:
switch (size)
{
case 1:
case 2:
case 4:
return sizeof(u16) * size;
case 3:
return sizeof(u16) * 4;
default:
break;
}
fmt::throw_exception("Wrong vector size");
case vertex_base_type::f: return sizeof(f32) * size;
case vertex_base_type::sf:
switch (size)
{
case 1:
case 2:
case 4:
return sizeof(f16) * size;
case 3:
return sizeof(f16) * 4;
default:
break;
}
fmt::throw_exception("Wrong vector size");
case vertex_base_type::ub:
switch (size)
{
case 1:
case 2:
case 4:
return sizeof(u8) * size;
case 3:
return sizeof(u8) * 4;
default:
break;
}
fmt::throw_exception("Wrong vector size");
case vertex_base_type::cmp: return 4;
case vertex_base_type::ub256: ensure(size == 4); return sizeof(u8) * 4;
default:
break;
}
fmt::throw_exception("Bad vertex data type (%d)!", static_cast<u8>(type));
}
void tiled_region::write(const void *src, u32 width, u32 height, u32 pitch)
{
if (!tile)
{
memcpy(ptr, src, height * pitch);
return;
}
const u32 offset_x = base % tile->pitch;
const u32 offset_y = base / tile->pitch;
switch (tile->comp)
{
case CELL_GCM_COMPMODE_C32_2X1:
case CELL_GCM_COMPMODE_DISABLED:
for (u32 y = 0; y < height; ++y)
{
memcpy(ptr + (offset_y + y) * tile->pitch + offset_x, static_cast<const u8*>(src) + pitch * y, pitch);
}
break;
/*
case CELL_GCM_COMPMODE_C32_2X1:
for (u32 y = 0; y < height; ++y)
{
const u32* src_line = reinterpret_cast<const u32*>(static_cast<const u8*>(src) + pitch * y);
u32* dst_line = reinterpret_cast<u32*>(ptr + (offset_y + y) * tile->pitch + offset_x);
for (u32 x = 0; x < width; ++x)
{
u32 value = src_line[x];
dst_line[x * 2 + 0] = value;
dst_line[x * 2 + 1] = value;
}
}
break;
*/
case CELL_GCM_COMPMODE_C32_2X2:
for (u32 y = 0; y < height; ++y)
{
const u32* src_line = reinterpret_cast<const u32*>(static_cast<const u8*>(src) + pitch * y);
u32* line_0 = reinterpret_cast<u32*>(ptr + (offset_y + y * 2 + 0) * tile->pitch + offset_x);
u32* line_1 = reinterpret_cast<u32*>(ptr + (offset_y + y * 2 + 1) * tile->pitch + offset_x);
for (u32 x = 0; x < width; ++x)
{
u32 value = src_line[x];
line_0[x * 2 + 0] = value;
line_0[x * 2 + 1] = value;
line_1[x * 2 + 0] = value;
line_1[x * 2 + 1] = value;
}
}
break;
default:
fmt::throw_exception("Bad tile compression type (%d)!", tile->comp);
}
}
void tiled_region::read(void *dst, u32 width, u32 height, u32 pitch)
{
if (!tile)
{
memcpy(dst, ptr, height * pitch);
return;
}
u32 offset_x = base % tile->pitch;
u32 offset_y = base / tile->pitch;
switch (tile->comp)
{
case CELL_GCM_COMPMODE_C32_2X1:
case CELL_GCM_COMPMODE_DISABLED:
for (u32 y = 0; y < height; ++y)
{
memcpy(static_cast<u8*>(dst) + pitch * y, ptr + (offset_y + y) * tile->pitch + offset_x, pitch);
}
break;
/*
case CELL_GCM_COMPMODE_C32_2X1:
for (u32 y = 0; y < height; ++y)
{
const u32* src_line = reinterpret_cast<const u32*>(ptr + (offset_y + y) * tile->pitch + offset_x);
u32* dst_line = reinterpret_cast<u32*>(static_cast<u8*>(dst) + pitch * y);
for (u32 x = 0; x < width; ++x)
{
dst_line[x] = src_line[x * 2 + 0];
}
}
break;
*/
case CELL_GCM_COMPMODE_C32_2X2:
for (u32 y = 0; y < height; ++y)
{
const u32* src_line = reinterpret_cast<const u32*>(ptr + (offset_y + y * 2 + 0) * tile->pitch + offset_x);
u32* dst_line = reinterpret_cast<u32*>(static_cast<u8*>(dst) + pitch * y);
for (u32 x = 0; x < width; ++x)
{
dst_line[x] = src_line[x * 2 + 0];
}
}
break;
default:
fmt::throw_exception("Bad tile compression type (%d)!", tile->comp);
}
}
thread::~thread()
{
g_access_violation_handler = nullptr;
}
void thread::save(utils::serial& ar)
{
[[maybe_unused]] const s32 version = GET_OR_USE_SERIALIZATION_VERSION(ar.is_writing(), rsx);
ar(rsx::method_registers);
for (auto& v : m_draw_processor.m_vertex_push_buffers)
{
ar(v.attr, v.size, v.type, v.vertex_count, v.dword_count, v.data);
}
ar(m_draw_processor.m_element_push_buffer, fifo_ret_addr, saved_fifo_ret, zcull_surface_active, m_surface_info, m_depth_surface_info, m_framebuffer_layout);
ar(dma_address, iomap_table, restore_point, tiles, zculls, display_buffers, display_buffers_count, current_display_buffer);
ar(enable_second_vhandler, requested_vsync);
ar(device_addr, label_addr, main_mem_size, local_mem_size, rsx_event_port, driver_info);
ar(in_begin_end);
ar(display_buffers, display_buffers_count, current_display_buffer);
ar(unsent_gcm_events, rsx::method_registers.current_draw_clause);
if (ar.is_writing() || version >= 2)
{
ar(vblank_count);
b8 flip_pending{};
if (ar.is_writing())
{
flip_pending = !!(async_flip_requested & flip_request::emu_requested);
}
ar(flip_pending);
if (flip_pending)
{
ar(vblank_at_flip);
ar(async_flip_buffer);
if (!ar.is_writing())
{
async_flip_requested |= flip_request::emu_requested;
flip_notification_count = 1;
}
}
}
if (ar.is_writing())
{
if (fifo_ctrl && state & cpu_flag::again)
{
ar(fifo_ctrl->get_remaining_args_count() + 1);
ar(fifo_ctrl->last_cmd());
}
else
{
ar(u32{0});
}
}
else if (u32 count{ar})
{
restore_fifo_count = count;
ar(restore_fifo_cmd);
}
}
thread::thread(utils::serial* _ar)
: cpu_thread(0x5555'5555)
{
g_access_violation_handler = [this](u32 address, bool is_writing)
{
return on_access_violation(address, is_writing);
};
m_textures_dirty.fill(true);
m_vertex_textures_dirty.fill(true);
m_graphics_state |= pipeline_state::all_dirty;
g_user_asked_for_frame_capture = false;
// TODO: Proper context management in the driver
s_ctx.rsxthr = this;
m_ctx = &s_ctx;
m_draw_processor.init(m_ctx);
if (g_cfg.misc.use_native_interface && (g_cfg.video.renderer == video_renderer::opengl || g_cfg.video.renderer == video_renderer::vulkan))
{
m_overlay_manager = g_fxo->init<rsx::overlays::display_manager>(0);
if (g_cfg.misc.play_music_during_boot)
{
if (const std::string audio_path = Emu.GetSfoDir(true) + "/SND0.AT3"; fs::is_file(audio_path))
{
m_overlay_manager->start_audio(audio_path);
}
}
}
if (!_ar)
{
add_remove_flags({}, cpu_flag::stop); // TODO: Remove workaround
return;
}
add_remove_flags(cpu_flag::suspend, cpu_flag::stop);
serialized = true;
save(*_ar);
if (dma_address)
{
ctrl = vm::_ptr<RsxDmaControl>(dma_address);
rsx_thread_running = true;
}
if (g_cfg.savestate.start_paused)
{
// Allow to render a whole frame within this emulation session so there won't be missing graphics
m_pause_after_x_flips = 2;
}
}
avconf::avconf(utils::serial& ar)
{
save(ar);
}
void avconf::save(utils::serial& ar)
{
[[maybe_unused]] const s32 version = GET_OR_USE_SERIALIZATION_VERSION(ar.is_writing(), rsx);
if (!ar.is_writing() && version < 3)
{
// Be compatible with previous bitwise serialization
ar(std::span<u8>(reinterpret_cast<u8*>(this), ::offset32(&avconf::scan_mode)));
ar.pos += utils::align<usz>(::offset32(&avconf::scan_mode), alignof(avconf)) - ::offset32(&avconf::scan_mode);
return;
}
ar(stereo_enabled, format, aspect, resolution_id, scanline_pitch, gamma, resolution_x, resolution_y, state, scan_mode);
}
void thread::capture_frame(const std::string& name) const
{
frame_trace_data::draw_state draw_state{};
draw_state.programs = get_programs();
draw_state.name = name;
frame_debug.draw_calls.emplace_back(std::move(draw_state));
}
void thread::begin()
{
// Backend-independent draw prologue. The read_barrier below is a hard sync on the
// software conditional render path, so this is not always the trivial flag work it
// looks like, and unscoped it was billed to draw_setup along with everything else.
RSX_PROF_SCOPE(draw_prologue);
if (cond_render_ctrl.hw_cond_active)
{
if (!cond_render_ctrl.eval_pending())
{
// End conditional rendering if still active
end_conditional_rendering();
}
// If hw cond render is enabled and evalutation is still pending, do nothing
}
else if (cond_render_ctrl.eval_pending())
{
// Evaluate conditional rendering test or enable hw cond render until results are available
if (backend_config.supports_hw_conditional_render)
{
// In this mode, it is possible to skip the cond render while the backend is still processing data.
// The backend guarantees that any draw calls emitted during this time will NOT generate any ROP writes
ensure(!cond_render_ctrl.hw_cond_active);
// Pending evaluation, use hardware test
begin_conditional_rendering(cond_render_ctrl.eval_sources);
}
else
{
// NOTE: eval_sources list is reversed with newest query first
zcull_ctrl->read_barrier(this, cond_render_ctrl.eval_address, cond_render_ctrl.eval_sources.front());
ensure(!cond_render_ctrl.eval_pending());
}
}
if (!backend_config.supports_normalized_barycentrics)
{
// Check for mode change between rasterized polys vs lines and points
// Luckily this almost never happens in real games
const auto current_mode = rsx::method_registers.current_draw_clause.classify_mode();
if (current_mode != m_current_draw_mode)
{
m_graphics_state |= (rsx::vertex_program_state_dirty | rsx::fragment_program_state_dirty);
m_current_draw_mode = current_mode;
}
}
in_begin_end = true;
}
void thread::end()
{
// Backend-independent draw epilogue: clause cleanup, push buffer teardown and the
// ZCULL draw hook, all of which scale with draw count rather than with frame count.
RSX_PROF_SCOPE(draw_epilogue);
if (capture_current_frame)
{
capture::capture_draw_memory(this);
}
in_begin_end = false;
m_frame_stats.draw_calls++;
// Counted here rather than at the backend call sites so every early return in
// VKGSRender::end is included; all of them route through this function.
if (rsx::prof::enabled()) [[unlikely]]
{
rsx::prof::g_draw_calls++;
if (rsx::prof::g_pass_ordinal < rsx::prof::pass_slot_count)
{
rsx::prof::g_pass_draws[rsx::prof::g_pass_ordinal]++;
rsx::prof::g_pass_vertices[rsx::prof::g_pass_ordinal] +=
method_registers.current_draw_clause.get_elements_count();
rsx::prof::g_pass_vp_words[rsx::prof::g_pass_ordinal] +=
::size32(current_vertex_program.data);
rsx::prof::g_pass_fp_words[rsx::prof::g_pass_ordinal] +=
current_fragment_program.ucode_length;
}
}
method_registers.current_draw_clause.post_execute_cleanup(m_ctx);
m_graphics_state |= rsx::pipeline_state::framebuffer_reads_dirty;
m_eng_interrupt_mask |= rsx::backend_interrupt;
ROP_sync_timestamp = rsx::get_shared_tag();
m_draw_processor.clear_push_buffers();
zcull_ctrl->on_draw();
if (capture_current_frame)
{
const u32 element_count = rsx::method_registers.current_draw_clause.get_elements_count();
capture_frame(fmt::format("Draw %s %d", rsx::method_registers.current_draw_clause.primitive, element_count));
}
}
void thread::execute_nop_draw()
{
method_registers.current_draw_clause.begin();
do
{
method_registers.current_draw_clause.execute_pipeline_dependencies(m_ctx);
}
while (method_registers.current_draw_clause.next());
}
void thread::cpu_task()
{
while (Emu.IsReady())
{
Emu.WaitReady();
}
do
{
on_task();
state -= cpu_flag::ret;
}
while (!is_stopped());
on_exit();
}
void thread::cpu_wait(bs_t<cpu_flag> old)
{
if (external_interrupt_lock)
{
wait_pause();
}
if ((state & (cpu_flag::dbg_global_pause + cpu_flag::exit)) == cpu_flag::dbg_global_pause)
{
// Wait 16ms during emulation pause. This reduces cpu load while still giving us the chance to render overlays.
do_local_task(rsx::FIFO::state::paused);
thread_ctrl::wait_on(state, old, 16000);
}
else
{
on_semaphore_acquire_wait();
std::this_thread::yield();
}
}
void thread::post_vblank_event(u64 post_event_time)
{
vblank_count++;
if (isHLE)
{
if (auto ptr = vblank_handler)
{
intr_thread->cmd_list
({
{ ppu_cmd::set_args, 1 }, u64{1},
{ ppu_cmd::lle_call, ptr },
{ ppu_cmd::sleep, 0 }
});
intr_thread->cmd_notify.store(1);
intr_thread->cmd_notify.notify_one();
}
}
else
{
sys_rsx_context_attribute(0x55555555, 0xFED, 1, get_guest_system_time(post_event_time), 0, 0);
}
}
namespace nv4097
{
void set_render_mode(context* rsx, u32, u32 arg);
}
void thread::on_task()
{
g_tls_log_prefix = []
{
const auto rsx = get_current_renderer();
return fmt::format("RSX [0x%07x]", rsx->ctrl ? +rsx->ctrl->get : 0);
};
if (!serialized) method_registers.init();
rsx::overlays::reset_performance_overlay();
rsx::overlays::reset_debug_overlay();
if (!is_initialized)
{
g_fxo->get<rsx::dma_manager>().init();
on_init_thread();
if (in_begin_end)
{
// on_init_thread should have prepared the backend resources
// Run draw call warmup again if the savestate happened mid-draw
ensure(serialized);
begin();
}
}
is_initialized = true;
is_initialized.notify_all();
if (!zcull_ctrl)
{
// Backend did not provide an implementation, provide NULL object
zcull_ctrl = std::make_unique<::rsx::reports::ZCULL_control>();
}
check_zcull_status(false);
nv4097::set_render_mode(m_ctx, 0, method_registers.registers[NV4097_SET_RENDER_ENABLE]);
performance_counters.state = FIFO::state::empty;
const u64 event_flags = unsent_gcm_events.exchange(0);
if (Emu.IsStarting())
{
Emu.CallFromMainThread([]
{
Emu.RunPPU();
});
}
// Wait for startup (TODO)
while (!rsx_thread_running || Emu.IsPausedOrReady())
{
// Execute backend-local tasks first
do_local_task(performance_counters.state);
// Update sub-units
zcull_ctrl->update(this);
if (is_stopped())
{
return;
}
thread_ctrl::wait_for(1000);
}
performance_counters.state = FIFO::state::running;
fifo_ctrl = std::make_unique<::rsx::FIFO::FIFO_control>(this);
fifo_ctrl->set_get(ctrl->get);
resolution_scaling_config =
{
.scale_percent = static_cast<u16>(g_cfg.video.resolution_scale_percent),
.min_scalable_dimension = static_cast<u16>(g_cfg.video.min_scalable_dimension),
};
last_guest_flip_timestamp = get_system_time() - 1000000;
vblank_count = 0;
if (restore_fifo_count)
{
fifo_ctrl->restore_state(restore_fifo_cmd, restore_fifo_count);
}
if (!send_event(0, event_flags, 0))
{
return;
}
g_fxo->get<vblank_thread>().set_thread(std::shared_ptr<named_thread<std::function<void()>>>(new named_thread<std::function<void()>>("VBlank Thread"sv, [this]() -> void
{
#ifdef __linux__
constexpr u32 host_min_quantum = 10;
#else
constexpr u32 host_min_quantum = 500;
#endif
u64 start_time = get_system_time();
u64 vblank_rate = g_cfg.video.vblank_rate;
u64 vblank_period = 1'000'000 + u64{g_cfg.video.vblank_ntsc.get()} * 1000;
u64 local_vblank_count = 0;
// TODO: exit condition
u64 last_heartbeat = 0;
u64 iterations = 0;
while (!is_stopped() && !unsent_gcm_events && thread_ctrl::state() != thread_state::aborting)
{
// Get current time
const u64 current = get_system_time();
// Heartbeat. This thread is the only source of the interrupt gcm waits on, so
// when it stops the guest hangs after gcm init with everything else looking
// idle -- and it said nothing either way. Distinguishes "still looping" from
// "blocked inside post_vblank_event" from "left the loop", which need
// different fixes and are indistinguishable from outside.
if (current - last_heartbeat >= 5'000'000)
{
last_heartbeat = current;
rsx_log.notice("VBlank: alive, iterations=%u, vblank_count=%u", iterations, local_vblank_count);
}
iterations++;
// Calculate the time at which we need to send a new VBLANK signal
const u64 post_event_time = start_time + (local_vblank_count + 1) * vblank_period / vblank_rate;
// Calculate time remaining to that time (0 if we passed it)
const u64 wait_for = current >= post_event_time ? 0 : post_event_time - current;
#ifdef __linux__
const u64 wait_sleep = wait_for;
#else
// Substract host operating system min sleep quantom to get sleep time
const u64 wait_sleep = wait_for - u64{wait_for >= host_min_quantum} * host_min_quantum;
#endif
if (!wait_for)
{
{
local_vblank_count++;
if (local_vblank_count == vblank_rate)
{
// Advance start_time to the moment of the current VBLANK
// Which is the last VBLANK event in this period
// This is in order for multiplication by ratio above to use only small numbers
start_time += vblank_period;
local_vblank_count = 0;
// We have a rare chance to update settings without losing precision whenever local_vblank_count is 0
vblank_rate = g_cfg.video.vblank_rate;
vblank_period = 1'000'000 + u64{g_cfg.video.vblank_ntsc.get()} * 1000;
}
post_vblank_event(post_event_time);
}
}
else if (wait_sleep)
{
thread_ctrl::wait_for(wait_sleep);
}
else if (wait_for >= host_min_quantum / 3 * 2)
{
std::this_thread::yield();
}
if (Emu.IsPaused())
{
// Save the difference before pause
start_time = get_system_time() - start_time;
while (Emu.IsPaused() && !is_stopped())
{
thread_ctrl::wait_for(5'000);
}
// Restore difference
start_time = get_system_time() - start_time;
}
}
// Which condition ended it. unsent_gcm_events is the savestate hand-off, so seeing
// it here outside a savestate means a live send failed and took the vblank source
// down with it -- permanently, since nothing restarts this thread.
rsx_log.error("VBlank: loop exited after %u iterations (stopped=%d, unsent_gcm_events=0x%x, aborting=%d)",
iterations, is_stopped() ? 1 : 0, unsent_gcm_events.load(),
thread_ctrl::state() == thread_state::aborting ? 1 : 0);
})));
struct join_vblank
{
~join_vblank() noexcept
{
g_fxo->get<vblank_thread>() = thread_state::finished;
}
} join_vblank_obj{};
// Raise priority above other threads
thread_ctrl::scoped_priority high_prio(+1);
if (g_cfg.core.thread_scheduler != thread_scheduler_mode::os)
{
thread_ctrl::set_thread_affinity_mask(thread_ctrl::get_affinity_mask(thread_class::rsx));
}
if (auto manager = g_fxo->try_get<rsx::overlays::display_manager>())
{
manager->stop_audio();
}
// Scoped around the whole loop, not around run_FIFO.
//
// run_FIFO executes a single FIFO command, so a scope inside it ran tens of thousands
// of times per frame, each reading the counter-timer twice. On ARM that is an mrs
// cntvct_el0: expensive and partially serialising. run_FIFO's share of RSX thread
// samples went from about 3% uninstrumented to 35% with the scope inside it, so the
// bucket had become mostly a measurement of itself.
//
// Entered once here instead. Attribution is exclusive, so every nested scope still
// carves out its own time and returns here afterwards, leaving this bucket meaning
// "RSX loop time nothing more specific claimed", which is what was wanted from it,
// at no per-command cost.
RSX_PROF_SCOPE(fifo_decode);
while (!test_stopped())
{
// Wait for external pause events
if (external_interrupt_lock)
{
wait_pause();
if (!rsx_thread_running)
{
return;
}
}
// Note a possible rollback address
if (sync_point_request && !in_begin_end)
{
restore_point = ctrl->get;
saved_fifo_ret = fifo_ret_addr;
sync_point_request.release(false);
}
// Update sub-units every 64 cycles. The local handler is invoked for other functions externally on-demand anyway.
// This avoids expensive calls to check timestamps which involves reading some values from TLS storage on windows.
// If something is going on in the backend that requires an update, set the interrupt bit explicitly.
if (rsx::prof::enabled()) [[unlikely]] rsx::prof::g_fifo_commands++;
if ((m_cycles_counter++ & 63) == 0 || m_eng_interrupt_mask)
{
// Execute backend-local tasks first
//
// These two run once per 64 FIFO commands rather than per command, so
// scoping them costs a counter read every 64th iteration instead of the
// per-command cost that made an earlier attempt measure mostly itself.
//
// Split out because fifo_decode holds 38ms of a 54ms frame in Arkham City
// and nothing else accounts for it: the whole draw path is under 5ms and
// page protection turned out to be 0.09ms. ZCULL is a live suspect here in
// particular, since this title trips "Reports area at location
// CELL_GCM_LOCATION_MAIN was accessed, ZCULL optimizations will be
// disabled" and then runs the unoptimised path for the entire session.
{
RSX_PROF_SCOPE(local_task);
do_local_task(performance_counters.state);
}
// Update other sub-units
{
RSX_PROF_SCOPE(zcull);
zcull_ctrl->update(this);
}
if (m_host_dma_ctrl)
{
m_host_dma_ctrl->update();
}
}
// Execute FIFO queue
run_FIFO();
}
}
void thread::on_exit()
{
if (zcull_ctrl)
{
zcull_ctrl->sync(this);
}
// Deregister violation handler
g_access_violation_handler = nullptr;
// Clear any pending flush requests to release threads
std::this_thread::sleep_for(10ms);
do_local_task(rsx::FIFO::state::lock_wait);
g_fxo->get<rsx::dma_manager>().join();
g_fxo->get<vblank_thread>() = thread_state::finished;
state += cpu_flag::exit;
}
u64 thread::timestamp()
{
const u64 freq = sys_time_get_timebase_frequency();
auto get_time_ns = [freq]()
{
const u64 t = get_timebased_time();
return (t / freq * 1'000'000'000 + t % freq * 1'000'000'000 / freq);
};
const u64 t = get_time_ns();
if (t != timestamp_ctrl)
{
timestamp_ctrl = t;
timestamp_subvalue = 0;
return t;
}
// Check if we passed the limit of what fixed increments is legal for
// Wait for the next time value reported if we passed the limit
if ((1'000'000'000 / freq) - timestamp_subvalue <= 2)
{
u64 now = get_time_ns();
for (; t == now; now = get_time_ns())
{
utils::pause();
}
timestamp_ctrl = now;
timestamp_subvalue = 0;
return now;
}
timestamp_subvalue += 2;
return t + timestamp_subvalue;
}
// Frame-stall notice. See check_frame_stall.
static atomic_t<u64> g_last_frame_time{0};
static atomic_t<bool> g_frame_stall_reported{false};
// How many guest-thread dumps this stall has produced. See check_frame_stall.
static atomic_t<u32> g_frame_stall_dumps{0};
// Defined below; check_frame_stall is what decides a stall has happened.
static void dump_guest_threads_stalled();
// Say when the picture has stopped, instead of leaving the last frame standing.
//
// A guest that stops progressing presents nothing further, so whatever was last drawn stays
// on screen indefinitely. When that frame happens to contain the boot progress bar, it reads
// as "stuck compiling at 1s remaining" -- and it looked exactly the same across five
// unrelated faults, sending every report of them to the wrong place. Nothing contradicts it
// either: the emulator has not crashed, so there is no error to be found.
//
// Only fires while nothing is legitimately in progress. A shader or PPU compile presents no
// frames for minutes at a time, and it holds a progress dialog that says as much, so an
// empty progress text is what separates "working, quietly" from "stopped".
// What the RSX thread is doing while it fails to present.
//
// The profiler can say the thread is in 'Local task' and that it has been in it for 0.00s,
// which together mean it is not stuck in there at all -- do_local_task has no loop and
// returns immediately. The FIFO loop is calling it over and over, and Borderlands 2 has the
// RSX thread at ~113% of a core doing that while no frame lands for 45s+. Which FIFO state
// it is called with separates the cases and is recorded nowhere else: 'empty' means the
// guest is not submitting (a guest-side stall the RSX only reflects), 'spinning' means the
// puller is jumping to itself waiting on a semaphore, 'lock_wait' means it is parked on a
// lock acquire. Counters only, dumped from the existing 5s stall report -- logging per call
// would be thousands of lines a second, which on Android is itself a stall.
static constexpr u32 s_ltask_states = 6;
static constexpr u32 s_ltask_branches = 4;
static u64 g_ltask_state_calls[s_ltask_states]{};
static u64 g_ltask_state_prev[s_ltask_states]{};
static u64 g_ltask_branch[s_ltask_branches]{};
static u64 g_ltask_branch_prev[s_ltask_branches]{};
static u64 g_ltask_last_report = 0;
enum ltask_branch : u32
{
ltask_emu_flip = 0,
ltask_invalidate = 1,
ltask_dma_control = 2,
ltask_pipe_flush = 3,
};
static void check_frame_stall()
{
const u64 now = get_system_time();
// Something is reporting progress, or no frame has ever landed yet: not a stall.
if (g_progr_text || !g_last_frame_time)
{
g_last_frame_time = now;
g_frame_stall_reported = false;
g_frame_stall_dumps = 0;
return;
}
const u64 since = now - g_last_frame_time;
if (since < 30'000'000)
{
return;
}
if (!g_frame_stall_reported)
{
g_frame_stall_reported = true;
rsx_log.error("No frame presented in %us with nothing in progress: the game has stopped.",
since / 1'000'000);
// Draw it, rather than logging into a file nobody has when they file the report. The
// native UI flip is what gets it on screen at all -- the guest is not flipping, which is
// the whole point.
rsx::overlays::queue_message(
std::string("Game has stopped responding - it is no longer drawing frames"),
10'000'000);
set_native_ui_flip();
}
// Say WHERE the guest is parked, not merely that it is.
//
// dump_guest_threads_stalled() already existed and is thorough, but the only thing that
// called it was the RSX profiler's poll_stall() -- which returns false immediately unless
// the profiler is switched on, and testers do not switch it on. So every freeze report
// arrived with this line and nothing behind it: the emulator detected the hang, told the
// user, and recorded none of what it could see. Confirmed against a Kane & Lynch capture
// where the guest stopped at 0:04:15 and this fired at 0:04:45 with no dump.
//
// Twice, ~15s apart, because one sample cannot distinguish a thread spinning from one
// making very slow progress -- a cia that has not moved between two samples is itself the
// finding. Twice and no more: this runs to hundreds of lines per thread, and log volume
// alone is enough to stall the emulator on Android.
if (const u32 taken = g_frame_stall_dumps; taken < 2 && since >= 30'000'000 + u64{taken} * 15'000'000)
{
g_frame_stall_dumps = taken + 1;
dump_guest_threads_stalled();
}
}
// Say where every guest thread is parked once frames have stopped arriving.
//
// A hang with the RSX idle is a guest-side wait, and nothing named the thread or the place.
// The syscall stats report sys_timer_usleep without saying who called it, /proc shows a
// thread that never started as indistinguishable from one that is blocked, and the RSX
// profiler only covers this side of the boundary. Name, state, PC and the function each
// PPU is in separate all of those.
//
// idm::unlocked deliberately: this runs on the RSX thread, and taking the id lock here to
// diagnose a hang would add exactly the kind of dependency being diagnosed. A torn read of
// a diagnostic line costs nothing.
static void dump_guest_threads_stalled()
{
std::string out;
idm::select<named_thread<ppu_thread>>([&out](u32 id, ppu_thread& ppu)
{
const auto func = ppu.current_function ? ppu.current_function : ppu.last_function;
fmt::append(out, "\n PPU 0x%07x '%s': state=%s cia=0x%08x %s func='%s'",
id, *ppu.ppu_tname.load(), ppu.state.load(), ppu.cia,
ppu.current_function ? "in" : "last", func ? func : "");
// Who called the wait, not just where it is parked.
//
// Borderlands 2's main_thread blocks in sys_event_queue_receive at cia=0x01b85e5c,
// and an instrumented x86 build blocks at the SAME libsre address -- but on a
// different queue: x86 gets 0x8d00c200 (CompPatch group, spup 17, which the SPUs
// signal constantly) and ARM gets 0x8d021700 (PhysWISE group, spup 20, which nothing
// ever signals). Same code, different queue handle, so the handle was chosen further
// up. cia cannot say by whom; the call stack can, and it names the cellSpurs* entry
// point that picked the instance.
if (const std::string trace = ppu.dump_callstack(); !trace.empty())
{
fmt::append(out, "%s", trace);
}
}, idm::unlocked);
rsx_log.error("Guest PPU threads while no frame has completed:%s", out);
// Once per session, follow the summary with everything each PPU can say about itself:
// registers, the guest call stack, and -- when "PPU Calling History" is enabled -- the
// last guest calls and HLE/LV2 calls it made.
//
// The summary above repeats every few seconds deliberately, because a cia that does not
// move between samples is itself the finding. This part must NOT repeat: it runs to
// hundreds of lines per thread, and log volume alone is enough to stall the emulator on
// Android, which is a failure mode we have already shipped once.
//
// The case it exists for is a thread reported as 'state=00[]' with a static cia and a
// core pegged at 100%: that is a guest busy-loop, and the one-line summary cannot say
// what the loop is waiting on. The call stack names the caller chain that entered it,
// and the registers hold whatever it keeps re-testing -- which together identify the
// loop in the executable. Reported against Saint Seiya: The Sanctuary (BLES01421,
// issue #25), which parks its main thread at cia=0x000bc7d0 forever, right after
// _sys_lwmutex_create and before it creates a single thread or submits a single frame.
//
// Wait for PPU/SPU compilation to finish before spending the one shot.
//
// No frame is presented while modules are compiling either, so the very first stall of
// every session is the compile itself -- six minutes of it on a cold cache. Dumping there
// burns the one-shot on a thread that has not run a single guest instruction: every GPR
// reads zero and the call stack is empty, which is exactly what happened the first time
// this shipped. Neither "has a frame ever been presented" nor a plain time threshold
// separates the two cases, because the hang being chased also never presents a frame and
// also lasts forever. Outstanding progress work does.
//
// dump_callstack_list validates the stack pointer and every frame with vm::check_addr and
// gives up rather than walking garbage, so this is safe against a thread that is running
// and modifying its own stack underneath us. Torn values are acceptable here for the same
// reason the summary takes them: an approximate answer now beats an exact one never.
static atomic_t<bool> s_dumped_detail{false};
const bool compiling = g_progr_ptotal.load() != g_progr_pdone.load();
if (!compiling && !s_dumped_detail.exchange(true))
{
std::string detail;
// The guest instructions around the stuck cia -- the loop itself.
//
// Registers and a call stack say where the thread is and what it holds, but not what
// the code DOES, and from the outside a two-instruction compare-and-branch-to-self is
// indistinguishable from a long computation that simply has not finished. Printing the
// window settles it, and cpu_disasm_mode::dump emits address, opcode bytes and mnemonic
// per line, so the branch target is readable straight out of the log and can be matched
// against the guest binary without the debugger UI, which Android does not build.
//
// A fixed window rather than the whole function because function bounds are not known
// on this side, and every address is checked first: cia is read from a thread that is
// still running and can be stale or outright garbage, and faulting inside the diagnostic
// that explains a hang would be the worst possible trade.
PPUDisAsm dis_asm(cpu_disasm_mode::dump, vm::g_sudo_addr);
idm::select<named_thread<ppu_thread>>([&detail, &dis_asm](u32 id, ppu_thread& ppu)
{
fmt::append(detail, "\n=== PPU 0x%07x '%s' ===\n", id, *ppu.ppu_tname.load());
ppu.dump_all(detail);
const u32 pc = ppu.cia;
const u32 from = pc >= 0x20 ? pc - 0x20 : 0;
// A wide window for a thread that is spinning, a narrow one for a thread that is
// merely parked in a syscall.
//
// Under the recompiler cia is only written at block boundaries, so on a spinning
// thread it names the ENTRY of the function that never returned, not the loop
// inside it -- and a handful of instructions from the entry is just the prologue.
// Reading the whole body is the point: the question being answered is which PPU
// instructions the function uses, because the ARM64 backend only diverges from the
// portable path on a few of them (VCFUX, VMAXFP, VMINFP, VPERM) and seeing one of
// those in a function that hangs is what turns a guess into a candidate.
//
// Threads blocked in sys_* are not the suspects and there can be a dozen of them,
// so they keep the short window. Their cia is in liblv2 anyway.
const bool spinning = ppu.state.none_of(cpu_flag::wait);
const u32 span = spinning ? 0x600 : 0x40;
// What the registers POINT AT, for a thread spinning in guest code.
//
// dump_all prints 8 bytes behind each GPR, which is enough to recognise a
// pointer and not enough to read the structure it points to. Assassin's Creed
// needs byte 0x74 of the SPURS job chain -- the workloadId the guest tests
// before deciding a chain is usable -- and that is 0x74 bytes past a value
// sitting in r5. Every fact this hunt has turned on so far came from a struct
// field just out of reach of the 8-byte preview.
//
// Spinning threads only, deduplicated, capped: a dozen parked threads each
// dragging 0x80 bytes per register would bury the dump that explains the hang.
if (spinning)
{
std::vector<u32> seen;
for (u32 i = 3; i < 32 && seen.size() < 6; i++)
{
const u32 ptr = static_cast<u32>(ppu.gpr[i]);
// Aligned, mapped, and not already printed. The alignment test is what
// keeps counters and small integers out of it.
if (!ptr || (ptr & 0xf) || !vm::check_addr(ptr, vm::page_readable, 0x80))
{
continue;
}
if (std::find(seen.begin(), seen.end(), ptr) != seen.end())
{
continue;
}
seen.push_back(ptr);
fmt::append(detail, "\n[r%u] 0x%08x:", i, ptr);
for (u32 off = 0; off < 0x80; off += 16)
{
fmt::append(detail, "\n +0x%02x ", off);
for (u32 b = 0; b < 16; b++)
{
fmt::append(detail, "%02x ", vm::read8(ptr + off + b));
}
}
}
detail += "\n";
}
fmt::append(detail, "\nCode around cia=0x%08x (%s):\n", pc,
spinning ? "spinning, wide window" : "waiting, short window");
for (u32 addr = from; addr <= from + span; addr += 4)
{
if (!vm::check_addr(addr))
{
continue;
}
dis_asm.disasm(addr);
// Mark the instruction the thread is actually parked on, so the loop can be
// read off without counting lines.
detail += (addr == pc ? " >>" : " ");
detail += dis_asm.last_opcode;
}
// The callers, which is where a control-flow divergence actually lives.
//
// cia only says where a thread is parked, and for anything blocked in an lv2 wait
// that is an address inside liblv2 or libsre -- the same address on every host, so
// it can never show a divergence. Borderlands 2 proves the point: ARM and an
// instrumented x86 build both park main_thread at 0x01b85e5c in libsre, both reach
// it through the cellSpursEventFlagWait import thunk at 0x011e785c, and both share
// an identical outer stack down to 0x001f59a4 -- then ARM calls straight into the
// wait via 0x000c6958 while x86 descends six further frames via 0x000c4b8c. The
// branch that picks between those two paths is the bug, and it is only visible by
// disassembling around the RETURN ADDRESSES, not around cia.
//
// Four frames: enough to cross the import thunk and reach guest code on either
// path, short enough that a dozen parked threads do not bury the log.
const auto frames = ppu.dump_callstack_list();
for (u32 i = 0; i < 4 && i < frames.size(); i++)
{
const u32 ret = frames[i].first;
if (!ret || !vm::check_addr(ret))
{
continue;
}
// The call is the instruction BEFORE the return address, so start behind it.
const u32 caller_from = ret >= 0x18 ? ret - 0x18 : 0;
fmt::append(detail, "\nCaller frame %u, code around return 0x%08x:\n", i, ret);
for (u32 addr = caller_from; addr <= ret + 0x8; addr += 4)
{
if (!vm::check_addr(addr))
{
continue;
}
dis_asm.disasm(addr);
detail += (addr == ret ? " >>" : " ");
detail += dis_asm.last_opcode;
}
}
}, idm::unlocked);
rsx_log.error("Stalled guest thread detail (reported once per session):%s", detail);
}
// The SPU half. A hang where every PPU is asleep and the SPUs are burning user time is
// the SPUs spinning in guest code, and nothing said WHICH code: /proc gives a tick count,
// a CPU profile gives a JIT address that resolves to nothing. The PC plus the block hash
// name the guest block, which is the only thing that identifies the loop.
std::string spus;
// One detailed kernel dump per report, not per SPU -- see the note at the use site.
bool spu_detail_done = false;
// WHICH SPU gets that dump, decided before walking them.
//
// It used to be whichever came first with raddr == spurs_addr -- a kernel waiting on its
// own control block, i.e. an IDLE one -- on the assumption that every kernel parks at the
// same pc so any of them would do. Tales of Xillia is the counter-example: one graphics
// kernel executed a guest HALT and sits stopped at pc=0x00f00 while the other four idle
// normally at 0x011a8. The old rule picked an idle one, so the only thread in the process
// that had anything to say printed no registers at all, and the capture could name the
// failing assertion but none of the values it tested.
//
// A stopped or halted SPU wins outright. Nothing else in a SPURS group stops on its own,
// so if one has, it is the reason the group never joined and the rest are just waiting on
// it.
u32 spu_detail_id = 0;
bool spu_detail_pinned = false;
idm::select<named_thread<spu_thread>>([&](u32 id, spu_thread& spu)
{
const auto st = spu.state.load();
if (!spu_detail_pinned && !st.none_of(cpu_flag::stop + cpu_flag::dbg_pause + cpu_flag::exit))
{
spu_detail_id = id;
spu_detail_pinned = true;
}
}, idm::unlocked);
idm::select<named_thread<spu_thread>>([&spus, &spu_detail_done, spu_detail_id, spu_detail_pinned](u32 id, spu_thread& spu)
{
const auto func = spu.current_func;
// Event state as well as position, because position alone cannot tell a lost wakeup
// from an idle wait. Both look like a thread parked in 'MFC Events read'.
//
// events is what has fired, mask is what this SPU asked to be woken for, waiting is
// whether it is parked in the channel read. events & mask non-zero while waiting is
// set means the wakeup it needs has ALREADY happened and was not delivered, which is
// a lost notification and our bug. events & mask == 0 means it is genuinely idle and
// whoever should signal it never did, which is a bug on the other side.
//
// Sonic Unleashed used to deadlock at the SEGA logo with all six SPURS kernels parked
// here, in a different arrangement on different boots, so it is a race in this
// handshake. That title has run correctly for several releases now; Borderlands 2
// reaches the same state after its logo, with one kernel still running and the rest
// parked, and which kernel keeps running changes between boots.
const auto ev = spu.ch_events.load();
fmt::append(spus, "\n SPU 0x%07x '%s': state=%s pc=0x%05x block=0x%016llx func='%s' events=0x%04x mask=0x%08x waiting=%u pending=0x%04x",
spu.lv2_id, *spu.spu_tname.load(), spu.state.load(), spu.pc,
static_cast<u64>(spu.block_hash), func ? func : "",
static_cast<u32>(ev.events), static_cast<u32>(ev.mask), static_cast<u32>(ev.waiting),
static_cast<u32>(ev.events) & static_cast<u32>(ev.mask));
// MFC state too, because an SPU can be stuck with no flag set at all.
//
// Sonic Unleashed hangs with RsdxPrimaryCellSpursKernel4 frozen at pc=0x07350 across
// every sample of a session, while its neighbours move through the kernel normally.
// state is 00, so it is not parked in a channel read -- it is spinning in guest code,
// which an SPU does while waiting for a transfer to land in local store. If a queued
// MFC command never retires, that spin never ends.
//
// mfc_size is the queue depth: non-zero and unchanging on the frozen thread means a
// transfer went in and never came out. tag_mask/stall are what it would be waiting on.
// interp_fallback distinguishes "the fallback never engaged" from "it engaged and the
// thread is stuck anyway". Sonic Unleashed marks block 0x07350 uncompilable and stays
// frozen on that pc; if this reads 1 there, the interpreter is looping too and the
// block is not miscompiled -- the SPU is genuinely waiting on something.
// intr/srr0 last, because they are what is left. Sonic Unleashed's stuck kernel holds
// an unmasked pending LR event, is not blocked on a channel or a transfer, and does
// not advance its pc even under the interpreter -- which is a branch-to-self idle
// loop. SPURS kernels leave that loop on an SPU interrupt, so either interrupts are
// disabled while an event is pending, or they are enabled and never delivered.
fmt::append(spus, " mfc_q=%u tag_mask=0x%08x stall_mask=0x%08x interp_fb=%u intr_en=%u srr0=0x%05x",
spu.mfc_size, spu.ch_tag_mask, spu.ch_stall_mask, spu.interp_fallback ? 1u : 0u,
spu.interrupts_enabled ? 1u : 0u, spu.srr0);
// Conditional-store activity. block_counter already says whether an SPU is executing
// guest code, but a thread livelocked retrying PUTLLC and a thread that is genuinely
// idle both report zero blocks per second and are otherwise indistinguishable here.
// 'suppressed' is the SPURS heuristic in do_putllc choosing not to wake the waiters:
// a store that succeeds and notifies nobody is the one way this hang can be nobody's
// fault locally and still never end.
static constexpr const char* where_names[]{ "?", "loop_top", "check_state", "stop_signal", "gateway_enter", "gateway_exit" };
const u32 where = spu.dbg_where < std::size(where_names) ? spu.dbg_where : 0u;
// raddr is the 128-byte line this SPU currently holds a reservation on, and spurs_addr
// is the control block the kernel schedules through. Borderlands 2 hangs with every
// PhysWISE kernel alive in guest code and ~39% of their conditional stores failing,
// which is contention rather than a stall -- but contention on WHAT is the thing none
// of the counters can say. If they are all hammering one line, that line is the bug's
// address; if they are spread out, this is not the contention story it looks like.
fmt::append(spus, " putllc={calls=%u fails=%u notify=%u suppressed=%u} blocks=%u loops=%u where=%s raddr=0x%x spurs=0x%x rtime=%u",
spu.putllc_calls, spu.putllc_fails, spu.putllc_notify, spu.putllc_suppressed,
spu.block_counter, spu.dbg_loops, where_names[where],
spu.raddr, spu.spurs_addr, spu.rtime);
fmt::append(spus, " events_sent=%u", spu.events_sent);
// Is the line this SPU is parked on being written at all?
//
// The wait loop wakes on either of two things: the reservation counter moving, or
// the 128 bytes themselves changing under an unchanged counter. So an SPU that
// stays asleep is not evidence of a lost notification -- it is evidence that
// NOTHING TOUCHED THE LINE. Assassin's Creed parks all six SPURS kernels on the
// control block forever while the PPU spins adding urgent commands to the job
// chain, which lives ~4KB away on a different line, so nothing the PPU does there
// can wake them. Printing the counter and the first bytes each time this dump runs
// turns "asleep" into "asleep and the line is provably static", which is a
// different bug with a different fix.
if (spu.raddr && vm::check_addr(spu.raddr))
{
const u64 res_now = vm::reservation_acquire(spu.raddr);
fmt::append(spus, " res_now=%u res_moved=%u", res_now, res_now != spu.rtime ? 1 : 0);
fmt::append(spus, " line=");
for (u32 i = 0; i < 16; i++)
{
fmt::append(spus, "%02x", vm::read8(spu.raddr + i));
}
}
// What the kernel actually looked at before deciding to sleep.
//
// Everything above says the SPU is parked and that the line it waits on is static.
// Neither says WHY it chose to wait, and that decision is guest code: the SPURS
// kernel reads the control block into local store, tests it, and either takes work
// or arms an LR wait. The registers hold the values it tested and the local store
// around pc holds the test itself, which is the same pairing the PPU half of this
// dump has always printed and the SPU half never did.
//
// One SPU only. All six kernels park at the same pc running the same code, so six
// copies is six times the log for no extra fact. Picked by raddr == spurs_addr,
// which is what identifies a SPURS kernel waiting on its own control block.
const bool is_detail_target = spu_detail_pinned
? id == spu_detail_id
: spu.raddr && spu.raddr == spu.spurs_addr;
if (!spu_detail_done && is_detail_target)
{
spu_detail_done = true;
fmt::append(spus, "\n --- kernel detail (%s) ---",
spu_detail_pinned ? "the stopped SPU -- the rest are waiting on it" : "one SPU; the others are idle at the same pc");
// All 128, not the first 16. Xillia's assertion is a validity check over
// r12/r16/r17/r19/r33/r34, so the sixteen that used to be printed did not include
// a single operand of the test the log had just finished disassembling.
for (u32 i = 0; i < 128; i++)
{
const auto& r = spu.gpr[i];
fmt::append(spus, "\n r%-3u = %08x %08x %08x %08x", i,
r._u32[3], r._u32[2], r._u32[1], r._u32[0]);
}
// Local store around pc. Narrow: this is a decision, not a function body, and
// the branch that armed the wait is within a few instructions of the channel read.
SPUDisAsm spu_dis(cpu_disasm_mode::dump, spu.ls);
const u32 from = spu.pc >= 0x40 ? spu.pc - 0x40 : 0;
fmt::append(spus, "\n Local store around pc=0x%05x:\n", spu.pc);
for (u32 addr = from; addr <= from + 0x90 && addr < SPU_LS_SIZE; addr += 4)
{
spu_dis.disasm(addr);
spus += (addr == spu.pc ? " >>" : " ");
spus += spu_dis.last_opcode;
}
}
// What the polling worker is actually looking at, once per process.
//
// Everything measured so far says this SPU is not miscomputing -- spu_alu is
// byte-exact and spu_fpu matches x86 on every line -- and that the event it should
// send is never sent rather than lost, since no queue holds a pending event with a
// waiter attached. So it is spinning on data it does not like, and these are the
// three views of that data:
//
// rdata the 128-byte snapshot GETLLAR took, which is what the guest compares
// live the same line in main memory right now
// ls local store 0x100..0x180, the SPURS control mirror -- RPCS3's own
// do_putllc heuristic reads the idle bitmap at 0x100 + 0x73, so this is
// the region it treats as the kernel's control area
//
// rdata differing from live while the guest keeps re-reserving means it is polling a
// stale view; identical means the data is fine and the loop's exit condition is not
// about this line at all. Once per process: three 128-byte blocks per SPU is a lot of
// hex, and a per-dump version of this is exactly the mistake the code window made.
static atomic_t<bool> s_ls_dumped{false};
if (spu.raddr && !s_ls_dumped && !s_ls_dumped.exchange(true))
{
const auto hex128 = [](const void* src)
{
const auto p = static_cast<const u8*>(src);
std::string out;
for (u32 i = 0; i < 128; i += 16)
{
fmt::append(out, "\n +0x%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x",
i, p[i + 0], p[i + 1], p[i + 2], p[i + 3], p[i + 4], p[i + 5], p[i + 6], p[i + 7],
p[i + 8], p[i + 9], p[i + 10], p[i + 11], p[i + 12], p[i + 13], p[i + 14], p[i + 15]);
}
return out;
};
fmt::append(spus, "\n reservation snapshot (rdata) for raddr=0x%x:%s", spu.raddr, hex128(spu.rdata));
if (vm::check_addr(spu.raddr, vm::page_readable, 128))
{
fmt::append(spus, "\n live main memory at 0x%x:%s", spu.raddr, hex128(vm::base(spu.raddr)));
fmt::append(spus, "\n -> snapshot %s live memory",
std::memcmp(spu.rdata, vm::base(spu.raddr), 128) == 0 ? "MATCHES" : "DIFFERS FROM");
}
if (spu.ls)
{
fmt::append(spus, "\n local store 0x100 (SPURS control mirror):%s", hex128(spu.ls + 0x100));
}
}
if (spu.mfc_size)
{
const auto& cmd = spu.mfc_queue[0];
fmt::append(spus, " head={cmd=0x%02x tag=%u lsa=0x%05x eal=0x%08x size=0x%x}",
+cmd.cmd, +cmd.tag, +cmd.lsa, +cmd.eal, +cmd.size);
}
// The loop itself, for an SPU that is RUNNING rather than parked.
//
// The state line above says where it is and what it holds; it cannot say what the code
// does, and that is the remaining question whenever every other guest thread is
// legitimately asleep. If pending == 0 on the sleepers (see above) then the fault is on
// the signalling side, which is whichever SPU is still running -- so print what that one
// is executing, and nothing for the parked ones, whose pc is just the kernel's idle
// loop and is identical on all of them.
//
// Borderlands 2 hangs after its logo with exactly one SPURS kernel at pc=0x25da8 and
// the rest at 0x011a8, and WHICH kernel spins changes between boots. Local store is a
// flat 256KiB buffer, so the window only needs bounding against SPU_LS_SIZE -- there is
// no address to validate the way the PPU side has to.
if (spu.state.load().none_of(cpu_flag::wait) && spu.ls)
{
// Registers first: the loop is a comparison, and which values it compares is the
// half the disassembly cannot supply.
spu.dump_all(spus);
// ONE window per process, because this dump repeats for as long as the stall
// lasts and the window is a whole SPU function. Unguarded it cost ~2400 lines
// every few seconds: measured 538 lines/sec over 31 dumps, ~74k of the 114k
// lines in a single Borderlands 2 capture, with a 690 MiB log left behind by the
// run before it. Log volume that heavy is itself a stall on Android, so this was
// slowing down the very hang it was meant to describe -- and it buried the state
// lines that actually answered the question. The code only needs printing once;
// the state line above still prints on every dump and is what shows change.
static atomic_t<bool> s_window_done{false};
if (s_window_done.exchange(true))
{
return;
}
const u32 pc = spu.pc;
const u32 from = pc >= 0x40 ? pc - 0x40 : 0;
// Big enough for a whole SPU function, which took two captures to establish.
//
// The recompiler only writes pc at block boundaries, so a thread that never leaves
// its block reports the block's ENTRY, not the loop. 0x120 bytes got a prologue --
// twenty-odd stqd of callee-saved registers. 0x600 got 385 instructions with no
// back-edge and no channel op in them, which reads like "not looping here" and is
// simply the window ending first: the log says this block is 2401 instructions
// ("Building function 0x25da8... (size 2401)"), i.e. 0x2584 bytes. Anything shorter
// than the function cannot answer "where does it loop", so the window covers one.
const u32 to = std::min<u32>(from + 0x2600, SPU_LS_SIZE - 4);
SPUDisAsm dis_asm(cpu_disasm_mode::dump, spu.ls);
fmt::append(spus, "\n running -- code from pc=0x%05x (block entry; loop is inside):\n", pc);
for (u32 addr = from; addr <= to; addr += 4)
{
dis_asm.disasm(addr);
spus += (addr == pc ? " >>" : " ");
spus += dis_asm.last_opcode;
}
}
}, idm::unlocked);
if (!spus.empty())
{
rsx_log.error("Guest SPU threads at the same moment:%s", spus);
}
// The groups the threads above belong to.
//
// Borderlands 2 does not fail at a boundary: the game runs thousands of
// start/join cycles on a group and loses one of them. The syscall histogram showed one
// PPU thread stopped at 2532 start/join pairs while its siblings passed 8000 and an x86
// host passed 7426, so the interesting number is which cycle this is and what the group
// believes about it. stop_count is that cycle counter, and 'running' is the count the
// join is waiting on -- if it stays above zero while every thread is spinning in the
// SPURS scheduler then the group never delivered the exit request, and if it reaches
// zero while a join still waits then the accounting is ours to fix. Neither is visible
// from the thread states alone.
std::string groups;
idm::select<lv2_spu_group>([&groups](u32 id, lv2_spu_group& group)
{
fmt::append(groups, "\n group 0x%07x '%s': state=%s running=%u/%u stop_count=%u join_state=0x%x exit_status=0x%x",
id, group.name, group.run_state.load(), group.running.load(), group.max_num,
group.stop_count.load(), group.join_state.load(), group.exit_status.load());
}, idm::unlocked);
if (!groups.empty())
{
rsx_log.error("Guest SPU thread groups:%s", groups);
}
// The queues those threads are blocked on -- the half never looked at.
//
// Every dump so far has said which threads are asleep and which SPU is spinning, and none
// has said whether the thing they are waiting for was ever posted. Borderlands 2 leaves
// main_thread in sys_event_queue_receive on 0x8d021700 while one SPURS worker polls for
// work at ~720k block-entries a second, and the two possibilities need opposite fixes:
//
// pending > 0 with a waiter attached -> the event WAS delivered and the wakeup was
// lost, which is ours, in lv2.
// pending == 0 with a waiter attached -> nothing was ever posted, so the worker is not
// signalling and the fault is upstream of the queue.
//
// Read without taking the queue mutex, deliberately: this runs on the RSX thread during a
// hang, and blocking on a lock held by a thread being diagnosed is how a diagnostic turns
// into a second deadlock. A torn size costs nothing here.
std::string queues;
idm::select<lv2_obj, lv2_event_queue>([&queues](u32 id, lv2_event_queue& eq)
{
const usz pending = eq.events.size();
const auto ppu_waiter = eq.pq;
const auto spu_waiter = eq.sq;
// Only the interesting ones: something queued, or somebody asleep on it.
if (!pending && !ppu_waiter && !spu_waiter)
{
return;
}
fmt::append(queues, "\n queue 0x%07x: type=%u size=%u pending=%u ppu_waiter=0x%x spu_waiter=0x%x",
id, eq.type, eq.size, static_cast<u32>(pending),
ppu_waiter ? ppu_waiter->id : 0u,
spu_waiter ? spu_waiter->id : 0u);
});
if (!queues.empty())
{
rsx_log.error("Guest event queues with a waiter or a pending event:%s", queues);
}
}
void thread::do_local_task(FIFO::state state)
{
if (const u32 state_idx = static_cast<u32>(state); state_idx < s_ltask_states)
{
g_ltask_state_calls[state_idx]++;
}
// Arm and poll from here as well as on_frame_end. Both of those run only once a frame
// has completed, so a boot that hangs before presenting left the profiler switched off
// and silent -- and that is the case where what the RSX thread is looping in is the
// whole question. Both calls return immediately once armed and are rate-limited.
prof::set_enabled(g_cfg.video.rsx_profiler.get());
// Always on, unlike the profiler-gated reports below: the whole point is that this
// reaches a user who has not enabled anything.
check_frame_stall();
// Both halves on the same condition. Every hang chased so far has been the guest
// waiting while the RSX idles, and only the RSX half was ever visible.
if (prof::poll_stall()) [[unlikely]]
{
dump_guest_threads_stalled();
const u64 report_now = get_system_time();
const u64 span = g_ltask_last_report ? report_now - g_ltask_last_report : 0;
g_ltask_last_report = report_now;
static constexpr const char* state_names[s_ltask_states]{ "running", "empty", "spinning", "nop", "lock_wait", "paused" };
static constexpr const char* branch_names[s_ltask_branches]{ "emu_flip", "invalidate", "dma_ctrl", "pipe_sync" };
std::string act;
for (u32 i = 0; i < s_ltask_states; i++)
{
const u64 delta = g_ltask_state_calls[i] - g_ltask_state_prev[i];
g_ltask_state_prev[i] = g_ltask_state_calls[i];
if (delta)
{
fmt::append(act, " %s=%u", state_names[i], delta);
}
}
for (u32 i = 0; i < s_ltask_branches; i++)
{
const u64 delta = g_ltask_branch[i] - g_ltask_branch_prev[i];
g_ltask_branch_prev[i] = g_ltask_branch[i];
if (delta)
{
fmt::append(act, " %s=%u", branch_names[i], delta);
}
}
// get/put straight out of the guest DMA control block rather than our mirror of it.
// get == put with state 'empty' is the guest not submitting; get != put while no
// frame lands is the RSX failing to drain what it already has, and those want
// opposite fixes.
u32 fifo_get = 0, fifo_put = 0, fifo_ref = 0;
if (dma_address && vm::check_addr(dma_address))
{
const auto ctrl = vm::_ptr<RsxDmaControl>(dma_address);
fifo_put = ctrl->put;
fifo_get = ctrl->get;
fifo_ref = ctrl->ref;
}
rsx_log.error("RSX local task over %.1fs:%s | fifo get=0x%x put=0x%x ref=0x%x internal_get=0x%x last_cmd=0x%08x args_left=%u | dma_ctrl_pending=%u pipe_flush_pending=%u in_begin_end=%u",
span / 1'000'000.,
act.empty() ? " (no calls)" : act.c_str(),
fifo_get, fifo_put, fifo_ref,
fifo_ctrl ? fifo_ctrl->get_pos() : 0u,
fifo_ctrl ? fifo_ctrl->last_cmd() : 0u,
fifo_ctrl ? fifo_ctrl->get_remaining_args_count() : 0u,
(m_eng_interrupt_mask & rsx::dma_control_interrupt) ? 1 : 0,
(m_eng_interrupt_mask & rsx::pipe_flush_interrupt) ? 1 : 0,
in_begin_end ? 1 : 0);
}
m_eng_interrupt_mask.clear(rsx::backend_interrupt);
if (async_flip_requested & flip_request::emu_requested)
{
g_ltask_branch[ltask_emu_flip]++;
// NOTE: This has to be executed immediately
// Delaying this operation can cause desync due to the delay in firing the flip event
handle_emu_flip(async_flip_buffer);
}
if (state != FIFO::state::lock_wait)
{
if (!in_begin_end && atomic_storage<u32>::load(m_invalidated_memory_range.end) != 0)
{
std::lock_guard lock(m_mtx_task);
if (m_invalidated_memory_range.valid())
{
g_ltask_branch[ltask_invalidate]++;
handle_invalidated_memory_range();
}
}
if (m_eng_interrupt_mask & rsx::dma_control_interrupt && !is_stopped())
{
g_ltask_branch[ltask_dma_control]++;
if (const u64 get_put = new_get_put.exchange(u64{umax});
get_put != umax)
{
vm::_ptr<atomic_be_t<u64>>(dma_address + ::offset32(&RsxDmaControl::put))->release(get_put);
fifo_ctrl->set_get(static_cast<u32>(get_put));
fifo_ctrl->abort();
fifo_ret_addr = RSX_CALL_STACK_EMPTY;
last_known_code_start = static_cast<u32>(get_put);
sync_point_request.release(true);
}
m_eng_interrupt_mask.clear(rsx::dma_control_interrupt);
}
}
if (m_eng_interrupt_mask & rsx::pipe_flush_interrupt)
{
g_ltask_branch[ltask_pipe_flush]++;
sync();
}
if (is_stopped())
{
std::lock_guard lock(m_mtx_task);
m_invalidated_memory_range = utils::address_range32::start_end(0x2 << 28, constants::local_mem_base + local_mem_size - 1);
handle_invalidated_memory_range();
}
}
std::array<u32, 4> thread::get_color_surface_addresses() const
{
u32 offset_color[] =
{
rsx::method_registers.surface_offset(0),
rsx::method_registers.surface_offset(1),
rsx::method_registers.surface_offset(2),
rsx::method_registers.surface_offset(3),
};
u32 context_dma_color[] =
{
rsx::method_registers.surface_dma(0),
rsx::method_registers.surface_dma(1),
rsx::method_registers.surface_dma(2),
rsx::method_registers.surface_dma(3),
};
return
{
rsx::get_address(offset_color[0], context_dma_color[0]),
rsx::get_address(offset_color[1], context_dma_color[1]),
rsx::get_address(offset_color[2], context_dma_color[2]),
rsx::get_address(offset_color[3], context_dma_color[3]),
};
}
u32 thread::get_zeta_surface_address() const
{
u32 m_context_dma_z = rsx::method_registers.surface_z_dma();
u32 offset_zeta = rsx::method_registers.surface_z_offset();
return rsx::get_address(offset_zeta, m_context_dma_z);
}
void thread::get_framebuffer_layout(rsx::framebuffer_creation_context context, framebuffer_layout &layout)
{
layout = {};
layout.ignore_change = true;
layout.width = rsx::method_registers.surface_clip_width();
layout.height = rsx::method_registers.surface_clip_height();
m_graphics_state.clear(rsx::rtt_config_contested | rsx::rtt_config_valid);
m_current_framebuffer_context = context;
if (layout.width == 0 || layout.height == 0)
{
rsx_log.trace("Invalid framebuffer setup, w=%d, h=%d", layout.width, layout.height);
return;
}
//const u16 clip_x = rsx::method_registers.surface_clip_origin_x();
//const u16 clip_y = rsx::method_registers.surface_clip_origin_y();
layout.color_addresses = get_color_surface_addresses();
layout.zeta_address = get_zeta_surface_address();
layout.zeta_pitch = rsx::method_registers.surface_z_pitch();
layout.color_pitch =
{
rsx::method_registers.surface_pitch(0),
rsx::method_registers.surface_pitch(1),
rsx::method_registers.surface_pitch(2),
rsx::method_registers.surface_pitch(3),
};
layout.color_format = rsx::method_registers.surface_color();
layout.depth_format = rsx::method_registers.surface_depth_fmt();
layout.target = rsx::method_registers.surface_color_target();
const auto mrt_buffers = rsx::utility::get_rtt_indexes(layout.target);
const auto aa_mode = rsx::method_registers.surface_antialias();
const u32 aa_factor_u = (aa_mode == rsx::surface_antialiasing::center_1_sample) ? 1 : 2;
const u32 aa_factor_v = (aa_mode == rsx::surface_antialiasing::center_1_sample || aa_mode == rsx::surface_antialiasing::diagonal_centered_2_samples) ? 1 : 2;
const u8 sample_count = get_format_sample_count(aa_mode);
const auto depth_texel_size = get_format_block_size_in_bytes(layout.depth_format) * aa_factor_u;
const auto color_texel_size = get_format_block_size_in_bytes(layout.color_format) * aa_factor_u;
const bool stencil_test_enabled = is_depth_stencil_format(layout.depth_format) && rsx::method_registers.stencil_test_enabled();
const bool depth_test_enabled = rsx::method_registers.depth_test_enabled();
// Check write masks
layout.zeta_write_enabled = (depth_test_enabled && rsx::method_registers.depth_write_enabled());
if (!layout.zeta_write_enabled && stencil_test_enabled)
{
// Check if stencil data is modified
auto mask = rsx::method_registers.stencil_mask();
bool active_write_op = (rsx::method_registers.stencil_op_zpass() != rsx::stencil_op::keep ||
rsx::method_registers.stencil_op_fail() != rsx::stencil_op::keep ||
rsx::method_registers.stencil_op_zfail() != rsx::stencil_op::keep);
if ((!mask || !active_write_op) && rsx::method_registers.two_sided_stencil_test_enabled())
{
mask |= rsx::method_registers.back_stencil_mask();
active_write_op |= (rsx::method_registers.back_stencil_op_zpass() != rsx::stencil_op::keep ||
rsx::method_registers.back_stencil_op_fail() != rsx::stencil_op::keep ||
rsx::method_registers.back_stencil_op_zfail() != rsx::stencil_op::keep);
}
layout.zeta_write_enabled = (mask && active_write_op);
}
// NOTE: surface_target_a is index 1 but is not MRT since only one surface is active
bool color_write_enabled = false;
for (uint i = 0; i < mrt_buffers.size(); ++i)
{
if (rsx::method_registers.color_write_enabled(i))
{
const auto real_index = mrt_buffers[i];
layout.color_write_enabled[real_index] = true;
color_write_enabled = true;
}
}
bool depth_buffer_unused = false, color_buffer_unused = false;
switch (context)
{
case rsx::framebuffer_creation_context::context_clear_all:
break;
case rsx::framebuffer_creation_context::context_clear_depth:
color_buffer_unused = true;
break;
case rsx::framebuffer_creation_context::context_clear_color:
depth_buffer_unused = true;
break;
case rsx::framebuffer_creation_context::context_draw:
// NOTE: As with all other hw, depth/stencil writes involve the corresponding depth/stencil test, i.e No test = No write
// NOTE: Depth test is not really using the memory if its set to always or never
// TODO: Perform similar checks for stencil test
if (!stencil_test_enabled)
{
if (!depth_test_enabled)
{
depth_buffer_unused = true;
}
else if (!rsx::method_registers.depth_write_enabled())
{
// Depth test is enabled but depth write is disabled
switch (rsx::method_registers.depth_func())
{
default:
break;
case rsx::comparison_function::never:
case rsx::comparison_function::always:
// No access to depth buffer memory
depth_buffer_unused = true;
break;
}
}
if (depth_buffer_unused) [[unlikely]]
{
// Check if depth bounds is active. Depth bounds test does NOT need depth test to be enabled to access the Z buffer
// Bind Z buffer in read mode for bounds check in this case
if (rsx::method_registers.depth_bounds_test_enabled() &&
(rsx::method_registers.depth_bounds_min() > 0.f || rsx::method_registers.depth_bounds_max() < 1.f))
{
depth_buffer_unused = false;
}
}
}
color_buffer_unused = !color_write_enabled || layout.target == rsx::surface_target::none;
if (color_buffer_unused || depth_buffer_unused)
{
m_graphics_state.set(rsx::rtt_config_contested);
}
break;
default:
fmt::throw_exception("Unknown framebuffer context 0x%x", static_cast<u32>(context));
}
// Swizzled render does tight packing of bytes
bool packed_render = false;
u32 minimum_color_pitch = 64u;
u32 minimum_zeta_pitch = 64u;
switch (layout.raster_type = rsx::method_registers.surface_type())
{
default:
rsx_log.error("Unknown raster mode 0x%x", static_cast<u32>(layout.raster_type));
[[fallthrough]];
case rsx::surface_raster_type::linear:
break;
case rsx::surface_raster_type::swizzle:
packed_render = true;
break;
}
if (!packed_render)
{
// Well, this is a write operation either way (clearing or drawing)
// We can deduce a minimum pitch for which this operation is guaranteed to require by checking for the lesser of scissor or clip
const u32 write_limit_x = std::min<u32>(layout.width, rsx::method_registers.scissor_origin_x() + rsx::method_registers.scissor_width());
minimum_color_pitch = color_texel_size * write_limit_x;
minimum_zeta_pitch = depth_texel_size * write_limit_x;
// Check for size fit and attempt to correct incorrect inputs.
// BLUS30072 is misconfigured here and renders fine on PS3. The width fails to account for AA being active in that engine.
u16 corrected_width = umax;
std::vector<u32*> pitch_fixups;
if (!depth_buffer_unused)
{
if (layout.zeta_pitch < minimum_zeta_pitch)
{
// Observed in CoD3 where the depth buffer is clearly misconfigured.
if (layout.zeta_pitch > 64)
{
corrected_width = layout.zeta_pitch / depth_texel_size;
layout.zeta_pitch = depth_texel_size;
pitch_fixups.push_back(&layout.zeta_pitch);
}
else
{
rsx_log.warning("Misconfigured surface could not fit a depth buffer. Dropping.");
layout.zeta_address = 0;
}
}
else if (layout.width * depth_texel_size > layout.zeta_pitch)
{
// This is ok, misconfigured raster dimensions, but we're only writing the pitch as determined by the scissor
corrected_width = layout.zeta_pitch / depth_texel_size;
}
}
if (!color_buffer_unused)
{
for (const auto& index : rsx::utility::get_rtt_indexes(layout.target))
{
if (layout.color_pitch[index] < minimum_color_pitch)
{
if (layout.color_pitch[index] > 64)
{
corrected_width = std::min<u16>(corrected_width, layout.color_pitch[index] / color_texel_size);
layout.color_pitch[index] = color_texel_size;
pitch_fixups.push_back(&layout.color_pitch[index]);
}
else
{
rsx_log.warning("Misconfigured surface could not fit color buffer %d. Dropping.", index);
layout.color_addresses[index] = 0;
}
continue;
}
if (layout.width * color_texel_size > layout.color_pitch[index])
{
// This is ok, misconfigured raster dimensions, but we're only writing the pitch as determined by the scissor
corrected_width = std::min<u16>(corrected_width, layout.color_pitch[index] / color_texel_size);
}
}
}
if (corrected_width != umax)
{
layout.width = corrected_width;
for (auto& value : pitch_fixups)
{
*value = *value * layout.width;
}
}
}
if (depth_buffer_unused)
{
layout.zeta_address = 0;
}
else if (packed_render)
{
layout.actual_zeta_pitch = (layout.width * depth_texel_size);
}
else
{
layout.actual_zeta_pitch = layout.zeta_pitch;
}
for (const auto &index : rsx::utility::get_rtt_indexes(layout.target))
{
if (color_buffer_unused)
{
layout.color_addresses[index] = 0;
continue;
}
if (layout.color_pitch[index] < minimum_color_pitch)
{
// Unlike the depth buffer, when given a color target we know it is intended to be rendered to
rsx_log.warning("Framebuffer setup error: Color target failed pitch check, Pitch=[%d, %d, %d, %d] + %d, target=%d, context=%d",
layout.color_pitch[0], layout.color_pitch[1], layout.color_pitch[2], layout.color_pitch[3],
layout.zeta_pitch, static_cast<u32>(layout.target), static_cast<u32>(context));
// Some games (COD4) are buggy and set incorrect width + AA + pitch combo. Force fit in such scenarios.
if (layout.color_pitch[index] > 64)
{
layout.width = layout.color_pitch[index] / color_texel_size;
}
else
{
layout.color_addresses[index] = 0;
continue;
}
}
if (layout.color_addresses[index] == layout.zeta_address)
{
rsx_log.warning("Framebuffer at 0x%X has aliasing color/depth targets, color_index=%d, zeta_pitch = %d, color_pitch=%d, context=%d",
layout.zeta_address, index, layout.zeta_pitch, layout.color_pitch[index], static_cast<u32>(context));
m_graphics_state.set(rsx::rtt_config_contested);
if (g_cfg.video.fb_aliasing_bias == framebuffer_aliasing_bias::prefer_color
&& layout.color_write_enabled[index]
&& !layout.zeta_write_enabled)
{
// Use address for color data
layout.zeta_address = 0;
}
else
{
// TODO: Research clearing both depth AND color
// TODO: If context is creation_draw, deal with possibility of a lost buffer clear
if (depth_test_enabled || stencil_test_enabled || (!layout.color_write_enabled[index] && layout.zeta_write_enabled))
{
// Use address for depth data
layout.color_addresses[index] = 0;
continue;
}
else
{
// Use address for color data
layout.zeta_address = 0;
}
}
}
ensure(layout.color_addresses[index]);
const auto packed_pitch = (layout.width * color_texel_size);
if (packed_render)
{
layout.actual_color_pitch[index] = packed_pitch;
}
else
{
layout.actual_color_pitch[index] = layout.color_pitch[index];
}
m_graphics_state.set(rsx::rtt_config_valid);
}
if (!m_graphics_state.test(rsx::rtt_config_valid) && !layout.zeta_address)
{
rsx_log.warning("Framebuffer setup failed. Draw calls may have been lost");
return;
}
// At least one attachment exists
m_graphics_state.set(rsx::rtt_config_valid);
// Window (raster) offsets
const auto window_offset_x = rsx::method_registers.window_offset_x();
const auto window_offset_y = rsx::method_registers.window_offset_y();
const auto window_clip_width = rsx::method_registers.window_clip_horizontal();
const auto window_clip_height = rsx::method_registers.window_clip_vertical();
if (window_offset_x || window_offset_y)
{
// Window offset is what affects the raster position!
// Tested with Turbo: Super stunt squad that only changes the window offset to declare new framebuffers
// Sampling behavior clearly indicates the addresses are expected to have changed
if (auto clip_type = rsx::method_registers.window_clip_type())
rsx_log.error("Unknown window clip type 0x%X", clip_type);
for (const auto &index : rsx::utility::get_rtt_indexes(layout.target))
{
if (layout.color_addresses[index])
{
const u32 window_offset_bytes = (layout.actual_color_pitch[index] * window_offset_y) + (color_texel_size * window_offset_x);
layout.color_addresses[index] += window_offset_bytes;
}
}
if (layout.zeta_address)
{
layout.zeta_address += (layout.actual_zeta_pitch * window_offset_y) + (depth_texel_size * window_offset_x);
}
}
if ((window_clip_width && window_clip_width < layout.width) ||
(window_clip_height && window_clip_height < layout.height))
{
rsx_log.error("Unexpected window clip dimensions: window_clip=%dx%d, surface_clip=%dx%d",
window_clip_width, window_clip_height, layout.width, layout.height);
}
layout.aa_mode = aa_mode;
layout.aa_factors[0] = aa_factor_u;
layout.aa_factors[1] = aa_factor_v;
// Log this to frame stats
if (layout.target != rsx::surface_target::none)
{
m_frame_stats.framebuffer_stats.add(layout.width, layout.height, aa_mode);
}
// Check if anything has changed
bool really_changed = false;
for (u8 i = 0; i < rsx::limits::color_buffers_count; ++i)
{
if (m_surface_info[i].address != layout.color_addresses[i])
{
really_changed = true;
break;
}
if (layout.color_addresses[i])
{
if (m_surface_info[i].width != layout.width ||
m_surface_info[i].height != layout.height ||
m_surface_info[i].color_format != layout.color_format ||
m_surface_info[i].samples != sample_count)
{
really_changed = true;
break;
}
}
}
if (!really_changed)
{
if (layout.zeta_address == m_depth_surface_info.address &&
layout.depth_format == m_depth_surface_info.depth_format &&
sample_count == m_depth_surface_info.samples)
{
// Same target is reused
return;
}
}
layout.ignore_change = false;
}
void thread::on_framebuffer_options_changed(u32 opt)
{
if (m_graphics_state & rsx::rtt_config_dirty)
{
// Nothing to do
return;
}
auto set_zeta_write_enabled = [&](bool state)
{
if (state == m_framebuffer_layout.zeta_write_enabled)
{
return;
}
if (m_graphics_state & rsx::zeta_address_is_cyclic)
{
m_graphics_state |= rsx::fragment_program_state_dirty;
}
m_framebuffer_layout.zeta_write_enabled = state;
};
auto evaluate_depth_buffer_state = [&]()
{
const bool zeta_write_en = (rsx::method_registers.depth_test_enabled() && rsx::method_registers.depth_write_enabled());
set_zeta_write_enabled(zeta_write_en);
};
auto evaluate_stencil_buffer_state = [&]()
{
if (!m_framebuffer_layout.zeta_write_enabled &&
rsx::method_registers.stencil_test_enabled() &&
is_depth_stencil_format(m_framebuffer_layout.depth_format))
{
// Check if stencil data is modified
auto mask = rsx::method_registers.stencil_mask();
bool active_write_op = (rsx::method_registers.stencil_op_zpass() != rsx::stencil_op::keep ||
rsx::method_registers.stencil_op_fail() != rsx::stencil_op::keep ||
rsx::method_registers.stencil_op_zfail() != rsx::stencil_op::keep);
if ((!mask || !active_write_op) && rsx::method_registers.two_sided_stencil_test_enabled())
{
mask |= rsx::method_registers.back_stencil_mask();
active_write_op |= (rsx::method_registers.back_stencil_op_zpass() != rsx::stencil_op::keep ||
rsx::method_registers.back_stencil_op_fail() != rsx::stencil_op::keep ||
rsx::method_registers.back_stencil_op_zfail() != rsx::stencil_op::keep);
}
set_zeta_write_enabled(mask && active_write_op);
}
};
auto evaluate_color_buffer_state = [&]() -> bool
{
const auto mrt_buffers = rsx::utility::get_rtt_indexes(m_framebuffer_layout.target);
bool any_found = false;
for (uint i = 0; i < mrt_buffers.size(); ++i)
{
if (m_ctx->register_state->color_write_enabled(i))
{
const auto real_index = mrt_buffers[i];
m_framebuffer_layout.color_write_enabled[real_index] = true;
any_found = true;
}
}
on_framebuffer_layout_updated();
return any_found;
};
auto evaluate_depth_buffer_contested = [&]()
{
if (m_framebuffer_layout.zeta_address) [[likely]]
{
// Nothing to do, depth buffer already exists
return false;
}
// Check if depth read/write is enabled
if (m_framebuffer_layout.zeta_write_enabled ||
rsx::method_registers.depth_test_enabled())
{
return true;
}
// Check if stencil read is enabled
if (is_depth_stencil_format(m_framebuffer_layout.depth_format) &&
rsx::method_registers.stencil_test_enabled())
{
return true;
}
return false;
};
switch (opt)
{
case NV4097_SET_DEPTH_TEST_ENABLE:
case NV4097_SET_DEPTH_MASK:
case NV4097_SET_DEPTH_FUNC:
{
evaluate_depth_buffer_state();
if (m_graphics_state.test(rsx::rtt_config_contested) && evaluate_depth_buffer_contested())
{
m_graphics_state.set(rsx::rtt_config_dirty);
}
break;
}
case NV4097_SET_TWO_SIDED_STENCIL_TEST_ENABLE:
case NV4097_SET_STENCIL_TEST_ENABLE:
case NV4097_SET_STENCIL_MASK:
case NV4097_SET_STENCIL_OP_ZPASS:
case NV4097_SET_STENCIL_OP_FAIL:
case NV4097_SET_STENCIL_OP_ZFAIL:
case NV4097_SET_BACK_STENCIL_MASK:
case NV4097_SET_BACK_STENCIL_OP_ZPASS:
case NV4097_SET_BACK_STENCIL_OP_FAIL:
case NV4097_SET_BACK_STENCIL_OP_ZFAIL:
{
// Stencil takes a back seat to depth buffer stuff
evaluate_depth_buffer_state();
if (!m_framebuffer_layout.zeta_write_enabled)
{
evaluate_stencil_buffer_state();
}
if (m_graphics_state.test(rsx::rtt_config_contested) && evaluate_depth_buffer_contested())
{
m_graphics_state.set(rsx::rtt_config_dirty);
}
break;
}
case NV4097_SET_COLOR_MASK:
case NV4097_SET_COLOR_MASK_MRT:
{
if (!m_graphics_state.test(rsx::rtt_config_contested)) [[likely]]
{
// Update write masks and continue
evaluate_color_buffer_state();
}
else
{
bool old_state = false;
for (const auto& enabled : m_framebuffer_layout.color_write_enabled)
{
if (old_state = enabled; old_state) break;
}
const auto new_state = evaluate_color_buffer_state();
if (!old_state && new_state)
{
// Color buffers now in use
m_graphics_state.set(rsx::rtt_config_dirty);
}
}
break;
}
default:
rsx_log.fatal("Unhandled framebuffer option changed 0x%x", opt);
break;
}
}
void thread::on_framebuffer_layout_updated()
{
if (m_graphics_state.test(rsx::fragment_program_state_dirty))
{
return;
}
const auto target = m_ctx->register_state->surface_color_target();
if (rsx::utility::get_mrt_buffers_count(target) == current_fragment_program.mrt_buffers_count)
{
return;
}
m_graphics_state |= rsx::fragment_program_state_dirty;
}
bool thread::get_scissor(areau& region, bool clip_viewport)
{
if (!m_graphics_state.test(rsx::pipeline_state::scissor_config_state_dirty))
{
if (clip_viewport == m_graphics_state.test(rsx::pipeline_state::scissor_setup_clipped))
{
// Nothing to do
return false;
}
}
m_graphics_state.clear(rsx::pipeline_state::scissor_config_state_dirty | rsx::pipeline_state::scissor_setup_clipped);
u16 x1, x2, y1, y2;
u16 scissor_x = rsx::method_registers.scissor_origin_x();
u16 scissor_w = rsx::method_registers.scissor_width();
u16 scissor_y = rsx::method_registers.scissor_origin_y();
u16 scissor_h = rsx::method_registers.scissor_height();
if (clip_viewport)
{
u16 raster_x = rsx::method_registers.viewport_origin_x();
u16 raster_w = rsx::method_registers.viewport_width();
u16 raster_y = rsx::method_registers.viewport_origin_y();
u16 raster_h = rsx::method_registers.viewport_height();
// Get the minimum area between these two
x1 = std::max(scissor_x, raster_x);
y1 = std::max(scissor_y, raster_y);
x2 = std::min(scissor_x + scissor_w, raster_x + raster_w);
y2 = std::min(scissor_y + scissor_h, raster_y + raster_h);
m_graphics_state |= rsx::pipeline_state::scissor_setup_clipped;
}
else
{
x1 = scissor_x;
x2 = scissor_x + scissor_w;
y1 = scissor_y;
y2 = scissor_y + scissor_h;
}
if (x2 <= x1 ||
y2 <= y1 ||
x1 >= rsx::method_registers.window_clip_horizontal() ||
y1 >= rsx::method_registers.window_clip_vertical())
{
m_graphics_state |= rsx::pipeline_state::scissor_setup_invalid;
m_graphics_state.clear(rsx::rtt_config_valid);
return false;
}
if (m_graphics_state & rsx::pipeline_state::scissor_setup_invalid)
{
m_graphics_state.clear(rsx::pipeline_state::scissor_setup_invalid);
m_graphics_state.set(rsx::rtt_config_valid);
}
std::tie(region.x1, region.y1) = rsx::apply_resolution_scale<false>(resolution_scaling_config, x1, y1, m_framebuffer_layout.width, m_framebuffer_layout.height);
std::tie(region.x2, region.y2) = rsx::apply_resolution_scale<true>(resolution_scaling_config, x2, y2, m_framebuffer_layout.width, m_framebuffer_layout.height);
return true;
}
rsx::flags32_t thread::get_fragment_program_export_config()
{
if (!g_cfg.video.emulate_depth_compare) [[ likely ]]
{
return 0;
}
if (m_ctx->register_state->current_draw_clause.classify_mode() != primitive_class::polygon)
{
return 0;
}
u32 expected_ctrl = 0;
if (m_framebuffer_layout.zeta_address &&
m_ctx->register_state->depth_test_enabled() &&
m_ctx->register_state->depth_func() == rsx::comparison_function::equal)
{
expected_ctrl |= RSX_SHADER_CONTROL_EMULATE_DEPTH_COMPARE;
if (backend_config.supports_hw_msaa &&
m_ctx->register_state->surface_antialias() != rsx::surface_antialiasing::center_1_sample)
{
expected_ctrl |= RSX_SHADER_CONTROL_MULTISAMPLED_ZBUFFER;
}
}
return expected_ctrl;
}
void thread::prefetch_fragment_program()
{
if (!m_graphics_state.test(rsx::pipeline_state::fragment_program_ucode_dirty))
{
return;
}
m_graphics_state.clear(rsx::pipeline_state::fragment_program_ucode_dirty);
// Request for update of fragment constants if the program block is invalidated
m_graphics_state |= rsx::pipeline_state::fragment_constants_dirty;
const auto [program_offset, program_location] = method_registers.shader_program_address();
const auto prev_textures_reference_mask = current_fp_metadata.referenced_textures_mask;
auto data_ptr = vm::base(rsx::get_address(program_offset, program_location));
current_fp_metadata = program_hash_util::fragment_program_utils::analyse_fragment_program(data_ptr);
current_fragment_program.data = (static_cast<u8*>(data_ptr) + current_fp_metadata.program_start_offset);
current_fragment_program.offset = program_offset + current_fp_metadata.program_start_offset;
current_fragment_program.ucode_length = current_fp_metadata.program_ucode_length;
current_fragment_program.total_length = current_fp_metadata.program_ucode_length + current_fp_metadata.program_start_offset;
current_fragment_program.texture_state.import(current_fp_texture_state, current_fp_metadata.referenced_textures_mask);
current_fragment_program.valid = true;
if (!m_graphics_state.test(rsx::pipeline_state::fragment_program_state_dirty))
{
// Verify current texture state is valid
for (u32 textures_ref = current_fp_metadata.referenced_textures_mask, i = 0; textures_ref; textures_ref >>= 1, ++i)
{
if (!(textures_ref & 1)) continue;
if (m_textures_dirty[i])
{
m_graphics_state |= rsx::pipeline_state::fragment_program_state_dirty;
break;
}
}
}
if (!m_graphics_state.test(rsx::pipeline_state::fragment_program_state_dirty) &&
(prev_textures_reference_mask != current_fp_metadata.referenced_textures_mask))
{
// If different textures are used, upload their coefficients.
// The texture parameters transfer routine is optimized and only writes data for textures consumed by the ucode.
m_graphics_state |= rsx::pipeline_state::fragment_texture_state_dirty;
}
}
void thread::prefetch_vertex_program()
{
if (!m_graphics_state.test(rsx::pipeline_state::vertex_program_ucode_dirty))
{
return;
}
m_graphics_state.clear(rsx::pipeline_state::vertex_program_ucode_dirty);
// Reload transform constants unconditionally for now
m_graphics_state |= rsx::pipeline_state::transform_constants_dirty;
const u32 transform_program_start = rsx::method_registers.transform_program_start();
current_vertex_program.data.reserve(512 * 4);
current_vertex_program.jump_table.clear();
current_vp_metadata = program_hash_util::vertex_program_utils::analyse_vertex_program
(
method_registers.transform_program.data(), // Input raw block
transform_program_start, // Address of entry point
current_vertex_program // [out] Program object
);
current_vertex_program.texture_state.import(current_vp_texture_state, current_vp_metadata.referenced_textures_mask);
if (!m_graphics_state.test(rsx::pipeline_state::vertex_program_state_dirty))
{
// Verify current texture state is valid
for (u32 textures_ref = current_vp_metadata.referenced_textures_mask, i = 0; textures_ref; textures_ref >>= 1, ++i)
{
if (!(textures_ref & 1)) continue;
if (m_vertex_textures_dirty[i])
{
m_graphics_state |= rsx::pipeline_state::vertex_program_state_dirty;
break;
}
}
}
}
void thread::analyse_current_rsx_pipeline()
{
// Vertex and fragment ucode analysis: a full program walk with recursive visitors
// and per-call allocations, run before load_program, so the pipeline scope never
// saw it and shader_translate had no site at all.
RSX_PROF_SCOPE(shader_translate);
m_program_cache_hint.invalidate(m_graphics_state.load());
constexpr u32 fs_export_config_mask = (RSX_SHADER_CONTROL_EMULATE_DEPTH_COMPARE | RSX_SHADER_CONTROL_MULTISAMPLED_ZBUFFER);
if (u32 export_ctrl = get_fragment_program_export_config();
(current_fragment_program.ctrl & fs_export_config_mask) != export_ctrl)
{
// Update control bits for immediate consumers
current_fragment_program.ctrl &= ~fs_export_config_mask;
current_fragment_program.ctrl |= export_ctrl;
// Signal backend to reload pipeline
m_graphics_state.set(rsx::pipeline_state::fragment_program_state_dirty);
}
prefetch_vertex_program();
prefetch_fragment_program();
}
void thread::get_current_vertex_program(const std::array<std::unique_ptr<rsx::sampled_image_descriptor_base>, rsx::limits::vertex_textures_count>& sampler_descriptors)
{
if (m_graphics_state.test(rsx::pipeline_state::xform_instancing_state_dirty))
{
current_vertex_program.ctrl &= ~RSX_SHADER_CONTROL_INSTANCED_CONSTANTS;
if (rsx::method_registers.current_draw_clause.is_trivial_instanced_draw)
{
current_vertex_program.ctrl |= RSX_SHADER_CONTROL_INSTANCED_CONSTANTS;
}
m_graphics_state.clear(rsx::pipeline_state::xform_instancing_state_dirty);
// Emit invalidate here in case ucode is actually clean
m_program_cache_hint.invalidate_vertex_program(current_vertex_program);
}
if (!m_graphics_state.test(rsx::pipeline_state::vertex_program_dirty))
{
return;
}
ensure(!m_graphics_state.test(rsx::pipeline_state::vertex_program_ucode_dirty));
current_vertex_program.output_mask = rsx::method_registers.vertex_attrib_output_mask();
current_vertex_program.ctrl &= ~RSX_SHADER_CONTROL_FLAT_SHADING;
if (rsx::method_registers.shade_mode() == rsx::shading_mode::flat &&
backend_config.supports_last_provoking_vertex)
{
current_vertex_program.ctrl |= RSX_SHADER_CONTROL_FLAT_SHADING;
}
for (u32 textures_ref = current_vp_metadata.referenced_textures_mask, i = 0; textures_ref; textures_ref >>= 1, ++i)
{
if (!(textures_ref & 1)) continue;
const auto &tex = rsx::method_registers.vertex_textures[i];
if (tex.enabled() && (current_vp_metadata.referenced_textures_mask & (1 << i)))
{
current_vp_texture_state.clear(i);
current_vp_texture_state.set_dimension(sampler_descriptors[i]->image_type, i);
if (backend_config.supports_hw_msaa &&
sampler_descriptors[i]->samples > 1)
{
current_vp_texture_state.multisampled_textures |= (1 << i);
}
}
}
current_vertex_program.texture_state.import(current_vp_texture_state, current_vp_metadata.referenced_textures_mask);
m_program_cache_hint.invalidate_vertex_program(current_vertex_program);
}
void thread::get_current_fragment_program(const std::array<std::unique_ptr<rsx::sampled_image_descriptor_base>, rsx::limits::fragment_textures_count>& sampler_descriptors)
{
if (!m_graphics_state.test(rsx::pipeline_state::fragment_program_dirty))
{
return;
}
ensure(!m_graphics_state.test(rsx::pipeline_state::fragment_program_ucode_dirty));
m_graphics_state.clear(rsx::pipeline_state::fragment_program_dirty);
current_fragment_program.ctrl = m_ctx->register_state->shader_control() & (CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS | CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT | RSX_SHADER_CONTROL_USES_KIL);
current_fragment_program.texcoord_control_mask = m_ctx->register_state->texcoord_control_mask();
current_fragment_program.two_sided_lighting = m_ctx->register_state->two_side_light_en();
current_fragment_program.mrt_buffers_count = rsx::utility::get_mrt_buffers_count(m_ctx->register_state->surface_color_target());
if (m_ctx->register_state->shade_mode() == rsx::shading_mode::flat &&
backend_config.supports_last_provoking_vertex)
{
current_fragment_program.ctrl |= RSX_SHADER_CONTROL_FLAT_SHADING;
}
if (m_ctx->register_state->current_draw_clause.classify_mode() == primitive_class::polygon)
{
if (!backend_config.supports_normalized_barycentrics)
{
current_fragment_program.ctrl |= RSX_SHADER_CONTROL_ATTRIBUTE_INTERPOLATION;
}
if (m_ctx->register_state->polygon_stipple_enabled())
{
current_fragment_program.ctrl |= RSX_SHADER_CONTROL_POLYGON_STIPPLE;
}
current_fragment_program.ctrl |= get_fragment_program_export_config();
}
else if (m_ctx->register_state->point_sprite_enabled() &&
m_ctx->register_state->current_draw_clause.primitive == primitive_type::points)
{
// Set high word of the control mask to store point sprite control
current_fragment_program.texcoord_control_mask |= u32(m_ctx->register_state->point_sprite_control_mask()) << 16;
}
if (m_ctx->register_state->alpha_test_enabled())
{
current_fragment_program.ctrl |= RSX_SHADER_CONTROL_ALPHA_TEST;
}
if (m_ctx->register_state->msaa_alpha_to_coverage_enabled())
{
const bool is_multiple_samples = m_ctx->register_state->surface_antialias() != rsx::surface_antialiasing::center_1_sample;
if (!backend_config.supports_hw_a2c || (!is_multiple_samples && !backend_config.supports_hw_a2c_1spp))
{
// Emulation required
current_fragment_program.ctrl |= RSX_SHADER_CONTROL_ALPHA_TO_COVERAGE;
}
}
// Check if framebuffer is actually an XRGB format and not a WZYX format
switch (m_ctx->register_state->surface_color())
{
case rsx::surface_color_format::w16z16y16x16:
case rsx::surface_color_format::w32z32y32x32:
case rsx::surface_color_format::x32:
// These behave very differently from "normal" formats.
break;
default:
// Integer framebuffer formats. These can support sRGB output as well as some special rules for output quantization.
current_fragment_program.ctrl |= RSX_SHADER_CONTROL_8BIT_FRAMEBUFFER;
if (!(current_fragment_program.ctrl & CELL_GCM_SHADER_CONTROL_32_BITS_EXPORTS) && // Cannot output sRGB from 32-bit registers
m_ctx->register_state->framebuffer_srgb_enabled())
{
current_fragment_program.ctrl |= RSX_SHADER_CONTROL_SRGB_FRAMEBUFFER;
}
if (m_ctx->register_state->surface_is_swizzle_remapped())
{
current_fragment_program.ctrl |= RSX_SHADER_CONTROL_ROP_OUTPUT_REMAP;
}
break;
}
const bool zeta_was_cyclic = m_graphics_state & rsx::zeta_address_is_cyclic;
m_graphics_state.clear(rsx::zeta_address_is_cyclic);
for (u32 textures_ref = current_fp_metadata.referenced_textures_mask, i = 0; textures_ref; textures_ref >>= 1, ++i)
{
if (!(textures_ref & 1)) continue;
auto &tex = m_ctx->register_state->fragment_textures[i];
current_fp_texture_state.clear(i);
if (!tex.enabled() || sampler_descriptors[i]->format_class == RSX_FORMAT_CLASS_UNDEFINED)
{
continue;
}
std::memcpy(
current_fragment_program.texture_params[i].scale,
sampler_descriptors[i]->texcoord_xform.scale,
sizeof(sampler_descriptors[i]->texcoord_xform.scale) * 2); // Copy scale and bias together
current_fragment_program.texture_params[i].remap = tex.remap();
m_graphics_state |= rsx::pipeline_state::fragment_texture_state_dirty;
u32 texture_control = 0;
current_fp_texture_state.set_dimension(sampler_descriptors[i]->image_type, i);
if (sampler_descriptors[i]->texcoord_xform.clamp)
{
std::memcpy(
current_fragment_program.texture_params[i].clamp_min,
sampler_descriptors[i]->texcoord_xform.clamp_min,
sizeof(sampler_descriptors[i]->texcoord_xform.clamp_min) * 2); // Copy clamp_min and clamp_max together
texture_control |= (1 << rsx::texture_control_bits::CLAMP_TEXCOORDS_BIT);
}
if (tex.alpha_kill_enabled())
{
//alphakill can be ignored unless a valid comparison function is set
texture_control |= (1 << texture_control_bits::ALPHAKILL);
current_fragment_program.ctrl |= RSX_SHADER_CONTROL_TEXTURE_ALPHA_KILL;
}
//const u32 texaddr = rsx::get_address(tex.offset(), tex.location());
const u32 raw_format = tex.format();
const u32 format = raw_format & ~(CELL_GCM_TEXTURE_LN | CELL_GCM_TEXTURE_UN);
if (raw_format & CELL_GCM_TEXTURE_UN)
{
if (tex.min_filter() == rsx::texture_minify_filter::nearest ||
tex.mag_filter() == rsx::texture_magnify_filter::nearest)
{
// Subpixel offset so that (X + bias) * scale will round correctly.
// This is done to work around fdiv precision issues in some GPUs (NVIDIA)
// We apply the simplification where (x + bias) * z = xz + zbias here.
constexpr auto subpixel_bias = 0.01f;
current_fragment_program.texture_params[i].bias[0] += (subpixel_bias * current_fragment_program.texture_params[i].scale[0]);
current_fragment_program.texture_params[i].bias[1] += (subpixel_bias * current_fragment_program.texture_params[i].scale[1]);
current_fragment_program.texture_params[i].bias[2] += (subpixel_bias * current_fragment_program.texture_params[i].scale[2]);
}
}
if (backend_config.supports_hw_msaa && sampler_descriptors[i]->samples > 1)
{
current_fp_texture_state.multisampled_textures |= (1 << i);
texture_control |= (static_cast<u32>(tex.zfunc()) << texture_control_bits::DEPTH_COMPARE_OP);
texture_control |= (static_cast<u32>(tex.mag_filter() != rsx::texture_magnify_filter::nearest) << texture_control_bits::FILTERED_MAG);
texture_control |= (static_cast<u32>(tex.min_filter() != rsx::texture_minify_filter::nearest) << texture_control_bits::FILTERED_MIN);
texture_control |= (((tex.format() & CELL_GCM_TEXTURE_UN) >> 6) << texture_control_bits::UNNORMALIZED_COORDS);
if (rsx::is_texcoord_wrapping_mode(tex.wrap_s()))
{
texture_control |= (1 << texture_control_bits::WRAP_S);
}
if (rsx::is_texcoord_wrapping_mode(tex.wrap_t()))
{
texture_control |= (1 << texture_control_bits::WRAP_T);
}
if (rsx::is_texcoord_wrapping_mode(tex.wrap_r()))
{
texture_control |= (1 << texture_control_bits::WRAP_R);
}
}
if (sampler_descriptors[i]->format_class != RSX_FORMAT_CLASS_COLOR)
{
switch (sampler_descriptors[i]->format_class)
{
case RSX_FORMAT_CLASS_DEPTH16_FLOAT:
case RSX_FORMAT_CLASS_DEPTH24_FLOAT_X8_PACK32:
texture_control |= (1 << texture_control_bits::DEPTH_FLOAT);
break;
default:
break;
}
switch (format)
{
case CELL_GCM_TEXTURE_A8R8G8B8:
case CELL_GCM_TEXTURE_D8R8G8B8:
{
// Emulate bitcast in shader
current_fp_texture_state.redirected_textures |= (1 << i);
const auto float_en = (sampler_descriptors[i]->format_class == RSX_FORMAT_CLASS_DEPTH24_FLOAT_X8_PACK32)? 1 : 0;
texture_control |= (float_en << texture_control_bits::DEPTH_FLOAT);
break;
}
case CELL_GCM_TEXTURE_X16:
{
// A simple way to quickly read DEPTH16 data without shadow comparison
break;
}
case CELL_GCM_TEXTURE_DEPTH16:
case CELL_GCM_TEXTURE_DEPTH24_D8:
case CELL_GCM_TEXTURE_DEPTH16_FLOAT:
case CELL_GCM_TEXTURE_DEPTH24_D8_FLOAT:
{
// Natively supported Z formats with shadow comparison feature
const auto compare_mode = tex.zfunc();
if (!tex.alpha_kill_enabled() &&
compare_mode < rsx::comparison_function::always &&
compare_mode > rsx::comparison_function::never)
{
current_fp_texture_state.shadow_textures |= (1 << i);
}
break;
}
default:
rsx_log.error("Depth texture bound to pipeline with unexpected format 0x%X", format);
}
if (sampler_descriptors[i]->is_cyclic_reference &&
m_framebuffer_layout.zeta_address != 0 &&
!g_cfg.video.strict_rendering_mode &&
g_cfg.video.shader_precision != gpu_preset_level::low)
{
m_graphics_state |= rsx::zeta_address_is_cyclic;
if (!(current_fragment_program.ctrl & (CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT | RSX_SHADER_CONTROL_META_USES_DISCARD | RSX_SHADER_CONTROL_EMULATE_DEPTH_COMPARE)) &&
m_framebuffer_layout.zeta_write_enabled)
{
current_fragment_program.ctrl |= RSX_SHADER_CONTROL_DISABLE_EARLY_Z;
}
}
}
else if (!backend_config.supports_hw_renormalization /* &&
tex.min_filter() == rsx::texture_minify_filter::nearest &&
tex.mag_filter() == rsx::texture_magnify_filter::nearest*/)
{
// FIXME: This check should only apply to point-sampled textures. However, it severely regresses some games (id tech 5).
// This is because even when filtering is active, the error from the PS3 texture expansion still applies.
// A proper fix is to expand these formats into BGRA8 when high texture precision is required. That requires different GUI settings and inflation shaders, so it will be handled separately.
switch (format)
{
case CELL_GCM_TEXTURE_A1R5G5B5:
case CELL_GCM_TEXTURE_A4R4G4B4:
case CELL_GCM_TEXTURE_D1R5G5B5:
case CELL_GCM_TEXTURE_R5G5B5A1:
case CELL_GCM_TEXTURE_R5G6B5:
case CELL_GCM_TEXTURE_R6G5B5:
texture_control |= (1 << texture_control_bits::RENORMALIZE);
current_fragment_program.ctrl |= RSX_SHADER_CONTROL_TEXTURE_FORMAT_CONVERT;
break;
default:
break;
}
}
if (const auto& format_ex = sampler_descriptors[i]->format_ex; format_ex.features != 0)
{
texture_control |= format_ex.texel_remap_control;
texture_control |= format_ex.features << texture_control_bits::FORMAT_FEATURES_OFFSET;
if (format_ex.texel_remap_control)
{
current_fragment_program.ctrl |= RSX_SHADER_CONTROL_TEXTURE_FORMAT_CONVERT;
}
if (current_fp_metadata.bx2_texture_reads_mask)
{
current_fragment_program.ctrl |= RSX_SHADER_CONTROL_TEXTURE_FORMAT_CONVERT;
const u32 remap_hi = tex.decoded_remap().shuffle_mask_bits(0xFu);
current_fragment_program.texture_params[i].remap &= ~(0xFu << 16u);
current_fragment_program.texture_params[i].remap |= (remap_hi << 16u);
}
}
current_fragment_program.texture_params[i].control = texture_control;
}
// Update texture configuration
current_fragment_program.texture_state.import(current_fp_texture_state, current_fp_metadata.referenced_textures_mask);
//Sanity checks
if (current_fragment_program.ctrl & CELL_GCM_SHADER_CONTROL_DEPTH_EXPORT)
{
//Check that the depth stage is not disabled
if (!m_ctx->register_state->depth_test_enabled())
{
rsx_log.trace("FS exports depth component but depth test is disabled (INVALID_OPERATION)");
}
}
m_program_cache_hint.invalidate_fragment_program(current_fragment_program);
if (zeta_was_cyclic && zeta_was_cyclic != m_graphics_state.test(rsx::zeta_address_is_cyclic))
{
// Forced "fall-out" barrier. This is a special case for Z buffers because they can be cyclic without writes.
// That condition can cause early-Z in a later call to introduce data hazard in previous cyclic draws.
m_graphics_state |= rsx::zeta_address_cyclic_barrier;
}
}
bool thread::invalidate_fragment_program(u32 dst_dma, u32 dst_offset, u32 size)
{
if (!current_fragment_program.total_length)
{
// No shader loaded
return false;
}
const auto [shader_offset, shader_dma] = rsx::method_registers.shader_program_address();
if ((dst_dma & CELL_GCM_LOCATION_MAIN) != shader_dma)
{
// Shader not loaded in XDR memory
return false;
}
const auto current_fragment_shader_range = address_range32::start_length(shader_offset, current_fragment_program.total_length);
if (!current_fragment_shader_range.overlaps(address_range32::start_length(dst_offset, size)))
{
// No range overlap
return false;
}
// Data overlaps. Force ucode reload.
m_graphics_state |= rsx::pipeline_state::fragment_program_ucode_dirty;
return true;
}
void thread::reset()
{
rsx::method_registers.reset();
check_zcull_status(false);
nv4097::set_render_mode(m_ctx, 0, method_registers.registers[NV4097_SET_RENDER_ENABLE]);
m_graphics_state |= pipeline_state::all_dirty;
}
void thread::init(u32 ctrlAddress)
{
dma_address = ctrlAddress;
ctrl = vm::_ptr<RsxDmaControl>(ctrlAddress);
flip_status = CELL_GCM_DISPLAY_FLIP_STATUS_DONE;
fifo_ret_addr = RSX_CALL_STACK_EMPTY;
vm::write32(device_addr + 0x30, 1);
std::memset(display_buffers, 0, sizeof(display_buffers));
rsx_thread_running = true;
}
std::pair<u32, u32> thread::calculate_memory_requirements(const vertex_input_layout& layout, u32 first_vertex, u32 vertex_count)
{
u32 persistent_memory_size = 0;
u32 volatile_memory_size = 0;
volatile_memory_size += ::size32(layout.referenced_registers) * 16u;
if (rsx::method_registers.current_draw_clause.command == rsx::draw_command::inlined_array)
{
for (const auto &block : layout.interleaved_blocks)
{
volatile_memory_size += block->attribute_stride * vertex_count;
}
}
else
{
//NOTE: Immediate commands can be index array only or both index array and vertex data
//Check both - but only check volatile blocks if immediate_draw flag is set
if (rsx::method_registers.current_draw_clause.is_immediate_draw)
{
for (const auto &info : layout.volatile_blocks)
{
volatile_memory_size += info.second;
}
}
persistent_memory_size = layout.calculate_interleaved_memory_requirements(first_vertex, vertex_count);
}
return std::make_pair(persistent_memory_size, volatile_memory_size);
}
void thread::flip(const display_flip_info_t& info)
{
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
if (info.emu_flip)
{
async_flip_requested.clear(flip_request::emu_requested);
}
else
{
async_flip_requested.clear(flip_request::native_ui);
}
}
if (info.emu_flip)
{
performance_counters.sampled_frames++;
if (m_pause_after_x_flips && m_pause_after_x_flips-- == 1)
{
Emu.Pause();
}
}
last_host_flip_timestamp = get_system_time();
}
void thread::check_zcull_status(bool framebuffer_swap)
{
const bool zcull_rendering_enabled = !!method_registers.registers[NV4097_SET_ZCULL_EN];
const bool zcull_stats_enabled = !!method_registers.registers[NV4097_SET_ZCULL_STATS_ENABLE];
const bool zcull_pixel_cnt_enabled = !!method_registers.registers[NV4097_SET_ZPASS_PIXEL_COUNT_ENABLE];
if (framebuffer_swap)
{
zcull_surface_active = false;
const u32 zeta_address = m_depth_surface_info.address;
if (zeta_address)
{
//Find zeta address in bound zculls
for (const auto& zcull : zculls)
{
if (zcull.bound &&
rsx::to_surface_depth_format(zcull.zFormat) == m_depth_surface_info.depth_format &&
rsx::to_surface_antialiasing(zcull.aaFormat) == rsx::method_registers.surface_antialias())
{
const u32 rsx_address = rsx::get_address(zcull.offset, CELL_GCM_LOCATION_LOCAL);
if (rsx_address == zeta_address)
{
zcull_surface_active = true;
break;
}
}
}
}
}
zcull_ctrl->set_enabled(this, zcull_rendering_enabled);
zcull_ctrl->set_status(this, zcull_surface_active, zcull_pixel_cnt_enabled, zcull_stats_enabled);
}
void thread::clear_zcull_stats(u32 type)
{
zcull_ctrl->clear(this, type);
}
void thread::get_zcull_stats(u32 type, vm::addr_t sink)
{
u32 value = 0;
if (!g_cfg.video.disable_zcull_queries)
{
switch (type)
{
case CELL_GCM_ZPASS_PIXEL_CNT:
case CELL_GCM_ZCULL_STATS:
case CELL_GCM_ZCULL_STATS1:
case CELL_GCM_ZCULL_STATS2:
case CELL_GCM_ZCULL_STATS3:
{
zcull_ctrl->read_report(this, sink, type);
return;
}
default:
rsx_log.error("Unknown zcull stat type %d", type);
break;
}
}
rsx::reservation_lock<true> lock(sink, 16);
vm::_ptr<atomic_t<CellGcmReportData>>(sink)->store({timestamp(), value, 0});
}
u32 thread::copy_zcull_stats(u32 memory_range_start, u32 memory_range, u32 destination)
{
return zcull_ctrl->copy_reports_to(memory_range_start, memory_range, destination);
}
void thread::enable_conditional_rendering(vm::addr_t ref)
{
cond_render_ctrl.enable_conditional_render(this, ref);
auto result = zcull_ctrl->find_query(ref, true);
if (result.found)
{
if (!result.queries.empty())
{
cond_render_ctrl.set_eval_sources(result.queries);
sync_hint(FIFO::interrupt_hint::conditional_render_eval, { .query = cond_render_ctrl.eval_sources.front(), .address = ref });
}
else
{
bool failed = (result.raw_zpass_result == 0);
cond_render_ctrl.set_eval_result(this, failed);
}
}
else
{
cond_render_ctrl.eval_result(this);
}
}
void thread::disable_conditional_rendering()
{
cond_render_ctrl.disable_conditional_render(this);
}
void thread::begin_conditional_rendering(const std::vector<reports::occlusion_query_info*>& /*sources*/)
{
cond_render_ctrl.hw_cond_active = true;
cond_render_ctrl.eval_sources.clear();
}
void thread::end_conditional_rendering()
{
cond_render_ctrl.hw_cond_active = false;
}
void thread::sync()
{
m_eng_interrupt_mask.clear(rsx::pipe_flush_interrupt);
mm_flush();
if (zcull_ctrl->has_pending())
{
zcull_ctrl->sync(this);
}
// Fragment constants may have been updated
m_graphics_state |= rsx::pipeline_state::fragment_constants_dirty;
// DMA sync; if you need this, don't use MTRSX
// g_fxo->get<rsx::dma_manager>().sync();
//TODO: On sync every sub-unit should finish any pending tasks
//Might cause zcull lockup due to zombie 'unclaimed reports' which are not forcefully removed currently
//ensure(async_tasks_pending.load() == 0);
}
void thread::sync_hint(FIFO::interrupt_hint /*hint*/, rsx::reports::sync_hint_payload_t payload)
{
zcull_ctrl->on_sync_hint(payload);
}
bool thread::is_fifo_idle() const
{
return ctrl == nullptr || ctrl->get == (ctrl->put & ~3);
}
void thread::flush_fifo()
{
// Make sure GET value is exposed before sync points
fifo_ctrl->sync_get();
fifo_ctrl->invalidate_cache();
}
std::pair<u32, u32> thread::try_get_pc_of_x_cmds_backwards(s32 count, u32 get) const
{
if (!ctrl || state & cpu_flag::exit)
{
return {0, umax};
}
if (!count)
{
return {0, get};
}
u32 true_get = ctrl->get;
u32 start = last_known_code_start;
RSXDisAsm disasm(cpu_disasm_mode::survey_cmd_size, vm::g_sudo_addr, 0, this);
std::vector<u32> pcs_of_valid_cmds;
if (get > start)
{
pcs_of_valid_cmds.reserve(std::min<u32>((get - start) / 16, 0x4000)); // Rough estimation of final array size
}
auto probe_code_region = [&](u32 probe_start) -> std::pair<u32, u32>
{
if (probe_start > get)
{
return {0, get};
}
pcs_of_valid_cmds.clear();
pcs_of_valid_cmds.push_back(probe_start);
usz index_of_get = umax;
usz until = umax;
while (pcs_of_valid_cmds.size() < until)
{
if (u32 advance = disasm.disasm(pcs_of_valid_cmds.back()))
{
pcs_of_valid_cmds.push_back(utils::add_saturate<u32>(pcs_of_valid_cmds.back(), advance));
}
else
{
break;
}
if (index_of_get == umax && pcs_of_valid_cmds.back() >= get)
{
index_of_get = pcs_of_valid_cmds.size() - 1;
until = index_of_get + 1;
if (count < 0 && pcs_of_valid_cmds.back() == get)
{
until -= count;
}
}
}
if (index_of_get == umax || pcs_of_valid_cmds[index_of_get] != get)
{
return {0, get};
}
if (count < 0)
{
const u32 found_cmds_count = static_cast<u32>(std::min<s64>(-count, pcs_of_valid_cmds.size() - 1LL - index_of_get));
return {found_cmds_count, pcs_of_valid_cmds[index_of_get + found_cmds_count]};
}
const u32 found_cmds_count = std::min<u32>(count, ::size32(pcs_of_valid_cmds) - 1);
return {found_cmds_count, *(pcs_of_valid_cmds.end() - 1 - found_cmds_count)};
};
auto pair = probe_code_region(start);
if (!pair.first)
{
pair = probe_code_region(true_get);
}
return pair;
}
void thread::recover_fifo(std::source_location src_loc)
{
bool kill_itself = g_cfg.core.rsx_fifo_accuracy == rsx_fifo_mode::as_ps3;
const u64 current_time = get_system_time();
if (recovered_fifo_cmds_history.size() == 20u)
{
const auto cmd_info = recovered_fifo_cmds_history.front();
// Check timestamp of last tracked cmd
// Shorten the range of forbidden difference if driver wake-up delay is used
if (current_time - cmd_info.timestamp < 2'000'000u - std::min<u32>(g_cfg.video.driver_wakeup_delay * 700, 1'400'000))
{
// Probably hopeless
kill_itself = true;
}
// Erase the last command from history, keep the size of the queue the same
recovered_fifo_cmds_history.pop();
}
if (kill_itself)
{
fmt::throw_exception("Dead FIFO commands queue state has been detected!"
"\nTry increasing \"Driver Wake-Up Delay\" setting or setting \"RSX FIFO Accuracy\" to \"%s\", both in Advanced settings. Called from %s", std::min<rsx_fifo_mode>(rsx_fifo_mode{static_cast<u32>(g_cfg.core.rsx_fifo_accuracy.get()) + 1}, rsx_fifo_mode::atomic_ordered), src_loc);
}
// Error. Should reset the queue
fifo_ctrl->set_get(restore_point);
fifo_ret_addr = saved_fifo_ret;
std::this_thread::sleep_for(2ms);
fifo_ctrl->abort();
if (std::exchange(in_begin_end, false) && !rsx::method_registers.current_draw_clause.empty())
{
execute_nop_draw();
rsx::thread::end();
}
recovered_fifo_cmds_history.push({fifo_ctrl->last_cmd(), current_time});
}
void thread::dump_misc(std::string& ret, std::any& custom_data) const
{
cpu_thread::dump_misc(ret, custom_data);
const auto flags = +state;
if (is_paused(flags) && flags & cpu_flag::wait)
{
fmt::append(ret, "\nFragment Program Hash: %X.fp", current_fragment_program.get_data() ? program_hash_util::fragment_program_utils::get_fragment_program_ucode_hash(current_fragment_program) : 0);
fmt::append(ret, "\nVertex Program Hash: %X.vp", current_vertex_program.data.empty() ? 0 : program_hash_util::vertex_program_utils::get_vertex_program_ucode_hash(current_vertex_program));
}
else
{
fmt::append(ret, "\n");
}
}
std::vector<std::pair<u32, u32>> thread::dump_callstack_list() const
{
std::vector<std::pair<u32, u32>> result;
if (u32 addr = fifo_ret_addr; addr != RSX_CALL_STACK_EMPTY)
{
result.emplace_back(addr, 0);
}
return result;
}
void thread::fifo_wake_delay(u64 div)
{
// TODO: Nanoseconds accuracy
u64 remaining = g_cfg.video.driver_wakeup_delay;
if (!remaining)
{
return;
}
// Some cases do not need full delay
remaining = utils::aligned_div(remaining, div);
const u64 until = get_system_time() + remaining;
while (true)
{
#ifdef __linux__
// NOTE: Assumption that timer initialization has succeeded
constexpr u64 host_min_quantum = 10;
#else
// Host scheduler quantum for windows (worst case)
// NOTE: On ps3 this function has very high accuracy
constexpr u64 host_min_quantum = 500;
#endif
if (remaining >= host_min_quantum)
{
#ifdef __linux__
thread_ctrl::wait_for(remaining, false);
#else
// Wait on multiple of min quantum for large durations to avoid overloading low thread cpus
thread_ctrl::wait_for(remaining - (remaining % host_min_quantum), false);
#endif
}
// TODO: Determine best value for yield delay
else if (remaining >= host_min_quantum / 2)
{
std::this_thread::yield();
}
else
{
busy_wait(100);
}
const u64 current = get_system_time();
if (current >= until)
{
break;
}
remaining = until - current;
}
}
u32 thread::get_fifo_cmd() const
{
// Last fifo cmd for logging and utility
return fifo_ctrl->last_cmd();
}
void invalid_method(context*, u32, u32);
void thread::dump_regs(std::string& result, std::any& /*custom_data*/) const
{
if (ctrl)
{
fmt::append(result, "FIFO: GET=0x%07x, PUT=0x%07x, REF=0x%08x\n", +ctrl->get, +ctrl->put, +ctrl->ref);
}
for (u32 i = 0; i < 1 << 14; i++)
{
if (rsx::methods[i] == &invalid_method)
{
continue;
}
switch (i)
{
case NV4097_NO_OPERATION:
case NV4097_INVALIDATE_L2:
case NV4097_INVALIDATE_VERTEX_FILE:
case NV4097_INVALIDATE_VERTEX_CACHE_FILE:
case NV4097_INVALIDATE_ZCULL:
case NV4097_WAIT_FOR_IDLE:
case NV4097_PM_TRIGGER:
case NV4097_ZCULL_SYNC:
continue;
case NV308A_COLOR:
{
i = NV3089_SET_OBJECT;
continue;
}
default:
{
break;
}
}
fmt::append(result, "[%04x] ", i);
ensure(rsx::get_pretty_printing_function(i))(result, i, method_registers.registers[i]);
result += '\n';
}
}
flags32_t thread::read_barrier(u32 memory_address, u32 memory_range, bool unconditional)
{
flags32_t zcull_flags = (unconditional)? reports::sync_none : reports::sync_defer_copy;
return zcull_ctrl->read_barrier(this, memory_address, memory_range, zcull_flags);
}
void thread::notify_zcull_info_changed()
{
check_zcull_status(false);
}
void thread::on_notify_memory_mapped(u32 address, u32 size)
{
// In the case where an unmap is followed shortly after by a remap of the same address space
// we must block until RSX has invalidated the memory
// or lock m_mtx_task and do it ourselves
if (!rsx_thread_running)
return;
reader_lock lock(m_mtx_task);
const auto map_range = address_range32::start_length(address, size);
if (!m_invalidated_memory_range.valid())
return;
if (m_invalidated_memory_range.overlaps(map_range))
{
lock.upgrade();
handle_invalidated_memory_range();
}
}
void thread::on_notify_pre_memory_unmapped(u32 address, u32 size, std::vector<std::pair<u64, u64>>& event_data)
{
// Always flush MM if memory mapping is going to change.
rsx::mm_flush();
if (rsx_thread_running && address < rsx::constants::local_mem_base)
{
// Each bit represents io entry to be unmapped
u64 unmap_status[512 / 64]{};
for (u32 ea = address >> 20, end = ea + (size >> 20); ea < end; ea++)
{
const u32 io = std::rotl<u32>(iomap_table.io[ea], 32 - 20);
if (io + 1)
{
unmap_status[io / 64] |= 1ull << (io & 63);
iomap_table.io[ea].release(-1);
iomap_table.ea[io].release(-1);
}
}
auto& cfg = g_fxo->get<gcm_config>();
std::optional<std::unique_lock<shared_mutex>> hle_lock;
for (u32 i = 0; i < std::size(unmap_status); i++)
{
// TODO: Check order when sending multiple events
if (u64 to_unmap = unmap_status[i])
{
if (isHLE)
{
if (!hle_lock)
{
hle_lock = std::unique_lock{cfg.gcmio_mutex};
}
int bit = 0;
while (to_unmap)
{
bit = (std::countr_zero<u64>(std::rotl<u64>(to_unmap, 0 - bit)) + bit);
to_unmap &= ~(1ull << bit);
constexpr u16 null_entry = 0xFFFF;
const u32 ea = std::exchange(cfg.offsetTable.eaAddress[(i * 64 + bit)], null_entry);
if (ea < (rsx::constants::local_mem_base >> 20))
{
cfg.offsetTable.eaAddress[ea] = null_entry;
}
}
continue;
}
// Each 64 entries are grouped by a bit
const u64 io_event = SYS_RSX_EVENT_UNMAPPED_BASE << i;
event_data.emplace_back(io_event, to_unmap);
}
}
if (hle_lock)
{
hle_lock->unlock();
}
// Pause RSX thread momentarily to handle unmapping
eng_lock elock(this);
// Queue up memory invalidation
std::lock_guard lock(m_mtx_task);
const bool existing_range_valid = m_invalidated_memory_range.valid();
const auto unmap_range = address_range32::start_length(address, size);
if (existing_range_valid && m_invalidated_memory_range.touches(unmap_range))
{
// Merge range-to-invalidate in case of consecutive unmaps
m_invalidated_memory_range.set_min_max(unmap_range);
}
else
{
if (existing_range_valid)
{
// We can only delay consecutive unmaps.
// Otherwise, to avoid VirtualProtect failures, we need to do the invalidation here
handle_invalidated_memory_range();
}
m_invalidated_memory_range = unmap_range;
}
m_eng_interrupt_mask |= rsx::memory_config_interrupt;
}
}
void thread::on_notify_post_memory_unmapped(u64 event_data1, u64 event_data2)
{
if (!isHLE)
{
send_event(0, event_data1, event_data2);
}
}
// NOTE: m_mtx_task lock must be acquired before calling this method
void thread::handle_invalidated_memory_range()
{
AUDIT(!m_mtx_task.is_free());
m_eng_interrupt_mask.clear(rsx::memory_config_interrupt);
if (!m_invalidated_memory_range.valid())
{
return;
}
if (is_stopped())
{
// We only need to commit host-resident memory to the guest in case of savestates or captures.
on_invalidate_memory_range(m_invalidated_memory_range, rsx::invalidation_cause::read);
}
// Host sync
rsx::mm_flush();
on_invalidate_memory_range(m_invalidated_memory_range, rsx::invalidation_cause::unmap);
m_invalidated_memory_range.invalidate();
}
void thread::renderctl(u32 request_code, void* /*args*/)
{
switch (request_code)
{
case rsx::mm_backend_ctrl::cmd_mm_flush:
rsx::mm_flush();
break;
default:
fmt::throw_exception("Unknown backend request: 0x%x", request_code);
}
}
//Pause/cont wrappers for FIFO ctrl. Never call this from rsx thread itself!
void thread::pause()
{
external_interrupt_lock++;
while (!external_interrupt_ack && !is_stopped())
{
utils::pause();
}
}
void thread::unpause()
{
// TODO: Clean this shit up
external_interrupt_lock--;
}
void thread::wait_pause()
{
do
{
if (g_cfg.video.multithreaded_rsx)
{
g_fxo->get<rsx::dma_manager>().sync();
}
external_interrupt_ack.store(true);
while (external_interrupt_lock && (cpu_flag::ret - state))
{
// TODO: Investigate non busy-spinning method
utils::pause();
}
external_interrupt_ack.store(false);
}
while (external_interrupt_lock && (cpu_flag::ret - state));
}
u32 thread::get_load()
{
// Average load over around 30 frames
if (!performance_counters.last_update_timestamp || performance_counters.sampled_frames > 30)
{
const auto timestamp = get_system_time();
const auto idle = performance_counters.idle_time.load();
const auto elapsed = timestamp - performance_counters.last_update_timestamp;
if (elapsed > idle)
performance_counters.approximate_load = static_cast<u32>((elapsed - idle) * 100 / elapsed);
else
performance_counters.approximate_load = 0u;
performance_counters.idle_time = 0;
performance_counters.sampled_frames = 0;
performance_counters.last_update_timestamp = timestamp;
}
return performance_counters.approximate_load;
}
void thread::on_frame_end(u32 buffer, bool forced)
{
// Only a frame the GUEST produced counts as the guest making progress.
//
// 'forced' means flip() found nothing queued and synthesised a frame end -- which is what
// a native-UI flip is. check_frame_stall() ARMS native-UI flipping when it reports a
// stall, so counting those frames made the detector disarm itself permanently: the first
// hang of a session switched on a flip source that then refreshed this timestamp forever,
// and no later hang in that session could ever be detected.
//
// Seen on Tales of Xillia 2: a stall was reported at 0:29:06, the game was closed and
// another booted, and when THAT one hung 90 seconds later nothing fired -- guest mutex
// traffic sat at exactly zero for minutes while VKGSRender::flip kept running. Without
// this the white-screen hang produces no dump at all, which is the one case it was
// written for.
if (!forced)
{
g_last_frame_time = get_system_time();
g_frame_stall_reported = false;
// Re-arm the dumps: a real frame landed, so any later stall is a new one worth capturing.
g_frame_stall_dumps = 0;
}
prof::set_enabled(g_cfg.video.rsx_profiler.get());
prof::tick_frame();
bool pause_emulator = false;
// MM sync. This is a pre-emptive operation, so we can use a deferred request.
rsx::mm_flush_lazy();
// Marks the end of a frame scope GPU-side
if (g_user_asked_for_frame_capture.exchange(false) && !capture_current_frame)
{
capture_current_frame = true;
frame_debug.reset();
frame_capture.reset();
// random number just to jumpstart the size
frame_capture.replay_commands.reserve(8000);
// capture first tile state with nop cmd
rsx::frame_capture_data::replay_command replay_cmd;
replay_cmd.rsx_command = std::make_pair(NV4097_NO_OPERATION, 0);
frame_capture.replay_commands.push_back(std::move(replay_cmd));
capture::capture_display_tile_state(this, frame_capture.replay_commands.back());
}
else if (capture_current_frame)
{
capture_current_frame = false;
const std::string file_path = fs::get_config_dir() + "captures/" + (Emu.GetTitleID().empty() ? Emu.GetTitle() : Emu.GetTitleID()) + "_" + date_time::current_time_narrow() + "_capture.rrc.gz";
fs::pending_file temp(file_path);
utils::serial save_manager;
if (temp.file)
{
save_manager.m_file_handler = make_compressed_serialization_file_handler(temp.file);
save_manager(frame_capture);
save_manager.m_file_handler->finalize(save_manager);
if (temp.commit(false))
{
rsx_log.success("Capture successful: %s", file_path);
frame_capture.reset();
pause_emulator = true;
}
else
{
rsx_log.error("Capture failed: %s (%s)", file_path, fs::g_tls_error);
}
}
else
{
rsx_log.fatal("Capture failed: %s (%s)", file_path, fs::g_tls_error);
}
}
if (zcull_ctrl->has_pending())
{
// NOTE: This is a workaround for buggy games.
// Some applications leave the zpass/stats gathering active but don't use the information.
// This can lead to the zcull unit using up all the memory queueing up operations that never get consumed.
// Seen in Diablo III and Yakuza 5
zcull_ctrl->clear(this, CELL_GCM_ZPASS_PIXEL_CNT | CELL_GCM_ZCULL_STATS);
}
// Save current state
m_queued_flip.stats = m_frame_stats;
m_queued_flip.push(buffer);
m_queued_flip.skip_frame = skip_current_frame;
if (!forced) [[likely]]
{
if (!g_cfg.video.disable_FIFO_reordering)
{
// Try to enable FIFO optimizations
// Only rarely useful for some games like RE4
m_flattener.evaluate_performance(m_frame_stats.draw_calls);
}
if (g_cfg.video.frame_skip_enabled)
{
m_skip_frame_ctr++;
if (m_skip_frame_ctr >= g_cfg.video.consecutive_frames_to_draw)
m_skip_frame_ctr = -g_cfg.video.consecutive_frames_to_skip;
skip_current_frame = (m_skip_frame_ctr < 0);
}
}
else
{
if (!g_cfg.video.disable_FIFO_reordering)
{
// Flattener is unusable due to forced random flips
m_flattener.force_disable();
}
if (g_cfg.video.frame_skip_enabled)
{
rsx_log.error("Frame skip is not compatible with this application");
}
}
if (pause_emulator)
{
Emu.Pause();
thread_ctrl::wait_for(30'000);
}
// Reset current stats
m_frame_stats = {};
m_profiler.enabled = !!g_cfg.video.debug_overlay;
}
f64 thread::get_cached_display_refresh_rate()
{
constexpr u64 uses_per_query = 512;
f64 result = m_cached_display_rate;
u64 count = m_display_rate_fetch_count++;
while (true)
{
if (count % 512 == 0)
{
result = get_display_refresh_rate();
m_cached_display_rate.store(result);
m_display_rate_fetch_count += uses_per_query; // Notify users of the new value
break;
}
const u64 new_count = m_display_rate_fetch_count;
const f64 new_cached = m_cached_display_rate;
if (result == new_cached && count / uses_per_query == new_count / uses_per_query)
{
break;
}
// An update might have gone through
count = new_count;
result = new_cached;
}
return result;
}
bool thread::request_emu_flip(u32 buffer)
{
if (is_current_thread()) // requested through command buffer
{
// NOTE: The flip will clear any queued flip requests
handle_emu_flip(buffer);
}
else // requested 'manually' through ppu syscall
{
if (async_flip_requested & flip_request::emu_requested)
{
// ignore multiple requests until previous happens
return true;
}
async_flip_buffer = buffer;
async_flip_requested |= flip_request::emu_requested;
m_eng_interrupt_mask |= rsx::display_interrupt;
if (state & cpu_flag::exit)
{
// Resubmit possibly-ignored flip on savestate load
return false;
}
}
return true;
}
void thread::handle_emu_flip(u32 buffer)
{
if (m_queued_flip.in_progress)
{
// Rescursion not allowed!
return;
}
if (!m_queued_flip.pop(buffer))
{
// Frame was not queued before flipping
on_frame_end(buffer, true);
ensure(m_queued_flip.pop(buffer));
}
double limit = 0.;
const auto frame_limit = g_disable_frame_limit ? frame_limit_type::none : g_cfg.video.frame_limit;
switch (frame_limit)
{
case frame_limit_type::none: limit = g_cfg.core.max_cpu_preempt_count_per_frame ? static_cast<double>(g_cfg.video.vblank_rate) : 0.; break;
case frame_limit_type::_30: limit = 30.; break;
case frame_limit_type::_50: limit = 50.; break;
case frame_limit_type::_60: limit = 60.; break;
case frame_limit_type::_120: limit = 120.; break;
case frame_limit_type::display_rate: limit = get_cached_display_refresh_rate(); break;
case frame_limit_type::_auto: limit = static_cast<double>(g_cfg.video.vblank_rate); break;
case frame_limit_type::_ps3: limit = 0.; break;
case frame_limit_type::infinite: limit = 0.; break;
default:
break;
}
if (double limit2 = g_cfg.video.second_frame_limit; limit2 >= 0.1 && (limit2 < limit || !limit))
{
// Apply a second limit
limit = limit2;
}
if (limit)
{
const u64 needed_us = static_cast<u64>(1000000 / limit);
const u64 time = std::max<u64>(get_system_time(), target_rsx_flip_time > needed_us ? target_rsx_flip_time - needed_us : 0);
if (int_flip_index)
{
if (target_rsx_flip_time > time + 1000)
{
const auto delay_us = target_rsx_flip_time - time;
{
RSX_PROF_SCOPE(idle);
lv2_obj::wait_timeout(delay_us, nullptr, false);
}
performance_counters.idle_time += delay_us;
}
}
target_rsx_flip_time = std::max(time, target_rsx_flip_time) + needed_us;
flip_notification_count = 1;
}
else if (frame_limit == frame_limit_type::_ps3)
{
bool exit = false;
if (vblank_at_flip == umax)
{
vblank_at_flip = +vblank_count;
flip_notification_count = 1;
exit = true;
}
if (requested_vsync && (exit || vblank_at_flip == vblank_count))
{
// Not yet signaled, handle it later
async_flip_requested |= flip_request::emu_requested;
async_flip_buffer = buffer;
return;
}
vblank_at_flip = umax;
}
else
{
flip_notification_count = 1;
}
int_flip_index += flip_notification_count;
current_display_buffer = buffer;
m_queued_flip.emu_flip = true;
m_queued_flip.in_progress = true;
m_queued_flip.skip_frame |= g_cfg.video.disable_video_output && !g_cfg.video.perf_overlay.enabled;
flip(m_queued_flip);
last_guest_flip_timestamp = get_system_time() - 1000000;
flip_status = CELL_GCM_DISPLAY_FLIP_STATUS_DONE;
m_queued_flip.in_progress = false;
while (flip_notification_count--)
{
if (!isHLE)
{
sys_rsx_context_attribute(0x55555555, 0xFEC, buffer, 0, 0, 0);
if (unsent_gcm_events)
{
// TODO: A proper fix
return;
}
continue;
}
if (auto ptr = flip_handler)
{
intr_thread->cmd_list
({
{ ppu_cmd::set_args, 1 }, u64{ 1 },
{ ppu_cmd::lle_call, ptr },
{ ppu_cmd::sleep, 0 }
});
intr_thread->cmd_notify.store(1);
intr_thread->cmd_notify.notify_one();
}
}
}
void thread::evaluate_cpu_usage_reduction_limits()
{
const u64 max_preempt_count = g_cfg.core.max_cpu_preempt_count_per_frame;
if (!max_preempt_count)
{
frame_times.clear();
lv2_obj::set_yield_frequency(0, 0);
return;
}
const u64 current_time = get_system_time();
const u64 current_tsc = utils::get_tsc();
u64 preempt_count = 0;
if (frame_times.size() >= 60)
{
u64 diffs = 0;
for (usz i = 1; i < frame_times.size(); i++)
{
const u64 cur_diff = frame_times[i].timestamp - frame_times[i - 1].timestamp;
diffs += cur_diff;
}
const usz avg_frame_time = diffs / 59;
u32 lowered_delay = 0;
u32 raised_delay = 0;
bool can_reevaluate = true;
u64 prev_preempt_count = umax;
for (usz i = frame_times.size() - 30; i < frame_times.size(); i++)
{
if (prev_preempt_count == umax)
{
prev_preempt_count = frame_times[i].preempt_count;
continue;
}
if (prev_preempt_count != frame_times[i].preempt_count)
{
if (prev_preempt_count > frame_times[i].preempt_count)
{
lowered_delay++;
}
else if (prev_preempt_count < frame_times[i].preempt_count)
{
raised_delay++;
}
if (i > frame_times.size() - 30)
{
// Slow preemption count increase
can_reevaluate = false;
}
}
prev_preempt_count = frame_times[i].preempt_count;
}
preempt_count = std::min<u64>(frame_times.back().preempt_count, max_preempt_count);
u32 fails = 0;
u32 hard_fails = 0;
bool is_last_frame_a_fail = false;
auto abs_dst = [](u64 a, u64 b)
{
return a >= b ? a - b : b - a;
};
for (u32 i = 1; i <= frame_times.size(); i++)
{
const u64 cur_diff = (i == frame_times.size() ? current_time : frame_times[i].timestamp) - frame_times[i - 1].timestamp;
if (const u64 diff_of_diff = abs_dst(cur_diff, avg_frame_time);
diff_of_diff >= avg_frame_time / 7)
{
if (diff_of_diff >= avg_frame_time / 3)
{
raised_delay++;
hard_fails++;
if (i == frame_times.size())
{
is_last_frame_a_fail = true;
}
}
if (fails != umax)
{
fails++;
}
}
}
bool hard_measures_taken = false;
const usz fps_10 = 10'000'000 / avg_frame_time;
auto lower_preemption_count = [&]()
{
if (preempt_count >= 10)
{
preempt_count -= 10;
}
else
{
preempt_count = 0;
}
if ((hard_fails > 2 || fails > 20) && is_last_frame_a_fail)
{
hard_measures_taken = preempt_count > 1;
preempt_count = preempt_count * 7 / 8;
prevent_preempt_increase_tickets = 10;
}
else
{
prevent_preempt_increase_tickets = std::max<u32>(7, prevent_preempt_increase_tickets);
}
};
const u64 vblank_rate_10 = g_cfg.video.vblank_rate * 10;
if (can_reevaluate)
{
const bool is_avg_fps_ok = (abs_dst(fps_10, 300) < 3 || abs_dst(fps_10, 600) < 4 || abs_dst(fps_10, vblank_rate_10) < 4 || abs_dst(fps_10, vblank_rate_10 / 2) < 3);
if (!hard_fails && fails < 6 && is_avg_fps_ok)
{
if (prevent_preempt_increase_tickets)
{
prevent_preempt_increase_tickets--;
}
else
{
preempt_count = std::min<u64>(preempt_count + 4, max_preempt_count);
}
}
else
{
lower_preemption_count();
}
}
// Sudden FPS drop detection
else if ((fails > 13 || hard_fails > 2 || !(abs_dst(fps_10, 300) < 20 || abs_dst(fps_10, 600) < 30 || abs_dst(fps_10, g_cfg.video.vblank_rate * 10) < 30 || abs_dst(fps_10, g_cfg.video.vblank_rate * 10 / 2) < 20)) && lowered_delay < raised_delay && is_last_frame_a_fail)
{
lower_preemption_count();
}
perf_log.trace("CPU preemption control: reeval=%d, preempt_count=%llu, fails=%u, hard=%u, avg_frame_time=%llu, highered=%u, lowered=%u, taken=%u", can_reevaluate, preempt_count, fails, hard_fails, avg_frame_time, raised_delay, lowered_delay, ::g_lv2_preempts_taken.load());
if (hard_measures_taken)
{
preempt_fail_old_preempt_count = std::max<u64>(preempt_fail_old_preempt_count, std::min<u64>(frame_times.back().preempt_count, max_preempt_count));
}
else if (preempt_fail_old_preempt_count)
{
perf_log.error("Lowering current preemption count significantly due to a performance drop, if this issue persists frequently consider lowering max preemptions count to 'new-count' or lower. (old-count=%llu, new-count=%llu)", preempt_fail_old_preempt_count, preempt_count);
preempt_fail_old_preempt_count = 0;
}
const u64 tsc_diff = (current_tsc - frame_times.back().tsc);
const u64 time_diff = (current_time - frame_times.back().timestamp);
const u64 preempt_diff = tsc_diff * (1'000'000 / 30) / (time_diff * std::max<u64>(preempt_count, 1ull));
if (!preempt_count)
{
lv2_obj::set_yield_frequency(0, 0);
}
else if (abs_dst(fps_10, 300) < 30)
{
// Set an upper limit so a backoff technique would be taken if there is a sudden performance drop
// Allow 4% of no yield to reduce significantly the risk of stutter
lv2_obj::set_yield_frequency(preempt_diff, current_tsc + (tsc_diff * (1'000'000 * 96 / (30 * 100)) / time_diff));
}
else if (abs_dst(fps_10, 600) < 40)
{
// 5% for 60fps
lv2_obj::set_yield_frequency(preempt_diff, current_tsc + (tsc_diff * (1'000'000 * 94 / (60 * 100)) / time_diff));
}
else if (abs_dst(fps_10, vblank_rate_10) < 40)
{
lv2_obj::set_yield_frequency(preempt_diff, current_tsc + (tsc_diff * (1'000'000 * 94 / (vblank_rate_10 * 10)) / time_diff));
}
else if (abs_dst(fps_10, vblank_rate_10 / 2) < 30)
{
lv2_obj::set_yield_frequency(preempt_diff, current_tsc + (tsc_diff * (1'000'000 * 96 / ((vblank_rate_10 / 2) * 10)) / time_diff));
}
else
{
// Undetected case, last 12% is with no yield
lv2_obj::set_yield_frequency(preempt_diff, current_tsc + (tsc_diff * 88 / 100));
}
frame_times.pop_front();
}
else
{
lv2_obj::set_yield_frequency(0, 0);
}
frame_times.push_back(frame_time_t{preempt_count, current_time, current_tsc});
}
void vblank_thread::set_thread(std::shared_ptr<named_thread<std::function<void()>>> thread)
{
std::swap(m_thread, thread);
}
vblank_thread& vblank_thread::operator=(thread_state state)
{
if (m_thread)
{
*m_thread = state;
}
return *this;
}
} // namespace rsx