From 1c371cfad7b2d532acbfec0f3422cf8d2826ccca Mon Sep 17 00:00:00 2001 From: jpolo1224 Date: Mon, 10 Aug 2026 00:31:26 -0400 Subject: [PATCH] Count FIFO dispatches, not packets, and stop paying for two hot loads Three separate fixes to the same hot path. The profiler's FIFO figure counted the wrong thing. g_fifo_commands is incremented once per run_FIFO entry, and one of those drains a whole packet, so dividing by it priced a packet rather than a method. Sonic '06 averages about 17 methods per packet, so the reported 612 ns per command was really 612 ns per packet and the per-method cost was closer to 36 ns. Cross-checks: 413.6 FIFO refills a frame at 4096 bytes is 1.69 MB, about 423000 words, which 24248 packets can only consume at roughly 17 words each. Dispatches are now counted where they are dispatched, both figures are reported, and the per-method histogram divides by the right one. The line is labelled packets, and notes that fifo_decode is a catch-all holding every handler body too, since no handler carries its own scope. rsx_state::decode was a cross-TU call per dispatched method. The body is one exchange, but the definition lived in rsx_methods.cpp and LTO is disabled project-wide, so it never inlined, and being opaque it also forced the caller to reload its context pointer afterwards. Moved to the header. set_transform_constant and set_transform_program read ctrl->put through a seq_cst load. That is an ldar on ARM64, on a cache line shared with ctrl->get which the guest PPU writes from another cluster, in the two hottest handlers in this title. Relaxed: a stale value only shrinks the batch, and the remainder is picked up on the next call. --- rpcs3/Emu/RSX/NV47/HW/nv4097.cpp | 1536 +++++++++++++++--------------- rpcs3/Emu/RSX/RSXFIFO.cpp | 9 +- rpcs3/Emu/RSX/rsx_methods.cpp | 5 - rpcs3/Emu/RSX/rsx_methods.h | 10 +- rpcs3/Emu/RSX/rsx_profiler.cpp | 17 +- rpcs3/Emu/RSX/rsx_profiler.h | 9 + 6 files changed, 814 insertions(+), 772 deletions(-) diff --git a/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp b/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp index cf7474177..9088b88c9 100644 --- a/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp +++ b/rpcs3/Emu/RSX/NV47/HW/nv4097.cpp @@ -1,763 +1,773 @@ -#include "stdafx.h" -#include "nv4097.h" -#include "nv47_sync.hpp" - -#include "Emu/RSX/RSXThread.h" -#include "Emu/RSX/Common/BufferUtils.h" - -#define RSX(ctx) ctx->rsxthr -#define REGS(ctx) (&rsx::method_registers) -#define RSX_CAPTURE_EVENT(name) if (RSX(ctx)->capture_current_frame) { RSX(ctx)->capture_frame(name); } - -namespace rsx -{ - namespace nv4097 - { - ///// Program management - - void set_shader_program_dirty(context* ctx, u32, u32) - { - RSX(ctx)->m_graphics_state |= rsx::pipeline_state::fragment_program_ucode_dirty; - } - - void set_transform_constant::decode_one([[maybe_unused]] context* ctx, u32 reg, u32 arg) - { - const u32 index = reg - NV4097_SET_TRANSFORM_CONSTANT; - const u32 constant_id = index / 4; - const u8 subreg = index % 4; - const u32 load = REGS(ctx)->transform_constant_load(); - - REGS(ctx)->transform_constants[load + constant_id][subreg] = arg; - } - - void set_transform_constant::batch_decode(context* ctx, u32 reg, const std::span& args, const std::function& notify) - { - const u32 index = reg - NV4097_SET_TRANSFORM_CONSTANT; - const u32 constant_id = index / 4; - const u8 subreg = index % 4; - const u32 load = REGS(ctx)->transform_constant_load(); - - auto dst = ®S(ctx)->transform_constants[load + constant_id][subreg]; - copy_data_swap_u32(dst, args.data(), ::size32(args)); - - // Notify - const u32 last_constant_id = ((reg + ::size32(args) + 3) - NV4097_SET_TRANSFORM_CONSTANT) / 4; // Aligned div - const u32 load_index = load + constant_id; - const u32 load_count = last_constant_id - constant_id; - - if (!notify || !notify(ctx, load_index, load_count)) - { - RSX(ctx)->patch_transform_constants(ctx, load_index, load_count); - } - } - - void set_transform_constant::impl(context* ctx, u32 reg, [[maybe_unused]] u32 arg) - { - const u32 index = reg - NV4097_SET_TRANSFORM_CONSTANT; - const u32 constant_id = index / 4; - const u8 subreg = index % 4; - - // FIFO args count including this one - const u32 fifo_args_cnt = RSX(ctx)->fifo_ctrl->get_remaining_args_count() + 1; - - // The range of methods this function resposible to - const u32 method_range = 32 - index; - - // Get limit imposed by FIFO PUT (if put is behind get it will result in a number ignored by min) - const u32 fifo_read_limit = static_cast(((RSX(ctx)->ctrl->put & ~3ull) - (RSX(ctx)->fifo_ctrl->get_pos())) / 4); - - const u32 count = std::min({ fifo_args_cnt, fifo_read_limit, method_range }); - - const u32 load = REGS(ctx)->transform_constant_load(); - - u32 rcount = count; - if (const u32 max = (load + constant_id) * 4 + count + subreg, limit = 468 * 4; max > limit) - { - // Ignore addresses outside the usable [0, 467] range - rsx_log.warning("Invalid transform register index (load=%u, index=%u, count=%u)", load, index, count); - - if ((max - count) < limit) - rcount -= max - limit; - else - rcount = 0; - } - - if (rcount == 0) - { - // Out-of-bounds write is a NOP - rsx_log.trace("Out of bounds write for transform constant block."); - RSX(ctx)->fifo_ctrl->skip_methods(fifo_args_cnt - 1); - return; - } - - if (RSX(ctx)->in_begin_end && !REGS(ctx)->current_draw_clause.empty()) - { - // Updating constants mid-draw is messy. Defer the writes - REGS(ctx)->current_draw_clause.insert_command_barrier( - rsx::transform_constant_update_barrier, - RSX(ctx)->fifo_ctrl->get_pos(), - rcount, - reg - NV4097_SET_TRANSFORM_CONSTANT - ); - - RSX(ctx)->fifo_ctrl->skip_methods(rcount - 1); - return; - } - - const auto values = ®S(ctx)->transform_constants[load + constant_id][subreg]; - - const auto fifo_span = RSX(ctx)->fifo_ctrl->get_current_arg_ptr(rcount); - - if (fifo_span.size() < rcount) - { - rcount = ::size32(fifo_span); - } - - if (RSX(ctx)->m_graphics_state & rsx::pipeline_state::transform_constants_dirty) - { - // Minor optimization: don't compare values if we already know we need invalidation - copy_data_swap_u32(values, fifo_span.data(), rcount); - } - else - { - if (copy_data_swap_u32_cmp(values, fifo_span.data(), rcount)) - { - // Transform constants invalidation is expensive (~8k bytes per update) - RSX(ctx)->m_graphics_state |= rsx::pipeline_state::transform_constants_dirty; - } - } - - RSX(ctx)->fifo_ctrl->skip_methods(rcount - 1); - } - - void set_transform_program::impl(context* ctx, u32 reg, u32 /*arg*/) - { - const u32 index = reg - NV4097_SET_TRANSFORM_PROGRAM; - - // FIFO args count including this one - const u32 fifo_args_cnt = RSX(ctx)->fifo_ctrl->get_remaining_args_count() + 1; - - // The range of methods this function resposible to - const u32 method_range = 32 - index; - - // Get limit imposed by FIFO PUT (if put is behind get it will result in a number ignored by min) - const u32 fifo_read_limit = static_cast(((RSX(ctx)->ctrl->put & ~3ull) - (RSX(ctx)->fifo_ctrl->get_pos())) / 4); - - const u32 count = std::min({ fifo_args_cnt, fifo_read_limit, method_range }); - - const u32 load_pos = REGS(ctx)->transform_program_load(); - - u32 rcount = count; - - if (const u32 max = load_pos * 4 + rcount + (index % 4); - max > max_vertex_program_instructions * 4) - { - rsx_log.warning("Program buffer overflow! Attempted to write %u VP instructions.", max / 4); - rcount -= max - (max_vertex_program_instructions * 4); - } - - if (!rcount) - { - // Out-of-bounds write is a NOP - rsx_log.trace("Out of bounds write for transform program block."); - RSX(ctx)->fifo_ctrl->skip_methods(fifo_args_cnt - 1); - return; - } - - const auto fifo_span = RSX(ctx)->fifo_ctrl->get_current_arg_ptr(rcount); - - if (fifo_span.size() < rcount) - { - rcount = ::size32(fifo_span); - } - - const auto out_ptr = ®S(ctx)->transform_program[load_pos * 4 + index % 4]; - - pipeline_state to_set_dirty = rsx::pipeline_state::vertex_program_ucode_dirty; - - if (rcount >= 4 && !RSX(ctx)->m_graphics_state.test(rsx::pipeline_state::vertex_program_ucode_dirty)) - { - // Assume clean - to_set_dirty = {}; - - const usz first_index_off = 0; - const usz second_index_off = (((rcount / 4) - 1) / 2) * 4; - - const u64 src_op1_2 = read_from_ptr>(fifo_span, first_index_off); - const u64 src_op2_2 = read_from_ptr>(fifo_span, second_index_off); - - // Fast comparison - if (src_op1_2 != read_from_ptr_unsafe(out_ptr, first_index_off) || src_op2_2 != read_from_ptr_unsafe(out_ptr, second_index_off)) - { - to_set_dirty = rsx::pipeline_state::vertex_program_ucode_dirty; - } - } - - if (to_set_dirty) - { - copy_data_swap_u32(out_ptr, fifo_span.data(), rcount); - } - else if (copy_data_swap_u32_cmp(out_ptr, fifo_span.data(), rcount)) - { - to_set_dirty = rsx::pipeline_state::vertex_program_ucode_dirty; - } - - RSX(ctx)->m_graphics_state |= to_set_dirty; - REGS(ctx)->transform_program_load_set(load_pos + ((rcount + index % 4) / 4)); - RSX(ctx)->fifo_ctrl->skip_methods(rcount - 1); - } - - ///// Texture management - - ///// Surface management - - void set_surface_dirty_bit(context* ctx, u32 reg, u32 arg) - { - if (arg == REGS(ctx)->latch) - { - return; - } - - switch (reg) - { - case NV4097_SET_SURFACE_COLOR_TARGET: - RSX(ctx)->m_graphics_state |= rsx::pipeline_state::pipeline_config_dirty; - break; - case NV4097_SET_SURFACE_CLIP_VERTICAL: - case NV4097_SET_SURFACE_CLIP_HORIZONTAL: - RSX(ctx)->m_graphics_state |= rsx::pipeline_state::vertex_state_dirty; - break; - default: - break; - } - - RSX(ctx)->m_graphics_state.set(rtt_config_dirty); - RSX(ctx)->m_graphics_state.clear(rtt_config_contested); - } - - void set_surface_format(context* ctx, u32 reg, u32 arg) - { - // The high bits of this register are just log2(dimension), ignore them - if ((arg & 0xFFFF) == (REGS(ctx)->latch & 0xFFFF)) - { - return; - } - - // The important parameters have changed (format, type, antialias) - RSX(ctx)->m_graphics_state |= rsx::pipeline_state::pipeline_config_dirty; - - // Check if we need to also update fragment state - const auto current = REGS(ctx)->decode(arg); - const auto previous = REGS(ctx)->decode(REGS(ctx)->latch); - - if (current.is_integer_color_format() != previous.is_integer_color_format()) // Different ROP emulation - { - RSX(ctx)->m_graphics_state |= rsx::pipeline_state::fragment_program_state_dirty; - } - - if (*current.antialias() != *previous.antialias()) // Antialias control has changed, update ROP parameters - { - RSX(ctx)->m_graphics_state |= rsx::pipeline_state::fragment_state_dirty; - } - - set_surface_dirty_bit(ctx, reg, arg); - } - - void set_surface_options_dirty_bit(context* ctx, u32 reg, u32 arg) - { - if (arg != REGS(ctx)->latch) - { - RSX(ctx)->on_framebuffer_options_changed(reg); - RSX(ctx)->m_graphics_state |= rsx::pipeline_config_dirty; - } - } - - void set_color_mask(context* ctx, u32 reg, u32 arg) - { - if (arg == REGS(ctx)->latch) - { - return; - } - - if (REGS(ctx)->decode(arg).is_invalid()) [[ unlikely ]] - { - // Rollback - REGS(ctx)->decode(reg, REGS(ctx)->latch); - return; - } - - set_surface_options_dirty_bit(ctx, reg, arg); - } - - void set_stencil_op(context* ctx, u32 reg, u32 arg) - { - if (arg == REGS(ctx)->latch) - { - return; - } - - if (to_stencil_op(arg)) [[ likely ]] - { - set_surface_options_dirty_bit(ctx, reg, arg); - return; - } - - // Rollback - REGS(ctx)->decode(reg, REGS(ctx)->latch); - } - - void set_aa_control(context* ctx, u32 /*reg*/, u32 arg) - { - const auto latch = REGS(ctx)->latch; - if (arg == latch) - { - return; - } - - // Reconfigure pipeline. - RSX(ctx)->m_graphics_state |= rsx::pipeline_config_dirty; - - // If we support A2C in hardware, leave the rest upto the hardware. The pipeline config should take care of it. - const auto& backend_config = RSX(ctx)->get_backend_config(); - if (backend_config.supports_hw_a2c && - backend_config.supports_hw_a2c_1spp) - { - return; - } - - // No A2C hardware support or partial hardware support. Invalidate the current program if A2C state changed. - const auto a2c_old = REGS(ctx)->decode(latch).msaa_alpha_to_coverage(); - const auto a2c_new = REGS(ctx)->decode(arg).msaa_alpha_to_coverage(); - if (a2c_old != a2c_new) - { - RSX(ctx)->m_graphics_state |= rsx::fragment_program_state_dirty; - } - } - - ///// Draw call setup (vertex, etc) - - void set_array_element16(context* ctx, u32, u32 arg) - { - if (RSX(ctx)->in_begin_end) - { - RSX(ctx)->GRAPH_frontend().append_array_element(arg & 0xFFFF); - RSX(ctx)->GRAPH_frontend().append_array_element(arg >> 16); - } - } - - void set_array_element32(context* ctx, u32, u32 arg) - { - if (RSX(ctx)->in_begin_end) - RSX(ctx)->GRAPH_frontend().append_array_element(arg); - } - - void draw_arrays(context* /*rsx*/, u32 /*reg*/, u32 arg) - { - REGS(ctx)->current_draw_clause.command = rsx::draw_command::array; - rsx::registers_decoder::decoded_type v(arg); - - REGS(ctx)->current_draw_clause.append(v.start(), v.count()); - } - - void draw_index_array(context* /*rsx*/, u32 /*reg*/, u32 arg) - { - REGS(ctx)->current_draw_clause.command = rsx::draw_command::indexed; - rsx::registers_decoder::decoded_type v(arg); - - REGS(ctx)->current_draw_clause.append(v.start(), v.count()); - } - - void draw_inline_array(context* /*rsx*/, u32 /*reg*/, u32 arg) - { - arg = std::bit_cast>(arg); - REGS(ctx)->current_draw_clause.command = rsx::draw_command::inlined_array; - REGS(ctx)->current_draw_clause.inline_vertex_array.push_back(arg); - } - - void set_transform_program_start(context* ctx, u32 reg, u32) - { - if (REGS(ctx)->registers[reg] != REGS(ctx)->latch) - { - RSX(ctx)->m_graphics_state |= rsx::pipeline_state::vertex_program_ucode_dirty; - } - } - - void set_vertex_attribute_output_mask(context* ctx, u32 reg, u32) - { - if (REGS(ctx)->registers[reg] != REGS(ctx)->latch) - { - RSX(ctx)->m_graphics_state |= rsx::pipeline_state::vertex_program_state_dirty; - } - } - - void set_vertex_base_offset(context* ctx, u32 reg, u32 arg) - { - util::push_draw_parameter_change(ctx, vertex_base_modifier_barrier, reg, arg); - } - - void set_index_base_offset(context* ctx, u32 reg, u32 arg) - { - util::push_draw_parameter_change(ctx, index_base_modifier_barrier, reg, arg); - } - - void check_index_array_dma(context* ctx, u32 reg, u32 arg) - { - // Check if either location or index type are invalid - if (arg & ~(CELL_GCM_LOCATION_MAIN | (CELL_GCM_DRAW_INDEX_ARRAY_TYPE_16 << 4))) - { - // Ignore invalid value, recover - REGS(ctx)->registers[reg] = REGS(ctx)->latch; - RSX(ctx)->recover_fifo(); - - rsx_log.error("Invalid NV4097_SET_INDEX_ARRAY_DMA value: 0x%x", arg); - } - } - - ///// Drawing - - void set_begin_end(context* ctx, u32 /*reg*/, u32 arg) - { - // Ignore upper bits - if (const u8 prim = static_cast(arg)) - { - const auto primitive_type = to_primitive_type(prim); - if (!primitive_type) - { - RSX(ctx)->in_begin_end = true; - - rsx_log.warning("Invalid NV4097_SET_BEGIN_END value: 0x%x", arg); - return; - } - - REGS(ctx)->current_draw_clause.reset(primitive_type); - RSX(ctx)->begin(); - return; - } - - // Check if we have immediate mode vertex data in a driver-local buffer - if (REGS(ctx)->current_draw_clause.command == rsx::draw_command::none) - { - const u32 push_buffer_vertices_count = RSX(ctx)->GRAPH_frontend().get_push_buffer_vertex_count(); - const u32 push_buffer_index_count = RSX(ctx)->GRAPH_frontend().get_push_buffer_index_count(); - - // Need to set this flag since it overrides some register contents - REGS(ctx)->current_draw_clause.is_immediate_draw = true; - - if (push_buffer_index_count) - { - REGS(ctx)->current_draw_clause.command = rsx::draw_command::indexed; - REGS(ctx)->current_draw_clause.append(0, push_buffer_index_count); - } - else if (push_buffer_vertices_count) - { - REGS(ctx)->current_draw_clause.command = rsx::draw_command::array; - REGS(ctx)->current_draw_clause.append(0, push_buffer_vertices_count); - } - } - else - { - REGS(ctx)->current_draw_clause.is_immediate_draw = false; - } - - if (!REGS(ctx)->current_draw_clause.empty()) - { - REGS(ctx)->current_draw_clause.compile(); - - if (g_cfg.video.disable_video_output) - { - RSX(ctx)->execute_nop_draw(); - RSX(ctx)->rsx::thread::end(); - return; - } - - // Notify the backend if the drawing style changes (instanced vs non-instanced) - if (REGS(ctx)->current_draw_clause.is_trivial_instanced_draw != RSX(ctx)->is_current_vertex_program_instanced()) - { - RSX(ctx)->m_graphics_state |= rsx::pipeline_state::xform_instancing_state_dirty; - } - - RSX(ctx)->end(); - } - else - { - RSX(ctx)->in_begin_end = false; - } - - if (RSX(ctx)->pause_on_draw && RSX(ctx)->pause_on_draw.exchange(false)) - { - RSX(ctx)->state -= cpu_flag::dbg_step; - RSX(ctx)->state += cpu_flag::dbg_pause; - RSX(ctx)->check_state(); - } - } - - void clear(context* ctx, u32 /*reg*/, u32 arg) - { - RSX(ctx)->clear_surface(arg); - - RSX_CAPTURE_EVENT("clear"); - } - - void clear_zcull(context* ctx, u32 /*reg*/, u32 /*arg*/) - { - RSX_CAPTURE_EVENT("clear zcull memory"); - } - - void set_face_property(context* ctx, u32 reg, u32 arg) - { - if (arg == REGS(ctx)->latch) - { - return; - } - - bool valid; - switch (reg) - { - case NV4097_SET_CULL_FACE: - valid = !!to_cull_face(arg); break; - case NV4097_SET_FRONT_FACE: - valid = !!to_front_face(arg); break; - default: - valid = false; break; - } - - if (valid) [[ likely ]] - { - RSX(ctx)->m_graphics_state |= rsx::pipeline_config_dirty; - } - else - { - REGS(ctx)->registers[reg] = REGS(ctx)->latch; - } - } - - void set_blend_equation(context* ctx, u32 reg, u32 arg) - { - if (arg == REGS(ctx)->latch) - { - return; - } - - if (to_blend_equation(arg & 0xFFFF) && - to_blend_equation((arg >> 16) & 0xFFFF)) [[ likely ]] - { - RSX(ctx)->m_graphics_state |= rsx::pipeline_config_dirty; - return; - } - - // Rollback - REGS(ctx)->decode(reg, REGS(ctx)->latch); - } - - void set_blend_factor(context* ctx, u32 reg, u32 arg) - { - if (arg == REGS(ctx)->latch) - { - return; - } - - if (to_blend_factor(arg & 0xFFFF) && - to_blend_factor((arg >> 16) & 0xFFFF)) [[ likely ]] - { - RSX(ctx)->m_graphics_state |= rsx::pipeline_config_dirty; - return; - } - - // Rollback - REGS(ctx)->decode(reg, REGS(ctx)->latch); - } - - void set_transform_constant_load(context* ctx, u32 reg, u32 arg) - { - util::push_draw_parameter_change(ctx, rsx::transform_constant_load_modifier_barrier, reg, arg); - } - - ///// Reports - - void get_report(context* ctx, u32 /*reg*/, u32 arg) - { - u8 type = arg >> 24; - u32 offset = arg & 0xffffff; - - auto address_ptr = util::get_report_data_impl(ctx, offset); - if (!address_ptr) - { - rsx_log.error("Bad argument passed to NV4097_GET_REPORT, arg=0x%X", arg); - return; - } - - 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: - RSX(ctx)->get_zcull_stats(type, vm::cast(address_ptr)); - break; - default: - rsx_log.error("NV4097_GET_REPORT: Bad type %d", type); - - vm::_ptr>(address_ptr)->atomic_op([&](CellGcmReportData& data) - { - data.timer = RSX(ctx)->timestamp(); - data.padding = 0; - }); - break; - } - } - - void clear_report_value(context* ctx, u32 /*reg*/, u32 arg) - { - switch (arg) - { - case CELL_GCM_ZPASS_PIXEL_CNT: - case CELL_GCM_ZCULL_STATS: - break; - default: - rsx_log.error("NV4097_CLEAR_REPORT_VALUE: Bad type: %d", arg); - break; - } - - RSX(ctx)->clear_zcull_stats(arg); - } - - void set_render_mode(context* ctx, u32, u32 arg) - { - const u32 mode = arg >> 24; - switch (mode) - { - case 1: - RSX(ctx)->disable_conditional_rendering(); - return; - case 2: - break; - default: - { - struct logged_t - { - atomic_t logged_cause[256]{}; - }; - - const auto& is_error = ::at32(g_fxo->get().logged_cause, mode).try_inc(10); - (is_error ? rsx_log.error : rsx_log.trace)("Unknown render mode %d", mode); - return; - } - } - - const u32 offset = arg & 0xffffff; - auto address_ptr = util::get_report_data_impl(ctx, offset); - - if (!address_ptr) - { - rsx_log.error("Bad argument passed to NV4097_SET_RENDER_ENABLE, arg=0x%X", arg); - return; - } - - // Defer conditional render evaluation - RSX(ctx)->enable_conditional_rendering(vm::cast(address_ptr)); - } - - void set_zcull_render_enable(context* ctx, u32, u32) - { - RSX(ctx)->notify_zcull_info_changed(); - } - - void set_zcull_stats_enable(context* ctx, u32, u32) - { - RSX(ctx)->notify_zcull_info_changed(); - } - - void set_zcull_pixel_count_enable(context* ctx, u32, u32) - { - RSX(ctx)->notify_zcull_info_changed(); - } - - ///// Misc (sync objects, etc) - - void set_notify(context* ctx, u32 /*reg*/, u32 /*arg*/) - { - const u32 location = REGS(ctx)->context_dma_notify(); - const u32 index = (location & 0x7) ^ 0x7; - - if ((location & ~7) != (CELL_GCM_CONTEXT_DMA_NOTIFY_MAIN_0 & ~7)) - { - if (rsx_log.trace) - rsx_log.trace("NV4097_NOTIFY: invalid context = 0x%x", REGS(ctx)->context_dma_notify()); - return; - } - - const u32 addr = RSX(ctx)->iomap_table.get_addr(0xf100000 + (index * 0x40)); - - ensure(addr != umax); - - vm::_ptr>(addr)->store( - { - RSX(ctx)->timestamp(), - 0 - }); - } - - void texture_read_semaphore_release(context* ctx, u32 reg, u32 arg) - { - // Pipeline barrier seems to be equivalent to a SHADER_READ stage barrier. - // Ideally the GPU only needs to have cached all textures declared up to this point before writing the label. - - // lle-gcm likes to inject system reserved semaphores, presumably for system/vsh usage - // Avoid calling render to avoid any havoc(flickering) they may cause from invalid flush/write - const u32 offset = REGS(ctx)->semaphore_offset_4097(); - - if (offset % 16) - { - rsx_log.error("NV4097 semaphore using unaligned offset, recovering. (offset=0x%x)", offset); - RSX(ctx)->recover_fifo(); - return; - } - - const u32 addr = get_address(offset, REGS(ctx)->semaphore_context_dma_4097()); - - if (RSX(ctx)->label_addr >> 28 != addr >> 28) - { - rsx_log.error("NV4097 semaphore unexpected address. Please report to the developers. (offset=0x%x, addr=0x%x)", offset, addr); - } - - if (g_cfg.video.strict_rendering_mode) [[ unlikely ]] - { - util::write_gcm_label(ctx, reg, addr, arg); - } - else - { - util::write_gcm_label(ctx, reg, addr, arg); - } - } - - void back_end_write_semaphore_release(context* ctx, u32 reg, u32 arg) - { - // Full pipeline barrier. GPU must flush pipeline before writing the label - - const u32 offset = REGS(ctx)->semaphore_offset_4097(); - - if (offset % 16) - { - rsx_log.error("NV4097 semaphore using unaligned offset, recovering. (offset=0x%x)", offset); - RSX(ctx)->recover_fifo(); - return; - } - - const u32 addr = get_address(offset, REGS(ctx)->semaphore_context_dma_4097()); - - if (RSX(ctx)->label_addr >> 28 != addr >> 28) - { - rsx_log.error("NV4097 semaphore unexpected address. Please report to the developers. (offset=0x%x, addr=0x%x)", offset, addr); - } - - const u32 val = (arg & 0xff00ff00) | ((arg & 0xff) << 16) | ((arg >> 16) & 0xff); - util::write_gcm_label(ctx, reg, addr, val); - } - - void sync(context* ctx, u32, u32) - { - RSX(ctx)->sync(); - } - } -} +#include "stdafx.h" +#include "nv4097.h" +#include "nv47_sync.hpp" + +#include "Emu/RSX/RSXThread.h" +#include "Emu/RSX/Common/BufferUtils.h" + +#define RSX(ctx) ctx->rsxthr +#define REGS(ctx) (&rsx::method_registers) +#define RSX_CAPTURE_EVENT(name) if (RSX(ctx)->capture_current_frame) { RSX(ctx)->capture_frame(name); } + +namespace rsx +{ + namespace nv4097 + { + ///// Program management + + void set_shader_program_dirty(context* ctx, u32, u32) + { + RSX(ctx)->m_graphics_state |= rsx::pipeline_state::fragment_program_ucode_dirty; + } + + void set_transform_constant::decode_one([[maybe_unused]] context* ctx, u32 reg, u32 arg) + { + const u32 index = reg - NV4097_SET_TRANSFORM_CONSTANT; + const u32 constant_id = index / 4; + const u8 subreg = index % 4; + const u32 load = REGS(ctx)->transform_constant_load(); + + REGS(ctx)->transform_constants[load + constant_id][subreg] = arg; + } + + void set_transform_constant::batch_decode(context* ctx, u32 reg, const std::span& args, const std::function& notify) + { + const u32 index = reg - NV4097_SET_TRANSFORM_CONSTANT; + const u32 constant_id = index / 4; + const u8 subreg = index % 4; + const u32 load = REGS(ctx)->transform_constant_load(); + + auto dst = ®S(ctx)->transform_constants[load + constant_id][subreg]; + copy_data_swap_u32(dst, args.data(), ::size32(args)); + + // Notify + const u32 last_constant_id = ((reg + ::size32(args) + 3) - NV4097_SET_TRANSFORM_CONSTANT) / 4; // Aligned div + const u32 load_index = load + constant_id; + const u32 load_count = last_constant_id - constant_id; + + if (!notify || !notify(ctx, load_index, load_count)) + { + RSX(ctx)->patch_transform_constants(ctx, load_index, load_count); + } + } + + void set_transform_constant::impl(context* ctx, u32 reg, [[maybe_unused]] u32 arg) + { + const u32 index = reg - NV4097_SET_TRANSFORM_CONSTANT; + const u32 constant_id = index / 4; + const u8 subreg = index % 4; + + // FIFO args count including this one + const u32 fifo_args_cnt = RSX(ctx)->fifo_ctrl->get_remaining_args_count() + 1; + + // The range of methods this function resposible to + const u32 method_range = 32 - index; + + // Get limit imposed by FIFO PUT (if put is behind get it will result in a number ignored by min) + // observe(), not the default load: atomic_t reads are seq_cst here, which is an + // ldar on ARM64, and `put` shares a cache line with `get` that the guest PPU writes + // from another cluster. This is one of the two hottest handlers in the FIFO, so the + // barrier was paid thousands of times a frame on a contended line. A stale value is + // harmless: it only ever shrinks the batch, and the remainder is picked up next time. + const u32 fifo_read_limit = static_cast(((RSX(ctx)->ctrl->put.observe() & ~3ull) - (RSX(ctx)->fifo_ctrl->get_pos())) / 4); + + const u32 count = std::min({ fifo_args_cnt, fifo_read_limit, method_range }); + + const u32 load = REGS(ctx)->transform_constant_load(); + + u32 rcount = count; + if (const u32 max = (load + constant_id) * 4 + count + subreg, limit = 468 * 4; max > limit) + { + // Ignore addresses outside the usable [0, 467] range + rsx_log.warning("Invalid transform register index (load=%u, index=%u, count=%u)", load, index, count); + + if ((max - count) < limit) + rcount -= max - limit; + else + rcount = 0; + } + + if (rcount == 0) + { + // Out-of-bounds write is a NOP + rsx_log.trace("Out of bounds write for transform constant block."); + RSX(ctx)->fifo_ctrl->skip_methods(fifo_args_cnt - 1); + return; + } + + if (RSX(ctx)->in_begin_end && !REGS(ctx)->current_draw_clause.empty()) + { + // Updating constants mid-draw is messy. Defer the writes + REGS(ctx)->current_draw_clause.insert_command_barrier( + rsx::transform_constant_update_barrier, + RSX(ctx)->fifo_ctrl->get_pos(), + rcount, + reg - NV4097_SET_TRANSFORM_CONSTANT + ); + + RSX(ctx)->fifo_ctrl->skip_methods(rcount - 1); + return; + } + + const auto values = ®S(ctx)->transform_constants[load + constant_id][subreg]; + + const auto fifo_span = RSX(ctx)->fifo_ctrl->get_current_arg_ptr(rcount); + + if (fifo_span.size() < rcount) + { + rcount = ::size32(fifo_span); + } + + if (RSX(ctx)->m_graphics_state & rsx::pipeline_state::transform_constants_dirty) + { + // Minor optimization: don't compare values if we already know we need invalidation + copy_data_swap_u32(values, fifo_span.data(), rcount); + } + else + { + if (copy_data_swap_u32_cmp(values, fifo_span.data(), rcount)) + { + // Transform constants invalidation is expensive (~8k bytes per update) + RSX(ctx)->m_graphics_state |= rsx::pipeline_state::transform_constants_dirty; + } + } + + RSX(ctx)->fifo_ctrl->skip_methods(rcount - 1); + } + + void set_transform_program::impl(context* ctx, u32 reg, u32 /*arg*/) + { + const u32 index = reg - NV4097_SET_TRANSFORM_PROGRAM; + + // FIFO args count including this one + const u32 fifo_args_cnt = RSX(ctx)->fifo_ctrl->get_remaining_args_count() + 1; + + // The range of methods this function resposible to + const u32 method_range = 32 - index; + + // Get limit imposed by FIFO PUT (if put is behind get it will result in a number ignored by min) + // observe(), not the default load: atomic_t reads are seq_cst here, which is an + // ldar on ARM64, and `put` shares a cache line with `get` that the guest PPU writes + // from another cluster. This is one of the two hottest handlers in the FIFO, so the + // barrier was paid thousands of times a frame on a contended line. A stale value is + // harmless: it only ever shrinks the batch, and the remainder is picked up next time. + const u32 fifo_read_limit = static_cast(((RSX(ctx)->ctrl->put.observe() & ~3ull) - (RSX(ctx)->fifo_ctrl->get_pos())) / 4); + + const u32 count = std::min({ fifo_args_cnt, fifo_read_limit, method_range }); + + const u32 load_pos = REGS(ctx)->transform_program_load(); + + u32 rcount = count; + + if (const u32 max = load_pos * 4 + rcount + (index % 4); + max > max_vertex_program_instructions * 4) + { + rsx_log.warning("Program buffer overflow! Attempted to write %u VP instructions.", max / 4); + rcount -= max - (max_vertex_program_instructions * 4); + } + + if (!rcount) + { + // Out-of-bounds write is a NOP + rsx_log.trace("Out of bounds write for transform program block."); + RSX(ctx)->fifo_ctrl->skip_methods(fifo_args_cnt - 1); + return; + } + + const auto fifo_span = RSX(ctx)->fifo_ctrl->get_current_arg_ptr(rcount); + + if (fifo_span.size() < rcount) + { + rcount = ::size32(fifo_span); + } + + const auto out_ptr = ®S(ctx)->transform_program[load_pos * 4 + index % 4]; + + pipeline_state to_set_dirty = rsx::pipeline_state::vertex_program_ucode_dirty; + + if (rcount >= 4 && !RSX(ctx)->m_graphics_state.test(rsx::pipeline_state::vertex_program_ucode_dirty)) + { + // Assume clean + to_set_dirty = {}; + + const usz first_index_off = 0; + const usz second_index_off = (((rcount / 4) - 1) / 2) * 4; + + const u64 src_op1_2 = read_from_ptr>(fifo_span, first_index_off); + const u64 src_op2_2 = read_from_ptr>(fifo_span, second_index_off); + + // Fast comparison + if (src_op1_2 != read_from_ptr_unsafe(out_ptr, first_index_off) || src_op2_2 != read_from_ptr_unsafe(out_ptr, second_index_off)) + { + to_set_dirty = rsx::pipeline_state::vertex_program_ucode_dirty; + } + } + + if (to_set_dirty) + { + copy_data_swap_u32(out_ptr, fifo_span.data(), rcount); + } + else if (copy_data_swap_u32_cmp(out_ptr, fifo_span.data(), rcount)) + { + to_set_dirty = rsx::pipeline_state::vertex_program_ucode_dirty; + } + + RSX(ctx)->m_graphics_state |= to_set_dirty; + REGS(ctx)->transform_program_load_set(load_pos + ((rcount + index % 4) / 4)); + RSX(ctx)->fifo_ctrl->skip_methods(rcount - 1); + } + + ///// Texture management + + ///// Surface management + + void set_surface_dirty_bit(context* ctx, u32 reg, u32 arg) + { + if (arg == REGS(ctx)->latch) + { + return; + } + + switch (reg) + { + case NV4097_SET_SURFACE_COLOR_TARGET: + RSX(ctx)->m_graphics_state |= rsx::pipeline_state::pipeline_config_dirty; + break; + case NV4097_SET_SURFACE_CLIP_VERTICAL: + case NV4097_SET_SURFACE_CLIP_HORIZONTAL: + RSX(ctx)->m_graphics_state |= rsx::pipeline_state::vertex_state_dirty; + break; + default: + break; + } + + RSX(ctx)->m_graphics_state.set(rtt_config_dirty); + RSX(ctx)->m_graphics_state.clear(rtt_config_contested); + } + + void set_surface_format(context* ctx, u32 reg, u32 arg) + { + // The high bits of this register are just log2(dimension), ignore them + if ((arg & 0xFFFF) == (REGS(ctx)->latch & 0xFFFF)) + { + return; + } + + // The important parameters have changed (format, type, antialias) + RSX(ctx)->m_graphics_state |= rsx::pipeline_state::pipeline_config_dirty; + + // Check if we need to also update fragment state + const auto current = REGS(ctx)->decode(arg); + const auto previous = REGS(ctx)->decode(REGS(ctx)->latch); + + if (current.is_integer_color_format() != previous.is_integer_color_format()) // Different ROP emulation + { + RSX(ctx)->m_graphics_state |= rsx::pipeline_state::fragment_program_state_dirty; + } + + if (*current.antialias() != *previous.antialias()) // Antialias control has changed, update ROP parameters + { + RSX(ctx)->m_graphics_state |= rsx::pipeline_state::fragment_state_dirty; + } + + set_surface_dirty_bit(ctx, reg, arg); + } + + void set_surface_options_dirty_bit(context* ctx, u32 reg, u32 arg) + { + if (arg != REGS(ctx)->latch) + { + RSX(ctx)->on_framebuffer_options_changed(reg); + RSX(ctx)->m_graphics_state |= rsx::pipeline_config_dirty; + } + } + + void set_color_mask(context* ctx, u32 reg, u32 arg) + { + if (arg == REGS(ctx)->latch) + { + return; + } + + if (REGS(ctx)->decode(arg).is_invalid()) [[ unlikely ]] + { + // Rollback + REGS(ctx)->decode(reg, REGS(ctx)->latch); + return; + } + + set_surface_options_dirty_bit(ctx, reg, arg); + } + + void set_stencil_op(context* ctx, u32 reg, u32 arg) + { + if (arg == REGS(ctx)->latch) + { + return; + } + + if (to_stencil_op(arg)) [[ likely ]] + { + set_surface_options_dirty_bit(ctx, reg, arg); + return; + } + + // Rollback + REGS(ctx)->decode(reg, REGS(ctx)->latch); + } + + void set_aa_control(context* ctx, u32 /*reg*/, u32 arg) + { + const auto latch = REGS(ctx)->latch; + if (arg == latch) + { + return; + } + + // Reconfigure pipeline. + RSX(ctx)->m_graphics_state |= rsx::pipeline_config_dirty; + + // If we support A2C in hardware, leave the rest upto the hardware. The pipeline config should take care of it. + const auto& backend_config = RSX(ctx)->get_backend_config(); + if (backend_config.supports_hw_a2c && + backend_config.supports_hw_a2c_1spp) + { + return; + } + + // No A2C hardware support or partial hardware support. Invalidate the current program if A2C state changed. + const auto a2c_old = REGS(ctx)->decode(latch).msaa_alpha_to_coverage(); + const auto a2c_new = REGS(ctx)->decode(arg).msaa_alpha_to_coverage(); + if (a2c_old != a2c_new) + { + RSX(ctx)->m_graphics_state |= rsx::fragment_program_state_dirty; + } + } + + ///// Draw call setup (vertex, etc) + + void set_array_element16(context* ctx, u32, u32 arg) + { + if (RSX(ctx)->in_begin_end) + { + RSX(ctx)->GRAPH_frontend().append_array_element(arg & 0xFFFF); + RSX(ctx)->GRAPH_frontend().append_array_element(arg >> 16); + } + } + + void set_array_element32(context* ctx, u32, u32 arg) + { + if (RSX(ctx)->in_begin_end) + RSX(ctx)->GRAPH_frontend().append_array_element(arg); + } + + void draw_arrays(context* /*rsx*/, u32 /*reg*/, u32 arg) + { + REGS(ctx)->current_draw_clause.command = rsx::draw_command::array; + rsx::registers_decoder::decoded_type v(arg); + + REGS(ctx)->current_draw_clause.append(v.start(), v.count()); + } + + void draw_index_array(context* /*rsx*/, u32 /*reg*/, u32 arg) + { + REGS(ctx)->current_draw_clause.command = rsx::draw_command::indexed; + rsx::registers_decoder::decoded_type v(arg); + + REGS(ctx)->current_draw_clause.append(v.start(), v.count()); + } + + void draw_inline_array(context* /*rsx*/, u32 /*reg*/, u32 arg) + { + arg = std::bit_cast>(arg); + REGS(ctx)->current_draw_clause.command = rsx::draw_command::inlined_array; + REGS(ctx)->current_draw_clause.inline_vertex_array.push_back(arg); + } + + void set_transform_program_start(context* ctx, u32 reg, u32) + { + if (REGS(ctx)->registers[reg] != REGS(ctx)->latch) + { + RSX(ctx)->m_graphics_state |= rsx::pipeline_state::vertex_program_ucode_dirty; + } + } + + void set_vertex_attribute_output_mask(context* ctx, u32 reg, u32) + { + if (REGS(ctx)->registers[reg] != REGS(ctx)->latch) + { + RSX(ctx)->m_graphics_state |= rsx::pipeline_state::vertex_program_state_dirty; + } + } + + void set_vertex_base_offset(context* ctx, u32 reg, u32 arg) + { + util::push_draw_parameter_change(ctx, vertex_base_modifier_barrier, reg, arg); + } + + void set_index_base_offset(context* ctx, u32 reg, u32 arg) + { + util::push_draw_parameter_change(ctx, index_base_modifier_barrier, reg, arg); + } + + void check_index_array_dma(context* ctx, u32 reg, u32 arg) + { + // Check if either location or index type are invalid + if (arg & ~(CELL_GCM_LOCATION_MAIN | (CELL_GCM_DRAW_INDEX_ARRAY_TYPE_16 << 4))) + { + // Ignore invalid value, recover + REGS(ctx)->registers[reg] = REGS(ctx)->latch; + RSX(ctx)->recover_fifo(); + + rsx_log.error("Invalid NV4097_SET_INDEX_ARRAY_DMA value: 0x%x", arg); + } + } + + ///// Drawing + + void set_begin_end(context* ctx, u32 /*reg*/, u32 arg) + { + // Ignore upper bits + if (const u8 prim = static_cast(arg)) + { + const auto primitive_type = to_primitive_type(prim); + if (!primitive_type) + { + RSX(ctx)->in_begin_end = true; + + rsx_log.warning("Invalid NV4097_SET_BEGIN_END value: 0x%x", arg); + return; + } + + REGS(ctx)->current_draw_clause.reset(primitive_type); + RSX(ctx)->begin(); + return; + } + + // Check if we have immediate mode vertex data in a driver-local buffer + if (REGS(ctx)->current_draw_clause.command == rsx::draw_command::none) + { + const u32 push_buffer_vertices_count = RSX(ctx)->GRAPH_frontend().get_push_buffer_vertex_count(); + const u32 push_buffer_index_count = RSX(ctx)->GRAPH_frontend().get_push_buffer_index_count(); + + // Need to set this flag since it overrides some register contents + REGS(ctx)->current_draw_clause.is_immediate_draw = true; + + if (push_buffer_index_count) + { + REGS(ctx)->current_draw_clause.command = rsx::draw_command::indexed; + REGS(ctx)->current_draw_clause.append(0, push_buffer_index_count); + } + else if (push_buffer_vertices_count) + { + REGS(ctx)->current_draw_clause.command = rsx::draw_command::array; + REGS(ctx)->current_draw_clause.append(0, push_buffer_vertices_count); + } + } + else + { + REGS(ctx)->current_draw_clause.is_immediate_draw = false; + } + + if (!REGS(ctx)->current_draw_clause.empty()) + { + REGS(ctx)->current_draw_clause.compile(); + + if (g_cfg.video.disable_video_output) + { + RSX(ctx)->execute_nop_draw(); + RSX(ctx)->rsx::thread::end(); + return; + } + + // Notify the backend if the drawing style changes (instanced vs non-instanced) + if (REGS(ctx)->current_draw_clause.is_trivial_instanced_draw != RSX(ctx)->is_current_vertex_program_instanced()) + { + RSX(ctx)->m_graphics_state |= rsx::pipeline_state::xform_instancing_state_dirty; + } + + RSX(ctx)->end(); + } + else + { + RSX(ctx)->in_begin_end = false; + } + + if (RSX(ctx)->pause_on_draw && RSX(ctx)->pause_on_draw.exchange(false)) + { + RSX(ctx)->state -= cpu_flag::dbg_step; + RSX(ctx)->state += cpu_flag::dbg_pause; + RSX(ctx)->check_state(); + } + } + + void clear(context* ctx, u32 /*reg*/, u32 arg) + { + RSX(ctx)->clear_surface(arg); + + RSX_CAPTURE_EVENT("clear"); + } + + void clear_zcull(context* ctx, u32 /*reg*/, u32 /*arg*/) + { + RSX_CAPTURE_EVENT("clear zcull memory"); + } + + void set_face_property(context* ctx, u32 reg, u32 arg) + { + if (arg == REGS(ctx)->latch) + { + return; + } + + bool valid; + switch (reg) + { + case NV4097_SET_CULL_FACE: + valid = !!to_cull_face(arg); break; + case NV4097_SET_FRONT_FACE: + valid = !!to_front_face(arg); break; + default: + valid = false; break; + } + + if (valid) [[ likely ]] + { + RSX(ctx)->m_graphics_state |= rsx::pipeline_config_dirty; + } + else + { + REGS(ctx)->registers[reg] = REGS(ctx)->latch; + } + } + + void set_blend_equation(context* ctx, u32 reg, u32 arg) + { + if (arg == REGS(ctx)->latch) + { + return; + } + + if (to_blend_equation(arg & 0xFFFF) && + to_blend_equation((arg >> 16) & 0xFFFF)) [[ likely ]] + { + RSX(ctx)->m_graphics_state |= rsx::pipeline_config_dirty; + return; + } + + // Rollback + REGS(ctx)->decode(reg, REGS(ctx)->latch); + } + + void set_blend_factor(context* ctx, u32 reg, u32 arg) + { + if (arg == REGS(ctx)->latch) + { + return; + } + + if (to_blend_factor(arg & 0xFFFF) && + to_blend_factor((arg >> 16) & 0xFFFF)) [[ likely ]] + { + RSX(ctx)->m_graphics_state |= rsx::pipeline_config_dirty; + return; + } + + // Rollback + REGS(ctx)->decode(reg, REGS(ctx)->latch); + } + + void set_transform_constant_load(context* ctx, u32 reg, u32 arg) + { + util::push_draw_parameter_change(ctx, rsx::transform_constant_load_modifier_barrier, reg, arg); + } + + ///// Reports + + void get_report(context* ctx, u32 /*reg*/, u32 arg) + { + u8 type = arg >> 24; + u32 offset = arg & 0xffffff; + + auto address_ptr = util::get_report_data_impl(ctx, offset); + if (!address_ptr) + { + rsx_log.error("Bad argument passed to NV4097_GET_REPORT, arg=0x%X", arg); + return; + } + + 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: + RSX(ctx)->get_zcull_stats(type, vm::cast(address_ptr)); + break; + default: + rsx_log.error("NV4097_GET_REPORT: Bad type %d", type); + + vm::_ptr>(address_ptr)->atomic_op([&](CellGcmReportData& data) + { + data.timer = RSX(ctx)->timestamp(); + data.padding = 0; + }); + break; + } + } + + void clear_report_value(context* ctx, u32 /*reg*/, u32 arg) + { + switch (arg) + { + case CELL_GCM_ZPASS_PIXEL_CNT: + case CELL_GCM_ZCULL_STATS: + break; + default: + rsx_log.error("NV4097_CLEAR_REPORT_VALUE: Bad type: %d", arg); + break; + } + + RSX(ctx)->clear_zcull_stats(arg); + } + + void set_render_mode(context* ctx, u32, u32 arg) + { + const u32 mode = arg >> 24; + switch (mode) + { + case 1: + RSX(ctx)->disable_conditional_rendering(); + return; + case 2: + break; + default: + { + struct logged_t + { + atomic_t logged_cause[256]{}; + }; + + const auto& is_error = ::at32(g_fxo->get().logged_cause, mode).try_inc(10); + (is_error ? rsx_log.error : rsx_log.trace)("Unknown render mode %d", mode); + return; + } + } + + const u32 offset = arg & 0xffffff; + auto address_ptr = util::get_report_data_impl(ctx, offset); + + if (!address_ptr) + { + rsx_log.error("Bad argument passed to NV4097_SET_RENDER_ENABLE, arg=0x%X", arg); + return; + } + + // Defer conditional render evaluation + RSX(ctx)->enable_conditional_rendering(vm::cast(address_ptr)); + } + + void set_zcull_render_enable(context* ctx, u32, u32) + { + RSX(ctx)->notify_zcull_info_changed(); + } + + void set_zcull_stats_enable(context* ctx, u32, u32) + { + RSX(ctx)->notify_zcull_info_changed(); + } + + void set_zcull_pixel_count_enable(context* ctx, u32, u32) + { + RSX(ctx)->notify_zcull_info_changed(); + } + + ///// Misc (sync objects, etc) + + void set_notify(context* ctx, u32 /*reg*/, u32 /*arg*/) + { + const u32 location = REGS(ctx)->context_dma_notify(); + const u32 index = (location & 0x7) ^ 0x7; + + if ((location & ~7) != (CELL_GCM_CONTEXT_DMA_NOTIFY_MAIN_0 & ~7)) + { + if (rsx_log.trace) + rsx_log.trace("NV4097_NOTIFY: invalid context = 0x%x", REGS(ctx)->context_dma_notify()); + return; + } + + const u32 addr = RSX(ctx)->iomap_table.get_addr(0xf100000 + (index * 0x40)); + + ensure(addr != umax); + + vm::_ptr>(addr)->store( + { + RSX(ctx)->timestamp(), + 0 + }); + } + + void texture_read_semaphore_release(context* ctx, u32 reg, u32 arg) + { + // Pipeline barrier seems to be equivalent to a SHADER_READ stage barrier. + // Ideally the GPU only needs to have cached all textures declared up to this point before writing the label. + + // lle-gcm likes to inject system reserved semaphores, presumably for system/vsh usage + // Avoid calling render to avoid any havoc(flickering) they may cause from invalid flush/write + const u32 offset = REGS(ctx)->semaphore_offset_4097(); + + if (offset % 16) + { + rsx_log.error("NV4097 semaphore using unaligned offset, recovering. (offset=0x%x)", offset); + RSX(ctx)->recover_fifo(); + return; + } + + const u32 addr = get_address(offset, REGS(ctx)->semaphore_context_dma_4097()); + + if (RSX(ctx)->label_addr >> 28 != addr >> 28) + { + rsx_log.error("NV4097 semaphore unexpected address. Please report to the developers. (offset=0x%x, addr=0x%x)", offset, addr); + } + + if (g_cfg.video.strict_rendering_mode) [[ unlikely ]] + { + util::write_gcm_label(ctx, reg, addr, arg); + } + else + { + util::write_gcm_label(ctx, reg, addr, arg); + } + } + + void back_end_write_semaphore_release(context* ctx, u32 reg, u32 arg) + { + // Full pipeline barrier. GPU must flush pipeline before writing the label + + const u32 offset = REGS(ctx)->semaphore_offset_4097(); + + if (offset % 16) + { + rsx_log.error("NV4097 semaphore using unaligned offset, recovering. (offset=0x%x)", offset); + RSX(ctx)->recover_fifo(); + return; + } + + const u32 addr = get_address(offset, REGS(ctx)->semaphore_context_dma_4097()); + + if (RSX(ctx)->label_addr >> 28 != addr >> 28) + { + rsx_log.error("NV4097 semaphore unexpected address. Please report to the developers. (offset=0x%x, addr=0x%x)", offset, addr); + } + + const u32 val = (arg & 0xff00ff00) | ((arg & 0xff) << 16) | ((arg >> 16) & 0xff); + util::write_gcm_label(ctx, reg, addr, val); + } + + void sync(context* ctx, u32, u32) + { + RSX(ctx)->sync(); + } + } +} diff --git a/rpcs3/Emu/RSX/RSXFIFO.cpp b/rpcs3/Emu/RSX/RSXFIFO.cpp index fc1d1078e..413dc8a42 100644 --- a/rpcs3/Emu/RSX/RSXFIFO.cpp +++ b/rpcs3/Emu/RSX/RSXFIFO.cpp @@ -912,7 +912,14 @@ namespace rsx m_ctx->register_state->decode(reg, value); - if (rsx::prof::enabled()) [[unlikely]] rsx::prof::g_method_counts[reg & (rsx::prof::method_slot_count - 1)]++; + if (rsx::prof::enabled()) [[unlikely]] + { + // The sequential counter is what the per-method figure should divide by. + // g_fifo_commands is incremented once per run_FIFO entry, which is a whole + // packet, so it prices packets. + rsx::prof::g_fifo_dispatches++; + rsx::prof::g_method_counts[reg & (rsx::prof::method_slot_count - 1)]++; + } if (auto method = methods[reg]) { diff --git a/rpcs3/Emu/RSX/rsx_methods.cpp b/rpcs3/Emu/RSX/rsx_methods.cpp index 03bbd1e52..894dafbc6 100644 --- a/rpcs3/Emu/RSX/rsx_methods.cpp +++ b/rpcs3/Emu/RSX/rsx_methods.cpp @@ -1198,11 +1198,6 @@ namespace rsx registers[NV406E_SEMAPHORE_OFFSET] = 0x10; } - void rsx_state::decode(u32 reg, u32 value) - { - // Store new value and save previous - latch = std::exchange(registers[reg], value); - } bool rsx_state::test(u32 reg, u32 value) const { diff --git a/rpcs3/Emu/RSX/rsx_methods.h b/rpcs3/Emu/RSX/rsx_methods.h index 8e1dd698f..46939847e 100644 --- a/rpcs3/Emu/RSX/rsx_methods.h +++ b/rpcs3/Emu/RSX/rsx_methods.h @@ -173,7 +173,15 @@ namespace rsx ~rsx_state() = default; - void decode(u32 reg, u32 value); + // Inlined deliberately. run_FIFO calls this once per dispatched method and the body + // is a single exchange, but with the definition in rsx_methods.cpp and LTO disabled + // project-wide it compiled to a real cross-TU call, which also forced the caller to + // reload its context pointer afterwards because the call is opaque. + void decode(u32 reg, u32 value) + { + // Store new value and save previous + latch = std::exchange(registers[reg], value); + } bool test(u32 reg, u32 value) const; diff --git a/rpcs3/Emu/RSX/rsx_profiler.cpp b/rpcs3/Emu/RSX/rsx_profiler.cpp index 2912b2929..19b5935fe 100644 --- a/rpcs3/Emu/RSX/rsx_profiler.cpp +++ b/rpcs3/Emu/RSX/rsx_profiler.cpp @@ -19,6 +19,7 @@ namespace rsx::prof const void* g_owner_thread = nullptr; u64 g_fifo_refills = 0; u64 g_fifo_commands = 0; + u64 g_fifo_dispatches = 0; u32 g_method_counts[method_slot_count] = {}; u64 g_fifo_refill_bytes = 0; u64 g_fifo_refill_stalls = 0; @@ -255,9 +256,20 @@ namespace rsx::prof // bucket with no owner left in it. const u64 decode_ticks = g_acc.ticks[static_cast(bucket::fifo_decode)]; - fmt::append(report, "\n\tFIFO commands %.0f/frame, %.0f ns each in FIFO decode", + // Labelled packets, because that is what it counts. fifo_decode is also a + // catch-all holding every method handler body, since no handler carries its + // own scope, so neither figure is dispatch overhead alone. + fmt::append(report, "\n\tFIFO packets %.0f/frame, %.0f ns each in FIFO decode", static_cast(g_fifo_commands) / frames, static_cast(decode_ticks) * to_ms * 1'000'000.0 / static_cast(g_fifo_commands)); + + if (g_fifo_dispatches) + { + fmt::append(report, "\n\tFIFO dispatches %.0f/frame, %.1f/packet, %.0f ns each", + static_cast(g_fifo_dispatches) / frames, + static_cast(g_fifo_dispatches) / static_cast(g_fifo_commands), + static_cast(decode_ticks) * to_ms * 1'000'000.0 / static_cast(g_fifo_dispatches)); + } } { @@ -284,7 +296,7 @@ namespace rsx::prof fmt::append(list, "\n\t %-46s %8.0f/frame %4.1f%%", name.empty() ? fmt::format("0x%05x", slot << 2) : std::string(name), static_cast(count) / frames, - static_cast(count) * 100.0 / static_cast(g_fifo_commands)); + static_cast(count) * 100.0 / static_cast(g_fifo_dispatches ? g_fifo_dispatches : g_fifo_commands)); } fmt::append(report, "\n\ttop methods%s", list); } @@ -293,6 +305,7 @@ namespace rsx::prof prof_log.success("%s", report); g_fifo_commands = 0; + g_fifo_dispatches = 0; std::fill(std::begin(g_method_counts), std::end(g_method_counts), 0u); g_fifo_refills = 0; g_fifo_refill_bytes = 0; diff --git a/rpcs3/Emu/RSX/rsx_profiler.h b/rpcs3/Emu/RSX/rsx_profiler.h index 1dd8627a2..47a33e830 100644 --- a/rpcs3/Emu/RSX/rsx_profiler.h +++ b/rpcs3/Emu/RSX/rsx_profiler.h @@ -189,6 +189,15 @@ namespace rsx::prof */ extern u64 g_fifo_commands; + /** + * Methods actually dispatched, as opposed to [g_fifo_commands], which counts entries + * into run_FIFO. One of those drains a WHOLE packet, so dividing by it prices a packet + * and not a method. Sonic '06 averages roughly 17 methods per packet, so the two differ + * by more than an order of magnitude and the per-packet figure was being read as a + * per-command one. + */ + extern u64 g_fifo_dispatches; + /** * Commands seen per RSX method register, indexed by (id >> 2). *