From 1baa4a15cc8d11ae7e16d04bbdef033e8ca5d54b Mon Sep 17 00:00:00 2001 From: Logan McNaughton <848146+loganmc10@users.noreply.github.com> Date: Mon, 2 Nov 2026 13:01:56 +0000 Subject: [PATCH] Improved RDP timing (#1331) --- build.rs | 1 + parallel-rdp/interface.cpp | 163 ++++++++++++++++++++++++++++++++++--- parallel-rdp/interface.hpp | 3 +- src/device/cop0.rs | 1 + src/device/rdp.rs | 17 +++- src/device/rsp_cpu.rs | 7 +- src/ui/video.rs | 8 +- 7 files changed, 183 insertions(+), 17 deletions(-) diff --git a/build.rs b/build.rs index 103c6c91..d16285c8 100644 --- a/build.rs +++ b/build.rs @@ -206,6 +206,7 @@ fn main() { .allowlist_function("rdp_update_screen") .allowlist_function("rdp_render_frame") .allowlist_function("rdp_process_commands") + .allowlist_function("rdp_translate_busy_until") .allowlist_function("rdp_onscreen_message") .allowlist_function("rdp_check_callback") .allowlist_function("rdp_check_framebuffers") diff --git a/parallel-rdp/interface.cpp b/parallel-rdp/interface.cpp index bef4cfe4..1334a207 100644 --- a/parallel-rdp/interface.cpp +++ b/parallel-rdp/interface.cpp @@ -86,7 +86,11 @@ typedef struct { uint32_t cmd_data[0x00040000 >> 2]; int cmd_cur; int cmd_ptr; - uint32_t region; + int32_t scissor_xh, scissor_yh, scissor_xl, scissor_yl; + uint32_t scissor_field; // interlace: only every other line is drawn + uint32_t scissor_keep_odd; // which lines are kept in field mode + uint32_t cycle_type; + uint64_t busy_until; // CPU cycle at which the RDP finishes its queued work FrameBufferInfo frame_buffer_info; } RDP_DEVICE; @@ -854,8 +858,116 @@ uint32_t pixel_size(uint32_t pixel_type, uint32_t area) { } } -uint64_t rdp_process_commands() { +// Rough RDP timing model used to schedule the DP interrupt after SyncFull. +// busy_until tracks when the RDP will finish its queued work, so that work +// overlapping earlier submissions is not counted twice. +// Each primitive costs its (scissor-clipped) pixel count divided by the fill +// rate of the current cycle type, plus a small fixed setup cost. Fill and copy +// modes write 64 bits per clock; 1-cycle mode is ~1 pixel per clock and +// 2-cycle mode ~2 clocks per pixel. Texture loads move 64 bits per clock. +static const uint64_t RDP_PRIMITIVE_SETUP_CLOCKS = 16; +// Each scanline of a texture load or textured rectangle is a separate RDRAM +// transfer, which pays a fixed overhead on top of its data. +static const uint64_t RDP_SCANLINE_CLOCKS = 16; +// SyncLoad / SyncPipe / SyncTile / SyncFull wait for the pipeline to drain. +static const uint64_t RDP_SYNC_CLOCKS = 50; + +// RDP clocks (62.5 MHz) to CPU cycles (93.75 MHz) +static void rdp_add_busy(uint64_t clocks) { + rdp_device.busy_until += clocks * 3 / 2; +} + +static uint64_t rdp_pixel_clocks(uint64_t pixels) { + switch (rdp_device.cycle_type) { + case 0: // 1-cycle + return pixels; + case 1: // 2-cycle + return pixels * 2; + default: { // copy / fill: 64 bits per clock + uint32_t bits = 4u << rdp_device.frame_buffer_info.framebuffer_pixel_size; + return (pixels * bits + 63) / 64; + } + } +} + +// number of scanlines in [top, bottom) that are actually rasterized: in +// interlaced field mode only the odd or even lines are drawn +static uint64_t rdp_scissor_rows(int32_t top, int32_t bottom) { + if (bottom <= top) + return 0; + if (!rdp_device.scissor_field) + return uint64_t(bottom - top); + // count lines y in [top, bottom) with (y & 1) == keep_odd + int32_t p = int32_t(rdp_device.scissor_keep_odd); + auto lines_below = [p](int32_t y) { return (y - p + 1) >> 1; }; + return uint64_t(lines_below(bottom) - lines_below(top)); +} + +// coordinates are in 10.2 (rectangles) or s11.2 (triangle Y) fixed point +static uint64_t rdp_clipped_area(int32_t xh, int32_t yh, int32_t xl, + int32_t yl) { + xh = std::max(xh >> 2, rdp_device.scissor_xh); + yh = std::max(yh >> 2, rdp_device.scissor_yh); + xl = std::min(xl >> 2, rdp_device.scissor_xl); + yl = std::min(yl >> 2, rdp_device.scissor_yl); + if (xl <= xh) + return 0; + return uint64_t(xl - xh) * rdp_scissor_rows(yh, yl); +} + +static uint64_t rdp_rectangle_pixels(uint32_t w1, uint32_t w2) { + int32_t xl = (w1 >> 12) & 0xFFF, yl = w1 & 0xFFF; + int32_t xh = (w2 >> 12) & 0xFFF, yh = w2 & 0xFFF; + if (rdp_device.cycle_type >= 2) { // copy/fill include the last row/column + xl += 4; + yl += 4; + } + return rdp_clipped_area(xh, yh, xl, yl); +} + +// number of scanlines a rectangle covers after scissoring +static uint64_t rdp_rectangle_rows(uint32_t w1, uint32_t w2) { + int32_t yl = w1 & 0xFFF, yh = w2 & 0xFFF; + if (rdp_device.cycle_type >= 2) // copy/fill include the last row + yl += 4; + return rdp_scissor_rows(std::max(yh >> 2, rdp_device.scissor_yh), + std::min(yl >> 2, rdp_device.scissor_yl)); +} + +static int32_t sign_extend_14(uint32_t v) { return int32_t(v << 18) >> 18; } + +// area of a triangle from its edge coefficients: half of height times the +// span width at the middle vertex +static uint64_t rdp_triangle_pixels(const uint32_t *cmd) { + int32_t yl = sign_extend_14(cmd[0]); + int32_t ym = sign_extend_14(cmd[1] >> 16); + int32_t yh = sign_extend_14(cmd[1]); + int32_t xl = int32_t(cmd[2]); + int32_t xh = int32_t(cmd[4]); + int32_t dxhdy = int32_t(cmd[5]); + int64_t major_x_at_ym = int64_t(xh) + int64_t(dxhdy) * (ym - yh) / 4; + int64_t width = std::llabs(major_x_at_ym - xl) >> 16; + int32_t sxh = rdp_device.scissor_xh, sxl = rdp_device.scissor_xl; + width = std::min(width, std::max(sxl - sxh, 0)); + int32_t top = std::max(yh >> 2, rdp_device.scissor_yh); + int32_t bottom = std::min(yl >> 2, rdp_device.scissor_yl); + return uint64_t(width) * rdp_scissor_rows(top, bottom) / 2; +} + +// keep busy_until relative to COUNT when the CPU rewrites COUNT +void rdp_translate_busy_until(uint64_t old_count, uint64_t new_count) { + uint64_t remaining = + rdp_device.busy_until > old_count ? rdp_device.busy_until - old_count : 0; + rdp_device.busy_until = new_count + remaining; +} + +uint64_t rdp_process_commands(uint64_t now) { uint64_t interrupt_timer = 0; + + // the RDP starts on this batch once it has finished any earlier work + if (rdp_device.busy_until < now) + rdp_device.busy_until = now; + const uint32_t DP_CURRENT = *gfx_info.DPC_CURRENT_REG & 0x00FFFFF8; const uint32_t DP_END = *gfx_info.DPC_END_REG & 0x00FFFFF8; @@ -910,6 +1022,35 @@ uint64_t rdp_process_commands() { processor->enqueue_command(cmd_length * 2, &rdp_device.cmd_data[2 * rdp_device.cmd_cur]); + if (command >= 0x26 && command <= 0x29) { // Sync{Load,Pipe,Tile,Full} + rdp_add_busy(RDP_SYNC_CLOCKS); + } else if (command >= 0x08 && command <= 0x0F) { + rdp_add_busy(RDP_PRIMITIVE_SETUP_CLOCKS + + rdp_pixel_clocks(rdp_triangle_pixels( + &rdp_device.cmd_data[2 * rdp_device.cmd_cur]))); + } else if (command == 0x24 || command == 0x25) { // TextureRectangle(Flip) + rdp_add_busy(RDP_PRIMITIVE_SETUP_CLOCKS + + rdp_pixel_clocks(rdp_rectangle_pixels(w1, w2)) + + rdp_rectangle_rows(w1, w2) * RDP_SCANLINE_CLOCKS); + } else if (command == 0x36) { // FillRectangle + rdp_add_busy(RDP_PRIMITIVE_SETUP_CLOCKS + + rdp_pixel_clocks(rdp_rectangle_pixels(w1, w2))); + } else if (command == 0x33 || command == 0x34) { // LoadBlock / LoadTile + uint32_t sl = (w1 >> 12) & 0xFFF, tl = w1 & 0xFFF; + uint32_t sh = (w2 >> 12) & 0xFFF, th = w2 & 0xFFF; + uint64_t texels = command == 0x33 + ? uint64_t(sh >= sl ? sh - sl + 1 : 0) + : uint64_t(sh >= sl ? ((sh - sl) >> 2) + 1 : 0) * + uint64_t(th >= tl ? ((th - tl) >> 2) + 1 : 0); + // LoadBlock is one contiguous transfer; LoadTile fetches row by row + uint64_t rows = command == 0x33 + ? 1 + : uint64_t(th >= tl ? ((th - tl) >> 2) + 1 : 0); + uint32_t bits = 4u << rdp_device.frame_buffer_info.texture_pixel_size; + rdp_add_busy(RDP_PRIMITIVE_SETUP_CLOCKS + (texels * bits + 63) / 64 + + rows * RDP_SCANLINE_CLOCKS); + } + switch (RDP::Op(command)) { case RDP::Op::FillTriangle: case RDP::Op::FillZBufferTriangle: @@ -1029,12 +1170,12 @@ uint64_t rdp_process_commands() { uint32_t upper_left_y = (w1 & 0xFFF) >> 2; uint32_t lower_right_x = ((w2 >> 12) & 0xFFF) >> 2; uint32_t lower_right_y = (w2 & 0xFFF) >> 2; - if (lower_right_x > upper_left_x && lower_right_y > upper_left_y) { - rdp_device.region = - (lower_right_x - upper_left_x) * (lower_right_y - upper_left_y); - } else { - rdp_device.region = 0; - } + rdp_device.scissor_xh = upper_left_x; + rdp_device.scissor_yh = upper_left_y; + rdp_device.scissor_xl = lower_right_x; + rdp_device.scissor_yl = lower_right_y; + rdp_device.scissor_field = (w2 >> 25) & 1; + rdp_device.scissor_keep_odd = (w2 >> 24) & 1; rdp_device.frame_buffer_info.framebuffer_y_offset = upper_left_y; rdp_device.frame_buffer_info.framebuffer_height = @@ -1042,6 +1183,7 @@ uint64_t rdp_process_commands() { } break; case RDP::Op::SetOtherModes: { uint8_t cycle_type = (w1 >> 20) & 3; + rdp_device.cycle_type = cycle_type; uint8_t depth_read_write = (w2 >> 4) & 3; rdp_device.frame_buffer_info.depth_buffer_enabled = ((cycle_type & 2) == 0) && (depth_read_write != 0); @@ -1049,9 +1191,8 @@ uint64_t rdp_process_commands() { case RDP::Op::SyncFull: sync_signal = processor->signal_timeline(); - interrupt_timer = rdp_device.region / 2; - if (interrupt_timer == 0) - interrupt_timer = 5000; + // SyncFull itself adds RDP_SYNC_CLOCKS, so busy_until > now here + interrupt_timer = rdp_device.busy_until - now; break; default: break; diff --git a/parallel-rdp/interface.hpp b/parallel-rdp/interface.hpp index c273d14a..5d603640 100644 --- a/parallel-rdp/interface.hpp +++ b/parallel-rdp/interface.hpp @@ -59,7 +59,8 @@ void rdp_set_vi_register(uint32_t reg, uint32_t value); void rdp_update_screen(); void rdp_render_frame(); CALL_BACK rdp_check_callback(); -uint64_t rdp_process_commands(); +uint64_t rdp_process_commands(uint64_t now); +void rdp_translate_busy_until(uint64_t old_count, uint64_t new_count); void rdp_idle(); void rdp_onscreen_message(const char *message, MESSAGE_LENGTH milliseconds); void rdp_check_framebuffers(uint32_t address, uint32_t length); diff --git a/src/device/cop0.rs b/src/device/cop0.rs index cbd98a13..26312cc1 100644 --- a/src/device/cop0.rs +++ b/src/device/cop0.rs @@ -206,6 +206,7 @@ fn set_control_registers(device: &mut device::Device, index: u32, mut data: u64) data &= 0xFFFFFFFF; data <<= 1; device::events::translate_events(device, device.cpu.cop0.regs[COP0_COUNT_REG], data); + device::rdp::translate_busy_until(device.cpu.cop0.regs[COP0_COUNT_REG], data); device.cpu.cop0.regs[COP0_COUNT_REG] = data; let compare = device.cpu.cop0.regs[COP0_COMPARE_REG]; schedule_compare_event(device, compare); diff --git a/src/device/rdp.rs b/src/device/rdp.rs index fcccb694..ed0afa0e 100644 --- a/src/device/rdp.rs +++ b/src/device/rdp.rs @@ -114,16 +114,29 @@ pub fn write_regs_dpc(device: &mut device::Device, address: u64, value: u32, mas } fn run_rdp(device: &mut device::Device) { - let timer = ui::video::process_rdp_list(); + // The RSP runs a whole burst while COUNT stands still, so commands it + // submits arrive at COUNT plus its progress through the current burst. + let rsp_cycles = if device.rsp.cpu.running { + device::rsp_cpu::elapsed_cpu_cycles(device) + } else { + 0 + }; + let now = device.cpu.cop0.regs[device::cop0::COP0_COUNT_REG] + rsp_cycles; + let timer = ui::video::process_rdp_list(now); device.rdp.regs_dpc[DPC_STATUS_REG] |= DPC_STATUS_START_GCLK | DPC_STATUS_PIPE_BUSY | DPC_STATUS_CMD_BUSY; device.rdp.regs_dpc[DPC_PIPEBUSY_REG] = 0xFFFFFF; if timer != 0 { - device::events::create_event(device, device::events::EVENT_TYPE_DP, timer) + device::events::create_event(device, device::events::EVENT_TYPE_DP, rsp_cycles + timer) } } +// keep the RDP's busy_until relative to COUNT when the CPU rewrites COUNT +pub fn translate_busy_until(old_count: u64, new_count: u64) { + ui::video::translate_rdp_busy_until(old_count, new_count) +} + pub fn read_regs_dps( device: &mut device::Device, address: u64, diff --git a/src/device/rsp_cpu.rs b/src/device/rsp_cpu.rs index 2ff0971f..6b7a7a53 100644 --- a/src/device/rsp_cpu.rs +++ b/src/device/rsp_cpu.rs @@ -188,7 +188,12 @@ pub fn run(device: &mut device::Device) -> u64 { } } device.rsp.cpu.running = false; - (device.rsp.cpu.cycle_counter as f64 * 1.5) as u64 // converting RCP clock to CPU clock + elapsed_cpu_cycles(device) +} + +// RSP cycles elapsed in the current burst, converted from RCP clock to CPU clock +pub fn elapsed_cpu_cycles(device: &device::Device) -> u64 { + device.rsp.cpu.cycle_counter * 3 / 2 } pub fn decode_opcode(device: &device::Device, opcode: u32) -> fn(&mut device::Device, u32) { diff --git a/src/ui/video.rs b/src/ui/video.rs index 74f9ccc6..c6fecc63 100644 --- a/src/ui/video.rs +++ b/src/ui/video.rs @@ -289,8 +289,12 @@ pub fn set_register(reg: u32, value: u32) { } } -pub fn process_rdp_list() -> u64 { - unsafe { rdp_process_commands() } +pub fn process_rdp_list(now: u64) -> u64 { + unsafe { rdp_process_commands(now) } +} + +pub fn translate_rdp_busy_until(old_count: u64, new_count: u64) { + unsafe { rdp_translate_busy_until(old_count, new_count) } } pub fn check_framebuffers(address: u32, length: u32) {