From 5cda8cf9ccc762d128df474b89994aa091c05ab3 Mon Sep 17 00:00:00 2001 From: Pegasus Date: Wed, 9 Sep 2026 10:36:36 +0300 Subject: [PATCH 1/4] Integrate qualified TAS timing and execution profile --- docs/TAS_ACCURACY.md | 66 ++ recompiler/CMakeLists.txt | 27 +- recompiler/src/code_generator.cpp | 16 +- recompiler/src/control_flow.cpp | 9 + recompiler/src/full_function_emitter.cpp | 99 ++- .../tests/full_function_emitter_test.cpp | 184 +++++- recompiler/tests/recompiler_patch_test.cpp | 72 +++ .../tests/test_aot_overlay_discovery.py | 35 +- runtime/CMakeLists.txt | 85 +++ runtime/check_test_registration.cmake | 33 + runtime/include/cdrom.h | 7 + runtime/include/cdrom_random_tape.h | 70 +++ runtime/include/debug_server.h | 9 + runtime/include/dma.h | 14 +- runtime/include/event_ring.h | 4 + runtime/include/gpu.h | 5 + runtime/include/gpu_render.h | 8 + runtime/include/gpu_sw_renderer.h | 6 + runtime/include/input_instruction_histogram.h | 13 + .../input_instruction_histogram_impl.h | 151 +++++ runtime/include/input_route_field_clock.h | 43 ++ runtime/include/input_route_file.h | 52 ++ runtime/include/input_route_observer.h | 11 + runtime/include/input_route_raster_clock.h | 108 ++++ runtime/include/input_update_clock.h | 134 ++++ runtime/include/input_update_replay.h | 223 +++++++ runtime/include/interrupts.h | 15 + runtime/include/overlay_api.h | 8 +- .../include/overlay_dispatch_preamble.c.inc | 12 + runtime/include/psx_cyc.h | 12 +- runtime/include/psx_icache.h | 34 +- runtime/include/psx_memory.h | 4 + runtime/include/source_cpu_block_bound.h | 48 ++ runtime/include/source_cpu_boundary_probe.h | 59 ++ .../include/source_gpu_command_projection.h | 290 +++++++++ .../include/source_gpu_polygon_projection.h | 76 +++ runtime/include/source_gpu_runtime.h | 28 + runtime/include/source_gpu_service_clock.h | 95 +++ runtime/include/source_gpu_texture.h | 17 + runtime/include/source_ram_page_probe.h | 66 ++ runtime/include/timer1_source_clock.h | 73 +++ runtime/include/timer2_source_clock.h | 78 +++ runtime/include/timers.h | 6 + runtime/runtime.cmake | 2 + runtime/src/cdrom.c | 437 +++++++++++-- runtime/src/debug_server.c | 129 +++- runtime/src/dirty_ram_interp.c | 354 +++++++++-- runtime/src/dma.c | 595 +++++++++++++++++- runtime/src/event_ring.c | 12 +- runtime/src/gpu.c | 160 ++++- runtime/src/gpu_render.c | 11 + runtime/src/gpu_sw_renderer.c | 183 +++++- runtime/src/input_route_observer.c | 376 +++++++++++ runtime/src/interrupts.c | 266 ++++++-- runtime/src/main.cpp | 27 + runtime/src/memory.c | 188 ++++-- runtime/src/overlay_loader.c | 4 + runtime/src/psx_cycles.c | 9 + runtime/src/psx_icache.c | 27 + runtime/src/sio.c | 90 ++- runtime/src/source_gpu_runtime.c | 128 ++++ runtime/src/spu.c | 253 +++++++- runtime/src/timers.c | 121 ++++ runtime/src/traps.c | 34 + runtime/tests/test_bios_alias_admission.c | 40 ++ runtime/tests/test_bios_syscall_resume.c | 50 ++ runtime/tests/test_cdrom_boot_model.c | 98 +++ runtime/tests/test_cdrom_explicit_seek.c | 116 ++++ runtime/tests/test_cdrom_implicit_seek.c | 153 +++++ runtime/tests/test_cdrom_random_tape.c | 45 ++ runtime/tests/test_cdrom_source_clock.c | 111 ++++ .../tests/test_cdrom_source_explicit_timing.c | 106 ++++ runtime/tests/test_cdrom_source_reset.c | 93 +++ runtime/tests/test_compiled_dma_read_wait.c | 35 ++ runtime/tests/test_compiled_ram_read_watch.c | 38 ++ runtime/tests/test_cpu_step_boundary.c | 39 ++ runtime/tests/test_critical_exception.c | 55 ++ runtime/tests/test_dma_cd_deadline.c | 91 +++ runtime/tests/test_dma_cd_source.c | 135 ++++ runtime/tests/test_dma_completion_deadline.c | 155 +++++ .../tests/test_dma_gpu_linked_list_source.c | 140 +++++ runtime/tests/test_dma_gpu_upload_source.c | 153 +++++ runtime/tests/test_dma_otc_timing.c | 57 ++ runtime/tests/test_gpu_frame_retirement.c | 128 ++++ runtime/tests/test_icache_isolated_store.c | 39 ++ runtime/tests/test_input_histogram_start.c | 30 + .../tests/test_input_instruction_histogram.c | 104 +++ runtime/tests/test_input_route_field_clock.c | 33 + runtime/tests/test_input_route_file.c | 49 ++ runtime/tests/test_input_route_observer.c | 72 +++ runtime/tests/test_input_route_observer.py | 57 ++ runtime/tests/test_input_route_raster_clock.c | 93 +++ runtime/tests/test_input_update_clock.c | 205 ++++++ runtime/tests/test_irq_cache_block.c | 78 +++ runtime/tests/test_irq_prefetch.c | 63 ++ runtime/tests/test_irq_recognition.c | 89 +++ runtime/tests/test_irq_vector_entry.c | 39 ++ runtime/tests/test_memory_dma_read_wait.c | 41 ++ runtime/tests/test_mmio_read_sample_order.c | 113 ++++ runtime/tests/test_muldiv_deferred.c | 43 ++ runtime/tests/test_overlay_step_boundary.py | 100 +++ .../tests/test_psx_cycle_event_boundaries.c | 2 + runtime/tests/test_release_zip.py | 2 +- runtime/tests/test_sio_ack_timing.c | 94 +++ runtime/tests/test_sio_card_repair.c | 26 + runtime/tests/test_source_cpu_block_bound.c | 36 ++ .../test_source_gpu_command_projection.c | 140 +++++ runtime/tests/test_source_gpu_cpu.c | 82 +++ runtime/tests/test_source_gpu_dma_runtime.c | 37 ++ runtime/tests/test_source_gpu_environment.c | 39 ++ runtime/tests/test_source_gpu_frame_request.c | 28 + runtime/tests/test_source_gpu_pending_frame.c | 37 ++ .../tests/test_source_gpu_reset_projection.c | 23 + runtime/tests/test_source_gpu_runtime.c | 90 +++ runtime/tests/test_source_gpu_service_clock.c | 78 +++ runtime/tests/test_spu_status_scheduler.c | 90 +++ runtime/tests/test_spu_status_visibility.c | 50 ++ .../tests/test_timer1_raster_integration.c | 46 ++ runtime/tests/test_timer1_source_clock.c | 28 + runtime/tests/test_timer2_divider_native.c | 14 + runtime/tests/test_timer2_source_clock.c | 18 + runtime/tests/test_timer2_source_scheduler.c | 56 ++ tools/tasreplays/tests/CMakeLists.txt | 26 + 123 files changed, 9658 insertions(+), 281 deletions(-) create mode 100644 docs/TAS_ACCURACY.md create mode 100644 runtime/include/cdrom_random_tape.h create mode 100644 runtime/include/input_instruction_histogram.h create mode 100644 runtime/include/input_instruction_histogram_impl.h create mode 100644 runtime/include/input_route_field_clock.h create mode 100644 runtime/include/input_route_file.h create mode 100644 runtime/include/input_route_observer.h create mode 100644 runtime/include/input_route_raster_clock.h create mode 100644 runtime/include/input_update_clock.h create mode 100644 runtime/include/input_update_replay.h create mode 100644 runtime/include/source_cpu_block_bound.h create mode 100644 runtime/include/source_cpu_boundary_probe.h create mode 100644 runtime/include/source_gpu_command_projection.h create mode 100644 runtime/include/source_gpu_polygon_projection.h create mode 100644 runtime/include/source_gpu_runtime.h create mode 100644 runtime/include/source_gpu_service_clock.h create mode 100644 runtime/include/source_gpu_texture.h create mode 100644 runtime/include/source_ram_page_probe.h create mode 100644 runtime/include/timer1_source_clock.h create mode 100644 runtime/include/timer2_source_clock.h create mode 100644 runtime/src/input_route_observer.c create mode 100644 runtime/src/source_gpu_runtime.c create mode 100644 runtime/tests/test_bios_alias_admission.c create mode 100644 runtime/tests/test_bios_syscall_resume.c create mode 100644 runtime/tests/test_cdrom_boot_model.c create mode 100644 runtime/tests/test_cdrom_explicit_seek.c create mode 100644 runtime/tests/test_cdrom_implicit_seek.c create mode 100644 runtime/tests/test_cdrom_random_tape.c create mode 100644 runtime/tests/test_cdrom_source_clock.c create mode 100644 runtime/tests/test_cdrom_source_explicit_timing.c create mode 100644 runtime/tests/test_cdrom_source_reset.c create mode 100644 runtime/tests/test_compiled_dma_read_wait.c create mode 100644 runtime/tests/test_compiled_ram_read_watch.c create mode 100644 runtime/tests/test_cpu_step_boundary.c create mode 100644 runtime/tests/test_critical_exception.c create mode 100644 runtime/tests/test_dma_cd_deadline.c create mode 100644 runtime/tests/test_dma_cd_source.c create mode 100644 runtime/tests/test_dma_completion_deadline.c create mode 100644 runtime/tests/test_dma_gpu_linked_list_source.c create mode 100644 runtime/tests/test_dma_gpu_upload_source.c create mode 100644 runtime/tests/test_dma_otc_timing.c create mode 100644 runtime/tests/test_gpu_frame_retirement.c create mode 100644 runtime/tests/test_icache_isolated_store.c create mode 100644 runtime/tests/test_input_histogram_start.c create mode 100644 runtime/tests/test_input_instruction_histogram.c create mode 100644 runtime/tests/test_input_route_field_clock.c create mode 100644 runtime/tests/test_input_route_file.c create mode 100644 runtime/tests/test_input_route_observer.c create mode 100644 runtime/tests/test_input_route_observer.py create mode 100644 runtime/tests/test_input_route_raster_clock.c create mode 100644 runtime/tests/test_input_update_clock.c create mode 100644 runtime/tests/test_irq_cache_block.c create mode 100644 runtime/tests/test_irq_prefetch.c create mode 100644 runtime/tests/test_irq_recognition.c create mode 100644 runtime/tests/test_irq_vector_entry.c create mode 100644 runtime/tests/test_memory_dma_read_wait.c create mode 100644 runtime/tests/test_mmio_read_sample_order.c create mode 100644 runtime/tests/test_muldiv_deferred.c create mode 100644 runtime/tests/test_overlay_step_boundary.py create mode 100644 runtime/tests/test_sio_ack_timing.c create mode 100644 runtime/tests/test_sio_card_repair.c create mode 100644 runtime/tests/test_source_cpu_block_bound.c create mode 100644 runtime/tests/test_source_gpu_command_projection.c create mode 100644 runtime/tests/test_source_gpu_cpu.c create mode 100644 runtime/tests/test_source_gpu_dma_runtime.c create mode 100644 runtime/tests/test_source_gpu_environment.c create mode 100644 runtime/tests/test_source_gpu_frame_request.c create mode 100644 runtime/tests/test_source_gpu_pending_frame.c create mode 100644 runtime/tests/test_source_gpu_reset_projection.c create mode 100644 runtime/tests/test_source_gpu_runtime.c create mode 100644 runtime/tests/test_source_gpu_service_clock.c create mode 100644 runtime/tests/test_spu_status_scheduler.c create mode 100644 runtime/tests/test_spu_status_visibility.c create mode 100644 runtime/tests/test_timer1_raster_integration.c create mode 100644 runtime/tests/test_timer1_source_clock.c create mode 100644 runtime/tests/test_timer2_divider_native.c create mode 100644 runtime/tests/test_timer2_source_clock.c create mode 100644 runtime/tests/test_timer2_source_scheduler.c create mode 100644 tools/tasreplays/tests/CMakeLists.txt diff --git a/docs/TAS_ACCURACY.md b/docs/TAS_ACCURACY.md new file mode 100644 index 000000000..5ae509152 --- /dev/null +++ b/docs/TAS_ACCURACY.md @@ -0,0 +1,66 @@ +# TAS accuracy profile and Tekken 3 validation + +The `tasreplays` branch integrates the source and code-generation corrections +qualified during the September2026 Tekken 3 TAS campaign. The reproducible +entry point is [tools/tasreplays](../tools/tasreplays/README.md). +It builds on `f23c5ba1a220fe1ca8818cc48c026d6c2f7f2c64`, the base used for the +measured campaign, with the final native integration's overlay ABI22. + +The unchanged Spikestuff movie contains7,974 inputs. The integrated native +build wins Arcade at8.80 seconds. Every original-input return clock and all512 +4KiB RAM-page hashes match the independent Octoshock2.2.2 reference;28 full +2MiB snapshots are also byte-identical. This is stronger evidence for this +route than a clean process exit or simply consuming the input file. It is not +a proof of complete hardware or cross-title compatibility. + +## Corrections integrated + +Related corrections are grouped here by their shared mechanism. Snapshot +numbers in the original research included diagnostics and experiments; they +are not bug counts. + +| Area | Corrected mechanisms | +|---|---| +| CPU/code generation | Uncached instruction fetch costs; copied BIOS/RAM alias admission and syscall EPC; exception transfer and post-SYS continuation; deferred multiply deadlines; isolated cache-tag stores; ROM/RAM precision guards and conservative block bounds; pending-load ownership across compiled/interpreted boundaries; signed arithmetic exceptions; GTE load cancellation, delayed register values, and COP2 interrupt ordering. | +| Interrupts | Execute the actual vector instructions; interrupted fetch and branch-delay context; EPC/Cause/BD/BT/TAR ownership; IRQ reentry/cooldown and repeated delivery after RFE; Cause CE bits; register read image and sampling; recognize IRQs after DMA halt; VBlank latching during a guest IRQ handler. | +| DMA | Live request/linked-list GPU transfer; upload/readback readiness and word ownership; cancellation at packet boundaries; initial credit and register-write order; OTC/CD CPU stalls and instruction fetch overlap; completion deadline invalidation; fractional-service/frontend ordering; SPU DMA initial/word/block timing. | +| GPU | One FIFO/service owner for command admission, dispatch and visible effects; phase-correct triangle rendering and queued environment state; A0/C0 transfers; polygons, textures/CLUT/cache, shading, masks and blending; fill/copy/rectangles; clipping and VRAM wrapping; field-aware row work; raster/status timing and automatic frame-return ownership. | +| CD-ROM | Implicit and explicit seek transitions; asynchronous sector IRQ presentation; firmware/cold tray state; TOC/seek/read pipeline and command clocks; reset; Pause ACK and head rewind/resume; trigger/read-head state. | +| Timers and SIO | Source raster field clock; timer1 HBlank and read sample; timer2 divider/IRQ/deadline; digital-pad ACK pulse; opt-in handling of an unrelated legacy card repair. | +| SPU | Delayed control/status visibility with independent sample service; key-on/envelope phase; ADPCM decode queue, END flags, pitch/filter state and guest register readback. | +| Overlay integration | ABI22 forwards live instruction-boundary callbacks and pending cycle ownership through generated DLLs, preserving replay exclusion and rejecting stale ABI caches. | + +Most detailed timing changes are enabled by explicit `octoshock-2.2.2` profile +options; they model the exact emulator on which this TAS was authored. +General CPU/controller defects also have source-level corrections. The profile +does not contain a title-name switch, a precomputed game-state stream, or +delays chosen to force an opponent or victory. + +The reference is the immutable +[BizHawk source commit](https://github.com/TASEmulators/BizHawk/tree/519e14aa1ad7a9d6df2edc7808c5ed687dfee046), +observed using ordinary authored guest programs and original movie playback. +The native CPU, devices and renderer execute independently. The isolated +external raw PRNG utility has a separate GPL license and file interface; +its arithmetic is not imported into the framework runtime. + +## Reproduction and validation boundaries + +The recipe generates the BIOS and title from owned assets using the repaired +emitters. All57 resulting C files must match the normalized fingerprints of the +verified integration. It builds dependencies from pinned archives and uses +software rendering in both visible and headless modes. Recompiler and authored +O0/O2 tests run without retail assets. The final replay verifier rejects an +input mismatch, a missing or reordered checkpoint, a different RAM-page hash, +a return-clock difference, or incomplete termination. + +Historical C fixtures that require retained original-core captures are still +identified as fixtures by the registration checker. Their old private driver +names describe provenance, not a claim that those drivers ship in this branch. +Portable CTest registrations and the complete retail replay are the repeatable +checks available from the clone. + +The ending is explicitly bounded at8,400 frames:7,974 original inputs plus426 +neutral inputs, through the visible victory. Later CDDA Play seek remains +unqualified and stopped the research tail after return10,704. PAL, save-state +restoration, other input devices, other titles, complete audio/pixel equality, +and unrestricted post-victory playback are outside this qualification. diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index 410076087..c72725640 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -195,16 +195,22 @@ endif() set(PSXRECOMP_CODEGEN_HASH_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..) include(${CMAKE_CURRENT_SOURCE_DIR}/../runtime/codegen_hash_sources.cmake) set(_baked_hash_hdr ${CMAKE_CURRENT_BINARY_DIR}/psxrecomp_baked_codegen_hash.h) +set(_runtime_hash_hdr ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/include/overlay_codegen_hash.h) add_custom_command( - OUTPUT ${_baked_hash_hdr} + OUTPUT ${_baked_hash_hdr} ${_runtime_hash_hdr} COMMAND ${CMAKE_COMMAND} -DOUT=${_baked_hash_hdr} "-DSRCS=${PSXRECOMP_CODEGEN_HASH_SRCS}" -P ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/hash_codegen.cmake + COMMAND ${CMAKE_COMMAND} -DOUT=${_runtime_hash_hdr} + "-DSRCS=${PSXRECOMP_CODEGEN_HASH_SRCS}" + -P ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/hash_codegen.cmake DEPENDS ${PSXRECOMP_CODEGEN_HASH_SRCS} ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/hash_codegen.cmake COMMENT "Hashing codegen sources -> psxrecomp_baked_codegen_hash.h" VERBATIM) -add_custom_target(psxrecomp_baked_codegen_hash DEPENDS ${_baked_hash_hdr}) +# Overlay generation is usable immediately after a tools-only build, before +# configuring any runtime. Both consumers must have the same cache identity. +add_custom_target(psxrecomp_baked_codegen_hash DEPENDS ${_baked_hash_hdr} ${_runtime_hash_hdr}) add_dependencies(psxrecomp-game psxrecomp_baked_codegen_hash) # Force main_psx.cpp to recompile whenever the hash header content changes # (same OBJECT_DEPENDS hardening as runtime.cmake — __has_include depfile gap). @@ -346,6 +352,17 @@ target_include_directories(emitter_directive_line_test PRIVATE target_link_libraries(emitter_directive_line_test PRIVATE fmt rabbitizer) if(BUILD_TESTING) + add_executable(source_cpu_block_bound_test + ../runtime/tests/test_source_cpu_block_bound.c + ../runtime/src/psx_icache.c) + target_include_directories(source_cpu_block_bound_test PRIVATE ../runtime/include) + target_compile_definitions(source_cpu_block_bound_test PRIVATE PSX_ENABLE_BLOCK_CYCLES=1) + if(MSVC) + target_compile_options(source_cpu_block_bound_test PRIVATE /UNDEBUG) + else() + target_compile_options(source_cpu_block_bound_test PRIVATE -UNDEBUG) + endif() + add_test(NAME source_cpu_block_bound_test COMMAND source_cpu_block_bound_test) add_test(NAME emitter_directive_line_test COMMAND emitter_directive_line_test) add_executable(guest_tty_test @@ -582,6 +599,7 @@ if(BUILD_TESTING) runtime_overlay_guards sdl3_main_single_include spu_sample_scheduler_default + overlay_step_boundary savestate_capture_boundary_guards vk_build_default vk_color_self_barrier @@ -646,6 +664,11 @@ target_include_directories(reachable_discovery_test PRIVATE target_link_libraries(reachable_discovery_test PRIVATE fmt) # ---- test-registration completeness guard ----------------------------------- +if(BUILD_TESTING) + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../tools/tasreplays/tests" + "${CMAKE_CURRENT_BINARY_DIR}/tas-models") +endif() + # Reads both CMakeLists.txt files as text, so it catches an orphan in either # project from whichever one is being configured. Placed before the collision # guard, which must stay last because it needs every target declared; this one diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 94b3507e1..42a0dc9f6 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -1432,7 +1432,8 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) case 0x0C: // syscall { uint32_t syscall_code = (instr >> 6) & 0xFFFFF; - code = fmt::format("psx_syscall(cpu, {}); /* syscall {} */", syscall_code, syscall_code); + code = fmt::format("cpu->pc = 0x{:08X}u; if (psx_syscall(cpu, {})) return; /* syscall {} */", + addr, syscall_code, syscall_code); } break; case 0x0D: // break @@ -1767,14 +1768,19 @@ std::string CodeGenerator::translate_basic_block( // emitted at cache-line LEADERS: a block leader / mid-block jump-table target (any // address reachable other than by fall-through, i.e. a possibly-cold cache entry) OR // a 16-byte-line start (addr&0xC==0, a sequential line crossing). Intra-line - // followers reached by fall-through are guaranteed hits — the leader's fetch - // refilled the line to its end — so they need no call (+0). Extra fetch points are + // followers reached by fall-through are hits only at cached virtual addresses; + // uncached instructions each require a fetch, including delay slots. Extra fetch points are // harmless (a hit is +0); only UNDER-counting a cold entry would diverge, which the // leader set prevents. The game runs at its KSEG0 load address, so `insn_addr` is // already the runtime guest PC (matching the dirty-RAM interp's cpu->pc and Beetle). auto emit_pre_icache = [&](uint32_t insn_addr, const std::string& indent) { - if (!(insn_addr == block.start_addr || (insn_addr & 0xCu) == 0 || - extra_labels_.count(insn_addr))) return; + if (!(insn_addr >= 0xA0000000u || insn_addr == block.start_addr || (insn_addr & 0xCu) == 0 || + extra_labels_.count(insn_addr))) { + ss << "#ifdef PSX_ENABLE_BLOCK_CYCLES\n"; + ss << indent << fmt::format("psx_cpu_step_boundary(cpu, 0x{:08X}u);\n", insn_addr); + ss << "#endif\n"; + return; + } ss << "#ifdef PSX_ENABLE_BLOCK_CYCLES\n"; ss << indent << fmt::format("psx_icache_fetch(cpu, 0x{:08X}u);\n", insn_addr); ss << "#endif\n"; diff --git a/recompiler/src/control_flow.cpp b/recompiler/src/control_flow.cpp index 68e452345..cd942a96e 100644 --- a/recompiler/src/control_flow.cpp +++ b/recompiler/src/control_flow.cpp @@ -191,6 +191,12 @@ std::set ControlFlowAnalyzer::find_block_boundaries(const Function& fu uint32_t instr = *instr_opt; + // A synchronous syscall can transfer to the guest exception vector. + // Its return is the following instruction (there is no delay slot), + // which must be independently re-enterable by the flat dispatcher. + if ((instr & 0xFC00003Fu) == 0x0000000Cu && addr + 4 < func.end_addr) + boundaries.insert(addr + 4); + if (is_control_flow(instr)) { ControlFlowInstr cf = analyze_instruction(addr, instr); @@ -387,6 +393,9 @@ ControlFlowGraph ControlFlowAnalyzer::analyze_function(const Function& func) { if (!instr_opt) continue; uint32_t instr = *instr_opt; + if ((instr & 0xFC00003Fu) == 0x0000000Cu) + add_boundary(addr + 4); // architectural post-syscall resume + if (is_control_flow(instr)) { ControlFlowInstr cf = analyze_instruction(addr, instr); diff --git a/recompiler/src/full_function_emitter.cpp b/recompiler/src/full_function_emitter.cpp index c15f32d45..2b08df9f4 100644 --- a/recompiler/src/full_function_emitter.cpp +++ b/recompiler/src/full_function_emitter.cpp @@ -226,6 +226,24 @@ bool FullFunctionEmitter::emit_function( // The function entry is always a leader. block_leaders.insert(func.entry_addr); + // A normal syscall resumes at the following guest instruction. Register + // that interior address with the owning function, like a call return. + // Without it, the unknown-dispatch trampoline can resolve a JR directly + // and omit both instruction fetches and its delay-slot effects. + for (const auto& [addr, raw] : addr_to_raw) { + if ((raw & 0xFC00003Fu) != 0x0000000Cu || !addr_to_raw.count(addr + 4u)) + continue; + const auto previous = addr_to_raw.find(addr - 4u); + if (previous != addr_to_raw.end()) { + const auto prior = StrictTranslator::translate( + PSXRecomp::MipsDecoder::decode(previous->second, previous->first)); + if (prior.is_terminator && + std::string(prior.terminator_kind ? prior.terminator_kind : "") != "rfe") + continue; // Delay-slot exception restart is a separate contract. + } + block_leaders.insert(addr + 4u); + } + // Collect continuation labels for this function (populated during emit). std::vector local_continuations; @@ -740,8 +758,9 @@ bool FullFunctionEmitter::emit_function( // cache-line LEADERS: a block leader (any branch/dispatch entry — a possibly-cold // cache entry; cross-function targets are inserted into block_leaders above) OR a // 16-byte-line start (addr&0xC==0, a sequential line crossing). Intra-line followers - // reached by fall-through are guaranteed hits (the leader refilled the line to its - // end) → no call (+0). `rom_addr` is the ROM/compile-time address; relocate_ra maps + // reached by fall-through are hits only in cached address space. Uncached code + // pays a fetch for every instruction, including delay slots. `rom_addr` is the + // ROM/compile-time address; relocate_ra maps // it to the RUNTIME guest PC the CPU actually fetches from (BIOS main stays in-place // KSEG1 0xBFC..; relocated kernel Part 2 → 0x500+, shell → 0x80030000+), so the // shared I-cache evolves identically to the dirty-RAM interp (cpu->pc) and Beetle — @@ -749,9 +768,14 @@ bool FullFunctionEmitter::emit_function( // relocation preserves bits[3:0], so the line-leader test is space-independent. auto emit_icache_fetch = [&](uint32_t rom_addr) { if (!per_insn_cycles) return; - if (!(block_leaders.count(rom_addr) || (rom_addr & 0xCu) == 0)) return; + const uint32_t runtime_addr = relocate_ra(rom_addr); + if (!(runtime_addr >= 0xA0000000u || block_leaders.count(rom_addr) || + (runtime_addr & 0xCu) == 0)) { + out += fmt::format("#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_cpu_step_boundary(cpu, 0x{:08X}u);\n#endif\n", runtime_addr); + return; + } out += fmt::format("#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_icache_fetch(cpu, 0x{:08X}u);\n#endif\n", - relocate_ra(rom_addr)); + runtime_addr); }; for (auto it = addr_to_raw.begin(); it != addr_to_raw.end(); ++it) { @@ -780,6 +804,42 @@ bool FullFunctionEmitter::emit_function( out += "#ifdef PSX_COSIM\n"; out += fmt::format(" cosim_block(0x{:08X}u);\n", normalize_address(addr)); out += "#endif\n"; + // Existing opt-in slicing also covers BIOS ROM in the explicit + // original-source profile. The runtime guards that admission; + // ordinary BIOS execution keeps its compiled path. + // A leader reached as a live delay slot cannot start a new slice. + bool follows_branch = false; + auto prev = addr_to_raw.find(addr - 4u); + if (prev != addr_to_raw.end()) { + const auto p = StrictTranslator::translate( + PSXRecomp::MipsDecoder::decode(prev->second, prev->first)); + const std::string kind = p.terminator_kind ? p.terminator_kind : ""; + follows_branch = p.is_terminator && + kind != "syscall" && kind != "break" && kind != "rfe"; + } + const uint32_t execution_phys = relocate_ra(addr) & 0x1FFFFFFFu; + const bool bios_rom = execution_phys >= 0x1FC00000u && + execution_phys < 0x1FC80000u; + if (per_insn_cycles && !follows_branch && + (execution_phys < 0x00200000u || bios_rom)) { + uint32_t count = 0; + bool may_stall_or_change_irq = false; + for (auto scan = it; scan != addr_to_raw.end(); ++scan) { + if (scan != it && block_leaders.count(scan->first)) break; + ++count; + const uint32_t op = scan->second >> 26; + const uint32_t fn = scan->second & 63u; + may_stall_or_change_irq |= op >= 0x20u || op == 0x10u || + op == 0x12u || (op == 0u && fn >= 0x10u && fn <= 0x1Bu); + } + // Current shared cache model: <=7 refill cycles plus one base + // cycle. This conservative guard budget is not a guest charge. + out += "#ifdef PSX_ENABLE_BLOCK_CYCLES\n"; + out += fmt::format( + " if (psx_slice_block(cpu, 0x{:08X}u, {}u, {})) return;\n", + relocate_ra(addr), count * 8u, may_stall_or_change_irq ? 1 : 0); + out += "#endif\n"; + } // Phase 1.0e-d: advance guest cycles for this block. Macro- // gated; when off, generated code matches pre-1.0e-d output. // In per-instruction mode the charge is emitted per instruction @@ -810,6 +870,10 @@ bool FullFunctionEmitter::emit_function( // Decode and translate. PSXRecomp::DecodedInstruction d = PSXRecomp::MipsDecoder::decode(raw, addr); + // SYSCALL publishes the guest exception PC. The ROM address remains + // the discovery/label identity, but copied code executes in RAM. + if ((raw & 0xFC00003Fu) == 0x0000000Cu) + d.address = relocate_ra(addr); TranslateResult tr = StrictTranslator::translate(d); if (!tr.supported) { @@ -1836,6 +1900,7 @@ void FullFunctionEmitter::emit_dispatch( out += "typedef struct {\n"; out += " uint32_t addr;\n"; out += " PsxRecompFunc func;\n"; + out += " uint32_t runtime_pc; /* represented guest alias, not lookup key */\n"; out += "} DispatchEntry;\n\n"; const size_t total_entries = emitted_normalized.size() + vec_handlers.size(); @@ -1844,16 +1909,21 @@ void FullFunctionEmitter::emit_dispatch( // Vector entries first (addresses 0xA0/0xB0/0xC0 < 0x500, always first). for (const auto& vh : vec_handlers) { - out += fmt::format(" {{ 0x{:08X}u, {} }},\n", vh.ram_addr, vh.func_name); + out += fmt::format(" {{ 0x{:08X}u, {}, 0x{:08X}u }},\n", vh.ram_addr, vh.func_name, vh.ram_addr); } + std::map runtime_by_norm; + for (const auto& fn : dr.functions) + runtime_by_norm[normalize_address(fn.entry_addr)] = bios_runtime_pc(fn.entry_addr); for (uint32_t norm : emitted_normalized) { if (continuations.count(norm)) { const auto& cl = continuations.at(norm); - out += fmt::format(" {{ 0x{:08X}u, {} }},\n", - norm, cont_sym(cl.parent_func_norm, cl.rom_addr)); + out += fmt::format(" {{ 0x{:08X}u, {}, 0x{:08X}u }},\n", + norm, cont_sym(cl.parent_func_norm, cl.rom_addr), bios_runtime_pc(cl.rom_addr)); } else { - out += fmt::format(" {{ 0x{:08X}u, {} }},\n", norm, fn_sym(norm)); + const auto entry = runtime_by_norm.find(norm); + const uint32_t runtime = entry == runtime_by_norm.end() ? norm : entry->second; + out += fmt::format(" {{ 0x{:08X}u, {}, 0x{:08X}u }},\n", norm, fn_sym(norm), runtime); } } out += "};\n\n"; @@ -1966,6 +2036,8 @@ void FullFunctionEmitter::emit_dispatch( out += "static int psx_bios_try_native_call_stub(CPUState* cpu, " "uint32_t addr) {\n"; out += " uint32_t phys = addr & 0x1FFFFFFFu;\n"; + out += " if (g_psx_cpu_step_boundary_callback) return 0; /* fused stub has no intermediate retirement boundary */\n"; + out += " if (addr >= 0xA0000000u) return 0; /* uncached RAM stubs need every fetch */\n"; out += " if (phys != 0xA0u && phys != 0xB0u && phys != 0xC0u) return 0;\n"; out += " uint32_t w0 = cpu->read_word(phys + 0u);\n"; out += " uint32_t w1 = cpu->read_word(phys + 4u);\n"; @@ -1998,6 +2070,7 @@ void FullFunctionEmitter::emit_dispatch( out += "extern int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr);\n"; out += "extern int dirty_ram_is_dirty(uint32_t phys);\n"; + out += "extern void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len);\n"; out += "extern int psx_kernel_bless_dispatchable(uint32_t phys);\n"; out += "extern void fntrace_record(CPUState* cpu, uint32_t target);\n"; out += "extern uint64_t g_dispatch_static_hits;\n"; @@ -2109,6 +2182,16 @@ void FullFunctionEmitter::emit_dispatch( out += " while (lo <= hi) {\n"; out += " int mid = (lo + hi) / 2;\n"; out += " if (dispatch_table[mid].addr == phys) {\n"; + out += " /* Normalization locates bytes; it does not change the guest\n"; + out += " * execution alias. Fixed native bodies encode fetch/link/PC\n"; + out += " * addresses. Interpret unrepresented RAM aliases as supplied. */\n"; + out += " if ((addr & 0x1FFFFFFFu) < 0x200000u &&\n"; + out += " addr != dispatch_table[mid].runtime_pc) {\n"; + out += " /* A known entry also admits clean RAM to the interpreter.\n"; + out += " * This marks host executable metadata, never guest bytes. */\n"; + out += " dirty_ram_mark_executable_range(addr & 0x1FFFFFFFu, 4u);\n"; + out += " break;\n"; + out += " }\n"; if (addr_model().has_kbless()) { out += fmt::format( " /* Kernel-image bless guard (CLAUDE.md Rule 18). The keys in\n" diff --git a/recompiler/tests/full_function_emitter_test.cpp b/recompiler/tests/full_function_emitter_test.cpp index 747e977d4..19e026730 100644 --- a/recompiler/tests/full_function_emitter_test.cpp +++ b/recompiler/tests/full_function_emitter_test.cpp @@ -62,6 +62,7 @@ std::string read_file(const std::filesystem::path& path) { struct RunResult { EmitStats stats; std::string dispatch; + std::string body; }; RunResult run_case(const char* name, const std::vector& words, @@ -86,6 +87,7 @@ RunResult run_case(const char* name, const std::vector& words, rom, kBase, kBase + static_cast(rom.size()) - 1u, discovery, "synthetic", out_dir.string(), stem); result.dispatch = read_file(out_dir / (stem + "_dispatch.c")); + result.body = read_file(out_dir / (stem + "_full.c")); std::filesystem::remove_all(out_dir); return result; } @@ -94,7 +96,7 @@ void expect_entry_absent(const RunResult& result, uint32_t normalized, const char* message) { char needle[96]; std::snprintf(needle, sizeof(needle), - "{ 0x%08Xu, Test_func_%08X }", normalized, normalized); + "{ 0x%08Xu, Test_func_%08X", normalized, normalized); expect(result.dispatch.find(needle) == std::string::npos, message); } @@ -197,9 +199,35 @@ void complementary_lwl_lwr_stays_native() { "complementary LWL/LWR remains native"); } +void linear_fetches_follow_runtime_address(uint32_t runtime) { + const bool uncached = runtime >= 0xA0000000u; + const auto result = run_case( + uncached ? "uncached-fetch" : "relocated-cached-fetch", + {0x24080001u, 0x25080001u, 0x25080001u, 0x25080001u, + 0x03E00008u, 0x00000000u}, + {function_at(kBase, kBase + 20u, {kBase})}); + expect(result.stats.functions_emitted == 1, "synthetic fetch program remains native"); + if (const char* output = std::getenv("PSX_TEST_FETCH_OUTPUT_DIR")) { + char name[64]; std::snprintf(name,sizeof name,"full-fetch-%08X.c",runtime); + std::ofstream generated(std::filesystem::path(output)/name);generated << result.body; + } + for (uint32_t offset = 0; offset < 24; offset += 4) { + char needle[96]; + std::snprintf(needle, sizeof needle, "psx_icache_fetch(cpu, 0x%08Xu);", runtime + offset); + const auto first = result.body.find(needle); + const bool required = uncached || (offset & 15u) == 0; + expect((first != std::string::npos) == required, + uncached ? "every uncached instruction pays a fetch, including return slot" + : "relocated cached followers retain the existing leader policy"); + if (first != std::string::npos) + expect(result.body.find(needle, first + 1) == std::string::npos, + "a fetch is not emitted twice for one instruction"); + } +} + } // namespace -int main() { +int main(int argc, char** argv) { BiosConfig config{}; config.config_path = "test://full-function-emitter"; config.load_address = kBase; @@ -216,11 +244,163 @@ int main() { FunctionDiscovery::set_address_model(&model); FullFunctionEmitter::set_address_model(&model); + if (argc == 3 && std::string(argv[2]).rfind("rom-irq", 0) == 0) { + config.address_copies.clear(); + BiosAddressModel rom_model = BiosAddressModel::from_config(config); + FunctionDiscovery::set_address_model(&rom_model); + FullFunctionEmitter::set_address_model(&rom_model); + std::vector words(0x564u / 4u, 0u); + const uint32_t branch = std::string(argv[2]) == "rom-irq-not-taken" ? + 0x13000001u : std::string(argv[2]) == "rom-irq-jump" ? 0x0BF00155u : 0x17000001u; + const uint32_t body[] = {0x27180001u, 0x27180001u, 0x27180001u, + branch, 0u, 0x24190009u, 0xAE190000u, 0x03E00008u, 0u}; + for (unsigned i = 0; i < 9; ++i) words[0x540u / 4u + i] = body[i]; + std::vector rom; + for (uint32_t op : words) append_word(rom, op); + DiscoveryResult discovery{}; discovery.ok = true; + discovery.functions = { + function_at(kBase + 0x540u, kBase + 0x550u, {kBase + 0x540u}), + function_at(kBase + 0x554u, kBase + 0x560u, {kBase + 0x554u})}; + std::filesystem::create_directories(argv[1]); + FullFunctionEmitter::emit(rom, kBase, kBase + (uint32_t)rom.size() - 1u, + discovery, "authored ROM IRQ fixture", argv[1], "Test"); + return 0; + } + + // A branch-slot return-address load crossing to a represented epilogue. + // The first target instruction also reads RA: retiring at handoff is early. + if (argc == 3 && (std::string(argv[2]) == "slice-load-return" || + std::string(argv[2]) == "slice-load-return-dependent")) { + config.address_copies[0].runtime_base = 0x80000500u; + BiosAddressModel return_model = BiosAddressModel::from_config(config); + FunctionDiscovery::set_address_model(&return_model); + FullFunctionEmitter::set_address_model(&return_model); + std::vector words = { + 0x27BDFFE8u, 0x10000003u, 0x8FBF0014u, 0u, 0u, + std::string(argv[2]) == "slice-load-return-dependent" ? 0x03E08021u : 0x00008021u, + 0x27BD0018u, 0x03E00008u, 0x26310001u, + 0x26520001u, 0x0260F821u, 0x03E00008u, 0u}; + std::vector rom; + for (uint32_t op : words) append_word(rom, op); + DiscoveryResult discovery{}; discovery.ok = true; + discovery.functions = { + function_at(kBase, kBase + 8u, {kBase}), + function_at(kBase + 0x14u, kBase + 0x20u, {kBase + 0x14u}), + function_at(kBase + 0x24u, kBase + 0x30u, {kBase + 0x24u})}; + std::filesystem::create_directories(argv[1]); + FullFunctionEmitter::emit(rom, kBase, kBase + (uint32_t)rom.size() - 1u, + discovery, "authored slice return fixture", argv[1], "Test"); + std::ofstream data(std::filesystem::path(argv[1]) / "authored-ram.bin", std::ios::binary); + data.write((const char*)rom.data(), rom.size()); + return 0; + } + + // Authored copied-BIOS timer IRQ recognition, with a single native group. + // Target/mode/mask are case inputs, never fitted to a retail outcome. + if (argc == 3 && std::string(argv[2]) == "irq-recognition") { + config.address_copies[0].runtime_base = 0x80000500u; + BiosAddressModel irq_model = BiosAddressModel::from_config(config); + FunctionDiscovery::set_address_model(&irq_model); + FullFunctionEmitter::set_address_model(&irq_model); + std::vector words = { + 0xA5690008u, 0xA56C0004u, 0x95680000u, 0x8E020000u, 0x8E180000u, + 0x00000000u, 0x2719FFFFu, 0x14400005u, 0xAE190000u, + 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, + 0x03E00008u, 0x26310001u}; + std::vector rom; + for (uint32_t op : words) append_word(rom, op); + DiscoveryResult discovery{}; discovery.ok = true; + discovery.functions = { + function_at(kBase, kBase + 0x20u, {kBase}), + function_at(kBase + 0x34u, kBase + 0x38u, {kBase + 0x34u})}; + std::filesystem::create_directories(argv[1]); + FullFunctionEmitter::emit(rom, kBase, kBase + (uint32_t)rom.size() - 1u, + discovery, "authored timer IRQ fixture", argv[1], "Test"); + std::ofstream data(std::filesystem::path(argv[1]) / "authored-ram.bin", std::ios::binary); + data.write((const char*)rom.data(), rom.size()); + return 0; + } + + // Authored uncached SYS resume and ordinary JAL continuation controls. + if (argc == 3 && std::string(argv[2]) == "syscall-resume") { + config.address_copies.clear(); + BiosAddressModel resume_model = BiosAddressModel::from_config(config); + FunctionDiscovery::set_address_model(&resume_model); + FullFunctionEmitter::set_address_model(&resume_model); + std::vector words(0x48u / 4u, 0u); + words[0] = 0x0000000Cu; words[1] = 0x03E00008u; words[2] = 0x26310001u; + words[8] = 0x0FF00010u; words[10] = 0x02000008u; words[11] = 0x26310001u; + words[16] = 0x03E00008u; + std::vector rom; + for (uint32_t op : words) append_word(rom, op); + DiscoveryResult discovery{}; discovery.ok = true; + discovery.functions = { + function_at(kBase, kBase + 8u, {kBase}), + function_at(kBase + 0x20u, kBase + 0x2Cu, {kBase + 0x20u, kBase + 0x28u}), + function_at(kBase + 0x40u, kBase + 0x44u, {kBase + 0x40u})}; + std::filesystem::create_directories(argv[1]); + FullFunctionEmitter::emit(rom, kBase, kBase + (uint32_t)rom.size() - 1u, + discovery, "authored syscall resume fixture", argv[1], "Test"); + std::ofstream data(std::filesystem::path(argv[1]) / "authored-rom.bin", std::ios::binary); + data.write((const char*)rom.data(), rom.size()); + return 0; + } + + // Export only authored instructions for a real-runtime admission fixture. + if (argc == 2) { + std::vector words(0x130u / 4u, 0u); + const auto put = [&](uint32_t at, std::initializer_list ops) { + for (uint32_t op : ops) { words[(at - 0x500u) / 4u] = op; at += 4u; } + }; + put(0x500u, {0x3C080000u, 0x25080598u, 0x01000008u, 0u}); + put(0x540u, {0x0C000160u, 0u, 0x03E08021u, 0x02200008u, 0u}); + put(0x580u, {0x03E00008u, 0u}); + put(0x5C0u, {0x0000000Cu, 0x03E00008u, 0u}); + put(0x600u, {0x10000007u, 0u}); + put(0x620u, {0x01001021u, 0x03E00008u, 0u}); + std::vector rom; + for (uint32_t op : words) append_word(rom, op); + DiscoveryResult discovery{}; discovery.ok = true; + discovery.functions = { + function_at(kBase, kBase + 12u, {kBase}), + function_at(kBase + 0x40u, kBase + 0x50u, {kBase + 0x40u, kBase + 0x48u}), + function_at(kBase + 0x80u, kBase + 0x84u, {kBase + 0x80u}), + function_at(kBase + 0xC0u, kBase + 0xC8u, {kBase + 0xC0u}), + function_at(kBase + 0x100u, kBase + 0x128u, {kBase + 0x100u, kBase + 0x120u})}; + std::filesystem::create_directories(argv[1]); + FullFunctionEmitter::emit(rom, kBase, kBase + (uint32_t)rom.size() - 1u, + discovery, "authored alias fixture", argv[1], "Test"); + std::ofstream data(std::filesystem::path(argv[1]) / "authored-ram.bin", std::ios::binary); + data.write((const char*)rom.data(), rom.size()); + return 0; + } + delay_slot_load_falls_back(); label_split_load_falls_back(); fragment_split_load_falls_back(); noncomplementary_lwl_falls_back(); complementary_lwl_lwr_stays_native(); + linear_fetches_follow_runtime_address(0x500u); + for (const uint32_t alias : {0x80010000u, 0xA0010000u}) { + config.address_copies[0].ram_lo = 0x00010000u; + config.address_copies[0].runtime_base = alias; + BiosAddressModel alias_model = BiosAddressModel::from_config(config); + FunctionDiscovery::set_address_model(&alias_model); + FullFunctionEmitter::set_address_model(&alias_model); + linear_fetches_follow_runtime_address(alias); + } + config.address_copies.clear(); + BiosAddressModel uncached_model = BiosAddressModel::from_config(config); + FunctionDiscovery::set_address_model(&uncached_model); + FullFunctionEmitter::set_address_model(&uncached_model); + linear_fetches_follow_runtime_address(kBase); + + const auto resume = run_case("syscall-resume", {0x0000000Cu, 0x03E00008u, 0x26310001u}, + {function_at(kBase, kBase + 8u, {kBase})}); + expect(resume.body.find("case 0xBFC00004u: goto label_BFC00004;") != std::string::npos, + "post-syscall resume enters its owning function at the exact instruction"); + expect(resume.dispatch.find("{ 0x1FC00004u,") != std::string::npos, + "post-syscall interior instruction has a dispatch key"); if (failures != 0) { std::fprintf(stderr, "%d full-function emitter test(s) failed\n", failures); diff --git a/recompiler/tests/recompiler_patch_test.cpp b/recompiler/tests/recompiler_patch_test.cpp index ab644eae4..d9ec6427a 100644 --- a/recompiler/tests/recompiler_patch_test.cpp +++ b/recompiler/tests/recompiler_patch_test.cpp @@ -1056,6 +1056,76 @@ void cfg_codegen_load_delay_test() { "CFG codegen preserves MIPS-I dependent load-delay value semantics"); } +void uncached_fetch_codegen_test() { + for (const uint32_t base : {0x00010000u, 0x80010000u, 0xA0010000u}) { + PSXRecomp::PS1Executable exe{}; + exe.header.load_address = base; + exe.header.initial_pc = base; + exe.header.file_size = 24u; + for (const uint32_t word : {0x24080001u, 0x25080001u, 0x25080001u, + 0x25080001u, 0x03E00008u, 0x00000000u}) + append_word(exe.code_data, word); + PSXRecomp::Function function{}; + function.start_addr = base; + function.end_addr = base + 24u; + function.size = 24u; + function.name = "fetch_test"; + PSXRecomp::ControlFlowAnalyzer analyzer(exe); + const auto cfg = analyzer.analyze_function(function); + PSXRecomp::CodeGenerator generator(exe); + const std::string code = generator.generate_function(function, cfg).full_code; + if (const char* output = std::getenv("PSX_TEST_FETCH_OUTPUT_DIR")) { + std::ofstream generated(fs::path(output)/fmt::format("cfg-fetch-{:08X}.c",base)); + generated << "#include \"cpu_state.h\"\n#include \"psx_icache.h\"\n#include \"psx_cyc.h\"\n" << code; + } + for (uint32_t offset = 0; offset < 24; offset += 4) { + const std::string needle = fmt::format("psx_icache_fetch(cpu, 0x{:08X}u);", base + offset); + const size_t found = code.find(needle); + check((found != std::string::npos) == (base >= 0xA0000000u || (offset & 15u) == 0), + fmt::format("virtual fetch policy at {:08X}, including return slot", base + offset)); + check(found == std::string::npos || code.find(needle, found + 1) == std::string::npos, + "one fetch per emitted instruction site"); + } + } +} + +void syscall_codegen_transfer_test() { + for (const uint32_t base : {0x80010000u, 0xA0010000u}) { + PSXRecomp::PS1Executable exe{}; + exe.header.load_address = base; exe.header.initial_pc = base; + exe.header.file_size = 20; + for (uint32_t word : {0x24040001u, 0x0000000Cu, 0x25080001u, + 0x03E00008u, 0x00000000u}) append_word(exe.code_data, word); + PSXRecomp::Function function{}; + function.start_addr=base; function.end_addr=base+20; function.size=20; + function.name=fmt::format("func_{:08X}",base); + PSXRecomp::ControlFlowAnalyzer analyzer(exe); + auto cfg=analyzer.analyze_function(function); + PSXRecomp::CodeGenerator generator(exe); + auto generated=generator.generate_function(function,cfg); + check(generated.full_code.find(fmt::format( + "cpu->pc = 0x{:08X}u; if (psx_syscall(cpu, 0)) return;",base+4))!=std::string::npos, + "game syscall sets executing virtual PC and preserves transfer"); + check(cfg.blocks.count(base+8)==1 && generator.cps_continuations().count(base+8)==1, + "post-syscall instruction has a dispatchable continuation"); + if (const char *dir=std::getenv("PSX_TEST_SYSCALL_OUTPUT_DIR")) { + std::ofstream out(fs::path(dir)/fmt::format("syscall-{:08X}.c",base)); + out << "#include \n#include \n#include \"cpu_state.h\"\n" + "#define PSX_NO_DEBUG_TOOLS 1\n" + "static int transfer; static uint32_t seen;\n" + "void debug_server_log_call_entry(uint32_t x) {(void)x;}\n" + "void psx_check_interrupts_at(CPUState*c,uint32_t p){(void)c;(void)p;}\n" + "int psx_syscall(CPUState*c,uint32_t code){seen=c->pc;c->pc=transfer?0x80000080:0;return transfer;}\n"; + out << generated.full_code; + out << fmt::format("\nint main(void){{CPUState c={{0}};c.gpr[31]=0x80090000;transfer=1;" + "func_{0:08X}(&c);assert(seen==0x{1:08X}u&&c.pc==0x80000080&&c.gpr[8]==0);" + "c.pc=0x{2:08X}u;func_{0:08X}(&c);assert(c.gpr[8]==1&&c.pc==c.gpr[31]);" + "c.pc=0;c.gpr[8]=0;transfer=0;func_{0:08X}(&c);assert(c.gpr[8]==1&&seen==0x{1:08X}u);return 0;}}\n", + base,base+4,base+8); + } + } +} + } // namespace int main() { @@ -1071,6 +1141,8 @@ int main() { gte_codegen_classification_tests(); jump_table_producer_codegen_test(); cfg_codegen_load_delay_test(); + uncached_fetch_codegen_test(); + syscall_codegen_transfer_test(); } catch (const std::exception& e) { fmt::print(stderr, "FAIL unexpected exception: {}\n", e.what()); ++failures; diff --git a/recompiler/tests/test_aot_overlay_discovery.py b/recompiler/tests/test_aot_overlay_discovery.py index 77d97acb4..ce9a92af2 100644 --- a/recompiler/tests/test_aot_overlay_discovery.py +++ b/recompiler/tests/test_aot_overlay_discovery.py @@ -1441,8 +1441,10 @@ def pause_after_manifest(src, dst): import _ctypes import ctypes import shutil - gcc = r'C:\msys64\mingw64\bin\gcc.exe' - assert os.path.isfile(gcc), "real loaded-DLL regression needs MinGW gcc" + gcc = (r'C:\msys64\mingw64\bin\gcc.exe' + if os.path.isfile(r'C:\msys64\mingw64\bin\gcc.exe') + else shutil.which('gcc')) + assert gcc and os.path.isfile(gcc), "real loaded-DLL regression needs MinGW gcc" with tempfile.TemporaryDirectory() as tmp: # The actual host compiler must export the same 64-bit identity # serialized in P; this catches width/decorated-name mistakes @@ -2145,8 +2147,10 @@ def racing_link(_source, staged, *_args, **_kwargs): if os.name == 'nt': # Exact pairs racing to distinct names may both commit: the namespace # lock makes the second writer observe the first pair's dedup identity. - gcc = r'C:\msys64\mingw64\bin\gcc.exe' - assert os.path.isfile(gcc) + gcc = (r'C:\msys64\mingw64\bin\gcc.exe' + if os.path.isfile(r'C:\msys64\mingw64\bin\gcc.exe') + else shutil.which('gcc')) + assert gcc and os.path.isfile(gcc) with tempfile.TemporaryDirectory() as tmp: pair_id = 0x1020304050607080 source = pathlib.Path(tmp) / 'capacity.c' @@ -2924,7 +2928,9 @@ def capacity_full(roots): f"P {pair_id:016X}\nF {entry:08X} {code_crc:08X} junk\n" f"R {entry:08X} 8\n") assert MOD.load_shard_entry_set(str(dll)) == set() - outside = 0x80200000 + # The loader also accepts high-bank code for 8 MiB modifications. + # Exercise the actual host-capacity boundary, not retail RAM size. + outside = 0x80000000 + MOD.PSX_RAM_SIZE ranges.write_text( f"P {pair_id:016X}\nF {outside:08X} {code_crc:08X}\n" f"R {outside:08X} 4\n") @@ -3421,7 +3427,7 @@ def check_full_candidate_cli_fastpath(recompiler): leaf = (cache_root / 'CYCT-00101' / 'gcc' / MOD.cache_arch_abi() / f'cg{MOD.codegen_ver(str(runtime_include))}_' f'{MOD.codegen_hash(str(runtime_include)):08x}_' - f'gc{MOD.overlay_config_hash(recompiler, str(game_toml)):08x}') + f'gc{MOD.overlay_config_hash(recompiler, str(game_toml)):08x}_f0') leaf.mkdir(parents=True) pair_id = 0x123456789ABCDEF0 captured_bytes = b'\x08\x00\xE0\x03\x00\x00\x00\x00' @@ -3723,4 +3729,19 @@ def main(): if __name__ == "__main__": - sys.exit(main()) + if os.name == 'nt': + # This test deliberately loads malformed DLL bytes. CTest may clear + # the shell's inherited error mode, leaving LoadLibrary in a modal + # Windows error dialog instead of returning the expected OSError. + import ctypes + kernel = ctypes.WinDLL('kernel32', use_last_error=True) + kernel.GetErrorMode.restype = ctypes.c_uint + kernel.SetErrorMode.argtypes = [ctypes.c_uint] + previous_mode = kernel.GetErrorMode() + kernel.SetErrorMode(previous_mode | 0x0001 | 0x8000) + try: + sys.exit(main()) + finally: + kernel.SetErrorMode(previous_mode) + else: + sys.exit(main()) diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 18d206be3..7367c6692 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -23,6 +23,80 @@ psxrecomp_add_runtime_target(psx-runtime # (ENHANCEMENTS.md G1.10). Titles get their _pgxp the same way. if(BUILD_TESTING) + add_executable(compiled_ram_read_watch_test tests/test_compiled_ram_read_watch.c) + target_include_directories(compiled_ram_read_watch_test PRIVATE include) + target_compile_definitions(compiled_ram_read_watch_test PRIVATE PSX_ENABLE_BLOCK_CYCLES=1) + add_test(NAME compiled_ram_read_watch_test COMMAND compiled_ram_read_watch_test) + add_executable(input_update_clock_test tests/test_input_update_clock.c) + target_include_directories(input_update_clock_test PRIVATE include) + add_test(NAME input_update_clock_test COMMAND input_update_clock_test) + add_executable(input_instruction_histogram_test tests/test_input_instruction_histogram.c) + target_include_directories(input_instruction_histogram_test PRIVATE include) + add_test(NAME input_instruction_histogram_test COMMAND input_instruction_histogram_test) + if(CMAKE_C_COMPILER_ID STREQUAL "GNU") + add_executable(critical_exception_test tests/test_critical_exception.c) + target_include_directories(critical_exception_test PRIVATE include) + target_compile_options(critical_exception_test PRIVATE -O3 -flto -fwhole-program) + target_link_options(critical_exception_test PRIVATE -flto) + add_test(NAME critical_exception_direct_test COMMAND critical_exception_test) + add_test(NAME critical_exception_vector_test COMMAND critical_exception_test exception) + add_test(NAME critical_exception_nested_reject_test COMMAND critical_exception_test exception nested) + set_tests_properties(critical_exception_nested_reject_test PROPERTIES WILL_FAIL TRUE) + add_executable(sio_card_repair_test tests/test_sio_card_repair.c) + target_include_directories(sio_card_repair_test PRIVATE include) + target_compile_options(sio_card_repair_test PRIVATE -O2 -flto -fwhole-program) + target_link_options(sio_card_repair_test PRIVATE -flto) + add_test(NAME sio_card_repair_test COMMAND sio_card_repair_test) + add_executable(sio_ack_timing_test tests/test_sio_ack_timing.c) + target_include_directories(sio_ack_timing_test PRIVATE include) + target_compile_options(sio_ack_timing_test PRIVATE -O2 -flto -fwhole-program) + target_link_options(sio_ack_timing_test PRIVATE -flto) + add_test(NAME sio_ack_timing_test COMMAND sio_ack_timing_test) + add_executable(dma_otc_timing_test tests/test_dma_otc_timing.c) + target_include_directories(dma_otc_timing_test PRIVATE include) + target_compile_options(dma_otc_timing_test PRIVATE -O2 -flto -fwhole-program) + target_link_options(dma_otc_timing_test PRIVATE -flto) + add_test(NAME dma_otc_timing_test COMMAND dma_otc_timing_test) + add_executable(dma_cd_source_test tests/test_dma_cd_source.c) + target_include_directories(dma_cd_source_test PRIVATE include) + target_compile_definitions(dma_cd_source_test PRIVATE PSX_TEST_SOURCE_CD_IMPLEMENTED) + target_compile_options(dma_cd_source_test PRIVATE -O2 -flto -fwhole-program) + target_link_options(dma_cd_source_test PRIVATE -flto) + add_test(NAME dma_cd_source_test COMMAND dma_cd_source_test) + endif() + add_executable(cdrom_boot_model_test tests/test_cdrom_boot_model.c src/psx_sha256.c) + target_include_directories(cdrom_boot_model_test PRIVATE include) + add_test(NAME cdrom_boot_model_test COMMAND cdrom_boot_model_test) + add_executable(cdrom_explicit_seek_test tests/test_cdrom_explicit_seek.c src/psx_sha256.c) + target_include_directories(cdrom_explicit_seek_test PRIVATE include) + add_test(NAME cdrom_explicit_seek_test COMMAND cdrom_explicit_seek_test) + add_executable(cdrom_source_explicit_timing_test tests/test_cdrom_source_explicit_timing.c src/psx_sha256.c) + target_include_directories(cdrom_source_explicit_timing_test PRIVATE include) + add_test(NAME cdrom_source_explicit_timing_test COMMAND cdrom_source_explicit_timing_test) + add_executable(cdrom_random_tape_test tests/test_cdrom_random_tape.c src/psx_sha256.c) + target_include_directories(cdrom_random_tape_test PRIVATE include) + add_test(NAME cdrom_random_tape_test COMMAND cdrom_random_tape_test) + add_executable(cdrom_implicit_seek_test tests/test_cdrom_implicit_seek.c src/psx_sha256.c) + target_include_directories(cdrom_implicit_seek_test PRIVATE include) + add_test(NAME cdrom_implicit_seek_test COMMAND cdrom_implicit_seek_test) + # This optional clock fixture takes a separately generated fixed tape path. + add_executable(cdrom_source_clock_test tests/test_cdrom_source_clock.c src/psx_sha256.c) + target_include_directories(cdrom_source_clock_test PRIVATE include) + add_executable(cdrom_source_reset_test tests/test_cdrom_source_reset.c src/psx_sha256.c) + target_include_directories(cdrom_source_reset_test PRIVATE include) + + add_executable(input_route_file_test tests/test_input_route_file.c) + target_include_directories(input_route_file_test PRIVATE include) + add_test(NAME input_route_file_test COMMAND input_route_file_test) + + find_package(Python3 REQUIRED COMPONENTS Interpreter) + add_executable(input_route_observer_test tests/test_input_route_observer.c + src/input_route_observer.c src/psx_sha256.c) + target_include_directories(input_route_observer_test PRIVATE include) + add_test(NAME input_route_observer_test COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_input_route_observer.py + $) + add_executable(launcher_device_roundtrip_test tests/test_launcher_device_roundtrip.cpp) target_include_directories(launcher_device_roundtrip_test PRIVATE include) @@ -478,6 +552,13 @@ if(BUILD_TESTING) target_include_directories(psx_vblank_clock_test PRIVATE include) add_test(NAME psx_vblank_clock_test COMMAND psx_vblank_clock_test) + add_executable(input_route_field_clock_test tests/test_input_route_field_clock.c) + target_include_directories(input_route_field_clock_test PRIVATE include) + add_test(NAME input_route_field_clock_test COMMAND input_route_field_clock_test) + add_executable(input_route_raster_clock_test tests/test_input_route_raster_clock.c) + target_include_directories(input_route_raster_clock_test PRIVATE include) + add_test(NAME input_route_raster_clock_test COMMAND input_route_raster_clock_test) + add_executable(spu_end_without_repeat_test tests/test_spu_end_without_repeat.c) target_include_directories(spu_end_without_repeat_test PRIVATE include) @@ -490,6 +571,10 @@ if(BUILD_TESTING) add_test(NAME spu_pitch_zero_test COMMAND spu_pitch_zero_test) + add_executable(spu_status_visibility_test tests/test_spu_status_visibility.c) + target_include_directories(spu_status_visibility_test PRIVATE include) + add_test(NAME spu_status_visibility_test COMMAND spu_status_visibility_test) + add_executable(fps_readout_test tests/test_fps_readout.c) target_include_directories(fps_readout_test PRIVATE include) add_test(NAME fps_readout_test COMMAND fps_readout_test) diff --git a/runtime/check_test_registration.cmake b/runtime/check_test_registration.cmake index a26d87f36..32d648fa0 100644 --- a/runtime/check_test_registration.cmake +++ b/runtime/check_test_registration.cmake @@ -44,6 +44,39 @@ set(_PSXRECOMP_TESTREG_DIR "${CMAKE_CURRENT_LIST_DIR}") # Every entry needs a reason. An entry with no reason is a silent orphan with # extra steps, which is the thing this file exists to prevent. set(PSXRECOMP_TESTS_NOT_REGISTERED + "test_irq_cache_block.c|Private TAS handoff work/check_irq_cache58.py links retained runtime objects and drives the unchanged original-core timer/branch controls; kernel76 and ROM85 variants share this driver" + "test_irq_recognition.c|Authored copied-BIOS timer IRQ fixture driven by shared tas-replay-research/verify_irq_recognition.py; source-qualified bounded emitted/load/return cases, not general precise continuation coverage" + "test_dma_completion_deadline.c|Authored actual-scheduler delayed-completion fixture driven by tas-replay-research/verify_dma_completion_deadline.py; O0/O2 deadline/cache/IRQ/timer controls" + "test_spu_status_scheduler.c|actual cycle/SPU integration with exact production sample-event extraction driven by tas-replay-research/verify_spu_scheduler.py" + "test_bios_syscall_resume.c|authored generated SYS resume and ordinary JAL control driven by tas-replay-research/verify_bios_syscall_resume.py" + "test_bios_alias_admission.c|authored generated program plus retained runtime integration driven by tas-replay-research/verify_alias_runtime.py" + "test_irq_prefetch.c|actual production IRQ poll and authored guest handler linked to retained runtime objects; original-core oracle driven by tas-replay-research/verify_irq_prefetch.py" + "test_compiled_dma_read_wait.c|authored fixture driven by tas-replay-research/verify_gpu_upload.py, with exact-source vectors and separate O0/O2 runs" + "test_dma_cd_deadline.c|authored fixture driven by tas-replay-research/verify_cd_dma_deadline.py, with exact-source vectors and separate O0/O2 runs" + "test_dma_gpu_linked_list_source.c|Authored controller fixture driven by tas-replay-research/verify_gpu_linked_list.py; source startup expectations and state/order negatives" + "test_source_gpu_command_projection.c|Standalone scalar projection driven by tas-replay-research/verify_gpu_command_projection.py; retained original-core checkpoints, no automatic scheduler or rendering claim" + "test_source_gpu_service_clock.c|Standalone event clock driven by tas-replay-research/verify_gpu_service_clock.py; cold authored NTSC service, excludes runtime scheduling and frame-end rescheduling" + "test_gpu_frame_retirement.c|Shared TAS verify_gpu_frame_retirement.py links actual RAM interpreter/IRQ/device objects; six source cases, tracing off/on. GPU service and generated paths are separate gates" + "test_cpu_step_boundary.c|Shared TAS verify_cpu_step_generated.py compiles authored output from both emitters against production objects; cold cached/uncached boundaries and callback/trace off/on" + "test_source_gpu_frame_request.c|Shared TAS verify_gpu_frame_request.py compares two automatic request/return streams against five authored original-core display-range controls" + "test_source_gpu_pending_frame.c|Shared TAS verify_gpu_pending_frame.py compares source pending quad across automatic frame request/return; supplied CPU steps, standalone service" + "test_source_gpu_runtime.c|Shared TAS verify_gpu_runtime.py links production scheduler and actual generated/interpreted CPU paths with independent source pending-quad state and tracing off/on" + "test_source_gpu_dma_runtime.c|Shared TAS verify_gpu_dma_runtime.py checks actual GPU reader, DMA controller and scheduler against retained authored scalar/readback states" + "test_source_gpu_environment.c|Shared TAS verify_gpu_environment.py compares cold ordinary environment/reset history through the actual GPU/DMA scheduler with original-source scalar states" + "test_source_gpu_cpu.c|Shared TAS verify_gpu_cpu.py executes authored RAM programs through production dirty-RAM CPU, GPU/DMA service and VRAM observations after a declared synthetic prologue" + "test_source_gpu_reset_projection.c|Shared TAS verify_gpu_reset_projection.py checks four ordinary original-source reset states at O0/O2 and unsupported draw-state rejection" + "test_dma_gpu_upload_source.c|authored fixture driven by tas-replay-research/verify_gpu_upload.py, with exact-source vectors and separate O0/O2 runs" + "test_icache_isolated_store.c|authored fixture driven by tas-replay-research/verify_cache_store.py, with exact-source vectors and separate O0/O2 runs" + "test_input_histogram_start.c|authored fixture driven by tas-replay-research/verify_histogram_start.py, with exact-source vectors and separate O0/O2 runs" + "test_irq_vector_entry.c|authored fixture driven by tas-replay-research/verify_irq_vector.py, with exact-source vectors and separate O0/O2 runs" + "test_memory_dma_read_wait.c|authored fixture driven by tas-replay-research/verify_gpu_upload.py, with exact-source vectors and separate O0/O2 runs" + "test_mmio_read_sample_order.c|authored fixture driven by tas-replay-research/verify_mmio_read_order.py, with exact-source vectors and separate O0/O2 runs" + "test_muldiv_deferred.c|authored fixture driven by tas-replay-research/verify_muldiv.py, with exact-source vectors and separate O0/O2 runs" + "test_timer1_raster_integration.c|authored fixture driven by tas-replay-research/verify_timer1.py, with exact-source vectors and separate O0/O2 runs" + "test_timer1_source_clock.c|authored fixture driven by tas-replay-research/verify_timer1.py, with exact-source vectors and separate O0/O2 runs" + "test_timer2_divider_native.c|exploratory divider probe documented in tas-replay-research/TIMER2-SOURCE-CLOCK.md; not driven by the portable verify_timer2.py regression" + "test_timer2_source_clock.c|authored fixture driven by tas-replay-research/verify_timer2.py, with exact-source vectors and separate O0/O2 runs" + "test_timer2_source_scheduler.c|authored fixture driven by tas-replay-research/verify_timer2.py, with exact-source vectors and separate O0/O2 runs" "test_overlay_posix.c|built and run by tests/run_overlay_posix_test.sh, which stages the dlopen fixture tree; that script is registered as overlay_posix_test on UNIX" ) diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index fa223777e..db7422096 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -152,6 +152,8 @@ void cdrom_tick(void); /* DMA channel 3 interface */ uint32_t cdrom_dma_read(void); +/* Source-compatible DMA byte drain: unavailable bytes are zero. */ +uint32_t cdrom_dma_read_padded(void); int cdrom_dma_ready(void); uint32_t cdrom_dma_sector_word_count(void); @@ -235,6 +237,8 @@ typedef struct CDROMSectorDebugState { typedef struct CDROMTraceEntry { uint64_t seq; + uint64_t cycle; + uint32_t guest_ra; uint32_t addr; uint32_t val; uint32_t func; @@ -299,6 +303,9 @@ typedef struct CDROMCommandHistoryEntry { uint8_t pending_pending; uint8_t queued_cmd; uint8_t queued_pending; + uint8_t source_clock; + uint32_t source_random_cursor; + uint32_t source_random_calls; } CDROMCommandHistoryEntry; typedef struct CDROMSectorHistoryEntry { diff --git a/runtime/include/cdrom_random_tape.h b/runtime/include/cdrom_random_tape.h new file mode 100644 index 000000000..c02ddcd1e --- /dev/null +++ b/runtime/include/cdrom_random_tape.h @@ -0,0 +1,70 @@ +#ifndef PSX_CDROM_RANDOM_TAPE_H +#define PSX_CDROM_RANDOM_TAPE_H +/* Generic bounded word source for explicit device-clock experiments. + * This reader contains no generator algorithm, seed, title or movie data. */ +#include +#include +#include +#include +#include "psx_sha256.h" + +typedef struct CdRandomTape { + uint8_t *bytes; + uint32_t count, cursor; + uint8_t sha256[32]; +} CdRandomTape; + +static uint32_t cd_tape_le32(const uint8_t *p) { + return (uint32_t)p[0] | (uint32_t)p[1]<<8 | (uint32_t)p[2]<<16 | (uint32_t)p[3]<<24; +} + +static int cd_tape_load(CdRandomTape *tape, const char *path) { + static const uint8_t magic[16] = "PSX-CD-RNG1"; + uint8_t header[20]; + CdRandomTape next = {0}; + FILE *file = fopen(path,"rb"); + if (!file) return 0; + if (fread(header,1,sizeof(header),file)!=sizeof(header) || memcmp(header,magic,16)) { + fclose(file); return 0; + } + next.count=cd_tape_le32(header+16); + if (!next.count || next.count>1048576u) { fclose(file); return 0; } + size_t size=(size_t)next.count*4u; + next.bytes=(uint8_t *)malloc(size); + if (!next.bytes) { fclose(file); return 0; } + if (fread(next.bytes,1,size,file)!=size || fgetc(file)!=EOF || ferror(file)) { + free(next.bytes); fclose(file); return 0; + } + fclose(file); + psx_sha256_ctx hash; + psx_sha256_init(&hash); + psx_sha256_update(&hash,header,sizeof(header)); + psx_sha256_update(&hash,next.bytes,size); + psx_sha256_final(&hash,next.sha256); + free(tape->bytes); + *tape=next; + return 1; +} + +static int cd_tape_bounded(CdRandomTape *tape, uint32_t maximum, uint32_t *value) { + uint32_t mask=maximum; + for (unsigned shift=1;shift<32;shift*=2) mask |= mask>>shift; + while (tape->cursorcount) { + uint32_t candidate=cd_tape_le32(tape->bytes+(size_t)tape->cursor++*4u)&mask; + if (candidate<=maximum) { *value=candidate; return 1; } + } + return 0; /* Exhaustion never wraps or supplies a made-up value. */ +} + +static int cd_tape_restore(CdRandomTape *tape, const uint8_t identity[32], + uint32_t count, uint32_t cursor) { + if (count!=tape->count || cursor>count || memcmp(identity,tape->sha256,32)) return 0; + tape->cursor=cursor; + return 1; +} + +static void cd_tape_reset(CdRandomTape *tape) { tape->cursor=0; } +static void cd_tape_free(CdRandomTape *tape) { + free(tape->bytes); memset(tape,0,sizeof(*tape)); +} +#endif diff --git a/runtime/include/debug_server.h b/runtime/include/debug_server.h index 9b7a6eb55..1f10109b9 100644 --- a/runtime/include/debug_server.h +++ b/runtime/include/debug_server.h @@ -83,6 +83,10 @@ typedef struct { * port=0 uses the default (4370). */ void debug_server_init(int port); +/* Opt-in PSXRTI1 route, validated before guest execution. First record is + * consumed at the first normal VBlank input-sampling boundary. */ +int debug_server_preload_input_route(const char *path); + /* Current listener status for heartbeat diagnostics. error is the platform * socket error captured by init, or 0 when the listener is active. */ void debug_server_get_status(int *listening, int *port, int *error); @@ -129,6 +133,7 @@ extern uint32_t g_debug_current_func_addr; * writes into the 1M-entry ring buffer with function + frame attribution. */ void debug_server_trace_write_check(uint32_t phys, uint32_t old_val, uint32_t new_val, uint8_t width); +uint16_t debug_server_update_poll(int slot, uint16_t buttons, int analog); /* MMIO write trace — separate ring buffer for 0x1F801xxx writes. */ void debug_server_trace_mmio_write(uint32_t addr, uint32_t val, uint8_t width); @@ -202,6 +207,8 @@ int debug_server_get_axis_override(unsigned char st[4]); /* TCP-controlled turbo mode. When enabled the frontend skips presentation and * wall-clock pacing at vblank, matching the keyboard TAB turbo path. */ int debug_server_turbo_enabled(void); +/* File playback only: assert actual P1 SIO state after normal VBlank sampling. */ +void debug_server_note_input_applied(void); /* TCP-controlled turbo mode. When enabled the frontend skips presentation and * wall-clock pacing at vblank, matching the keyboard TAB turbo path. */ @@ -233,6 +240,8 @@ void debug_server_send_fmt(const char *fmt, ...); * ring head. Forensic value is in the bulk of older entries which are * stable. */ void debug_server_freeze_dump_wtrace_all_json(FILE *f, uint32_t max_count); +/* Passive scalar filter of existing write history; no new recording or RAM reads. */ +void debug_server_dump_watched_writes(FILE *f, const uint32_t *u16_addresses, uint32_t count); void debug_server_freeze_dump_wtrace_json(FILE *f, uint32_t max_count); void debug_server_freeze_dump_mmio_json(FILE *f, uint32_t max_count); void debug_server_freeze_dump_frame_history_json(FILE *f, uint32_t max_count); diff --git a/runtime/include/dma.h b/runtime/include/dma.h index dccddd3da..dc2e50e4b 100644 --- a/runtime/include/dma.h +++ b/runtime/include/dma.h @@ -18,12 +18,24 @@ #define PSXRECOMP_DMA_H #include - #ifdef __cplusplus extern "C" { #endif +/* Source service owner: GPU, CD, SPU and OTC channel work at the supplied actual + * runtime cycle, including DMA writes and frame returns between periodic + * events. No register/input/state override is performed. */ +void dma_source_gpu_service_at(uint64_t cycle); + void dma_init(void); +uint32_t dma_cpu_read_penalty(void); /* Optional source upload's bus-read wait. */ +/* Source-profile CPU owner calls after prior-instruction service, before fetch. + * Direct device fixtures retain the live wait until this owner is entered. */ +void dma_cpu_read_wait_boundary(void); +/* Source-profile CPU owns the halt between instruction fetch attempts. */ +int dma_cpu_otc_halted(void); +/* OTC or manual, non-chopped CD DMA in the source profile. */ +int dma_cpu_source_halted(void); uint32_t dma_read(uint32_t addr); void dma_write(uint32_t addr, uint32_t val); void dma_write_masked(uint32_t addr, uint32_t val, uint32_t mask); diff --git a/runtime/include/event_ring.h b/runtime/include/event_ring.h index 1f82f31a3..115b40038 100644 --- a/runtime/include/event_ring.h +++ b/runtime/include/event_ring.h @@ -23,6 +23,7 @@ #define PSXRECOMP_EVENT_RING_H #include +#include #ifdef __cplusplus extern "C" { @@ -114,6 +115,9 @@ void event_ring_record_aux(uint16_t kind, uint8_t detail, uint32_t aux); /* Write the whole live window (oldest->newest) as a JSON array to `path`. * Returns entries written, or -1 on open failure. No TCP size limit. */ int event_ring_dump_file(const char *path); +/* Same retained window, written to a caller-owned stream. Does not close it. + * Allows capture tools to enforce exclusive creation before exporting. */ +int event_ring_dump_stream(FILE *stream); /* Emit a bounded JSON tail (most-recent `max_entries`, oldest->newest) into * `out`. Returns bytes written. For quick TCP inspection. */ diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index bee1e5759..dd81ab7b5 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -16,6 +16,10 @@ extern "C" { #endif void gpu_init(void); +uint32_t gpu_dma_vram_upload_words(void); /* Active GP0(A0) payload remaining. */ +/* Experimental LL gate: 1 idle, 0 qualified mono-polyline wait, -1 outside + * scope. This does not represent source draw budget or FIFO occupancy. */ +int gpu_dma_source_ll_ready(void); uint32_t gpu_read_gpustat(void); /* 0x1F801814 read */ uint32_t gpu_read_gpuread(void); /* 0x1F801810 read */ void gpu_write_gp0(uint32_t val); /* 0x1F801810 write */ @@ -52,6 +56,7 @@ void gpu_texture_correction_stats(uint64_t *attempts, uint64_t *armed, * both wastes bandwidth and force-flushes when UP_RECTS_MAX is hit (MotK FMV). */ int gpu_display_is_depth24(void); int gpu_video_standard_is_pal(void); /* GP1(08h) bit 3: 0 NTSC, 1 PAL */ +int gpu_display_is_interlaced(void); /* GP1(08h) bit 5, passive accessor */ void gpu_display_pixel_rgb(const GpuDisplayInfo* di, uint32_t x, uint32_t y, uint8_t* r, uint8_t* g, uint8_t* b); uint32_t gpu_display_pixel_argb(const GpuDisplayInfo* di, uint32_t x, uint32_t y); diff --git a/runtime/include/gpu_render.h b/runtime/include/gpu_render.h index ace31f119..b0e623925 100644 --- a/runtime/include/gpu_render.h +++ b/runtime/include/gpu_render.h @@ -12,6 +12,7 @@ * (a requested backend that fails to initialize falls back to software). */ #include +#include "source_gpu_texture.h" #ifdef __cplusplus extern "C" { @@ -56,6 +57,13 @@ void gr_set_precise_triangle(int enabled, void gr_set_perspective_triangle(int enabled, float q0, float q1, float q2); /* Primitives */ +/* Experimental native-software source-profile polygon path; full RGB888 + * interpolation, field skip and shared directed span/work traversal. */ +int gr_draw_source_block(const SourceGPUBlock *block,int *extra_work); +void gr_source_texture_control(unsigned action,uint32_t page); +int gr_draw_source_triangle(const int *x,const int *y,const uint32_t *colors, + int shaded,int dither,int interlace,unsigned skip_field, + const SourceGPUTexture *texture,int *extra_work); void gr_fill_rect(int x, int y, int w, int h, uint16_t color); void gr_copy_rect(int src_x, int src_y, int dst_x, int dst_y, int w, int h); void gr_draw_flat_triangle(int x0, int y0, int x1, int y1, int x2, int y2, diff --git a/runtime/include/gpu_sw_renderer.h b/runtime/include/gpu_sw_renderer.h index b8cea0e3a..6089b36b2 100644 --- a/runtime/include/gpu_sw_renderer.h +++ b/runtime/include/gpu_sw_renderer.h @@ -2,6 +2,7 @@ #define PSX_GPU_SW_RENDERER_H #include +#include "source_gpu_texture.h" #ifdef __cplusplus extern "C" { @@ -11,6 +12,11 @@ extern "C" { #define SW_MAX_INTERNAL_SCALE 4 /* Initialize software renderer */ +int sw_draw_source_block(const SourceGPUBlock *block,int *extra_work); +void sw_source_texture_control(unsigned action,uint32_t page); +int sw_draw_source_triangle(const int *x,const int *y,const uint32_t *colors, + int shaded,int dither,int interlace,unsigned skip_field, + const SourceGPUTexture *texture,int *extra_work); void sw_renderer_init(uint16_t* vram); /* Internal-resolution supersampling (SSAA). diff --git a/runtime/include/input_instruction_histogram.h b/runtime/include/input_instruction_histogram.h new file mode 100644 index 000000000..a0953517b --- /dev/null +++ b/runtime/include/input_instruction_histogram.h @@ -0,0 +1,13 @@ +#ifndef PSX_INPUT_INSTRUCTION_HISTOGRAM_H +#define PSX_INPUT_INSTRUCTION_HISTOGRAM_H +#include +#ifdef __cplusplus +extern "C" { +#endif +extern int g_input_instruction_histogram_active; +extern void (*g_input_instruction_histogram_callback)(uint32_t pc); +void input_instruction_histogram_sample(uint32_t pc); +#ifdef __cplusplus +} +#endif +#endif diff --git a/runtime/include/input_instruction_histogram_impl.h b/runtime/include/input_instruction_histogram_impl.h new file mode 100644 index 000000000..f366e9efe --- /dev/null +++ b/runtime/include/input_instruction_histogram_impl.h @@ -0,0 +1,151 @@ +/* Private debug-server implementation. Test-configured passive fetch counts. + * No opcodes, registers, input words or guest memory are changed. */ +#include "input_instruction_histogram.h" +static void input_instruction_histogram_record(uint32_t pc); +typedef struct {uint32_t pc,count;int used;uint64_t first_sequence,first_cycle,first_effective_cycle;uint32_t first_cache_tag;int first_cache_active;} InputInstructionHistogramRow; +static InputInstructionHistogramRow s_instruction_rows[16384]; +static uint32_t s_instruction_first,s_instruction_last,s_instruction_frame; +static int s_instruction_configured; +static int s_instruction_partial; +static void input_instruction_histogram_boundary(uint32_t frame); +static void input_instruction_histogram_exit(void) { + if(!g_input_instruction_histogram_active)return; + s_instruction_partial=1; + input_instruction_histogram_boundary(UINT32_MAX); +} +static uint64_t s_instruction_events,s_instruction_dropped,s_instruction_cycle; +typedef struct {uint32_t pc;uint64_t sequence,cycle,effective_cycle;uint32_t cache_tag;int cache_active;} InputInstructionSiteRow; +typedef struct {int precise,in_exception;uint32_t istat,imask,last_block,last_committed;uint64_t slices,takes;} InputInstructionContextRow; +static InputInstructionContextRow s_instruction_context_rows[8192]; +extern int g_precise_mode; +extern uint32_t i_stat,i_mask,g_slice_last_block,g_slice_last_committed; +extern uint64_t g_slice_fired,g_slice_irq_taken; +extern int psx_get_in_exception(void); +extern uint32_t g_psx_cyc_batch; +extern uint32_t *g_psx_cyc_local_acc; +extern uint32_t g_psx_icache_tv[1024]; +extern int g_psx_icache_active; +/* Observe the pending clock without publishing it or servicing a device. */ +static uint64_t input_instruction_effective_cycle(void) { + return psx_cycle_count + g_psx_cyc_batch + + (g_psx_cyc_local_acc ? (uint64_t)*g_psx_cyc_local_acc : 0u); +} +static uint32_t s_instruction_sites[16],s_instruction_site_count; +static InputInstructionSiteRow s_instruction_site_rows[8192]; +static uint32_t s_instruction_site_retained,s_instruction_site_dropped; +static int input_instruction_sites_parse(const char *p) { + s_instruction_site_count=0; + if(!p)return 1; + for(;;) { + uint32_t pc=0; + if(s_instruction_site_count==16)return 0; + for(unsigned i=0;i<8;i++) { + unsigned c=(unsigned char)*p++,v; + if(c>='0' && c<='9')v=c-'0'; + else if(c>='A' && c<='F')v=c-'A'+10; + else if(c>='a' && c<='f')v=c-'a'+10; + else return 0; + pc=(pc<<4)|v; + } + if(pc&3)return 0; + for(unsigned i=0;i7 || last>60000)return 0; + if(!input_instruction_sites_parse(sites))return 0; + if(!s_instruction_configured && atexit(input_instruction_histogram_exit))return 0; + s_instruction_first=first;s_instruction_last=last;s_instruction_configured=1; + g_input_instruction_histogram_callback=input_instruction_histogram_record; + /* Configuration runs before guest execution. Interval zero ends at the + * first runtime VBlank; it previously fell outside every fetch capture. */ + if(first==0)input_instruction_histogram_boundary(0); + return 1; +} +static void input_instruction_histogram_record(uint32_t pc) { + if(!g_input_instruction_histogram_active || g_ls_replay_active)return; + if(s_instruction_events==1000000){s_instruction_dropped++;return;} + for(unsigned i=0;i>2],g_psx_icache_active}; + s_instruction_context_rows[n]=(InputInstructionContextRow){g_precise_mode,psx_get_in_exception(),i_stat,i_mask,g_slice_last_block,g_slice_last_committed,g_slice_fired,g_slice_irq_taken}; + } + break; + } + uint32_t slot=((pc>>2)*2654435761u)&16383u; + for(unsigned n=0;n<16384;n++,slot=(slot+1)&16383u) { + InputInstructionHistogramRow *row=&s_instruction_rows[slot]; + if(!row->used || row->pc==pc){ + if(!row->used){row->first_sequence=s_instruction_events;row->first_cycle=psx_cycle_count;row->first_effective_cycle=input_instruction_effective_cycle();row->first_cache_tag=g_psx_icache_tv[(pc & 0xFFCu)>>2];row->first_cache_active=g_psx_icache_active;} + row->used=1;row->pc=pc;row->count++;s_instruction_events++;return; + } + } + s_instruction_dropped++; +} +static void input_instruction_histogram_boundary(uint32_t frame) { + if(g_input_instruction_histogram_active) { + const char *prefix=s_instruction_partial?"partial-":""; + char name[64];snprintf(name,sizeof(name),"%sinstructions-%06u.tsv",prefix,s_instruction_frame); + FILE *f=input_route_observer_output(name); + fprintf(f,"# retained=%llu dropped=%llu start_cycle=%llu end_cycle=%llu\npc\tcount\n", + (unsigned long long)s_instruction_events,(unsigned long long)s_instruction_dropped, + (unsigned long long)s_instruction_cycle,(unsigned long long)psx_cycle_count); + for(unsigned i=0;i<16384;i++)if(s_instruction_rows[i].used) + fprintf(f,"%08X\t%u\n",s_instruction_rows[i].pc,s_instruction_rows[i].count); + if(fclose(f)){fprintf(stderr,"instruction histogram close failed\n");exit(4);} + snprintf(name,sizeof(name),"%sinstruction-first-%06u.tsv",prefix,s_instruction_frame); + f=input_route_observer_output(name); + fprintf(f,"# first observed cache-fetch site per PC; not complete retired execution; effective clock includes unpublished charges\npc\tfirst_sequence\tfirst_cycle\teffective_cycle\tcache_tag\tcache_active\n"); + for(unsigned i=0;i<16384;i++)if(s_instruction_rows[i].used) + fprintf(f,"%08X\t%llu\t%llu\t%llu\t%08X\t%d\n",s_instruction_rows[i].pc, + (unsigned long long)s_instruction_rows[i].first_sequence, + (unsigned long long)s_instruction_rows[i].first_cycle, + (unsigned long long)s_instruction_rows[i].first_effective_cycle, + s_instruction_rows[i].first_cache_tag,s_instruction_rows[i].first_cache_active); + if(fclose(f)){fprintf(stderr,"histogram first-site close failed\n");exit(4);} + if(s_instruction_site_count) { + snprintf(name,sizeof(name),"%sinstruction-sites-%06u.tsv",prefix,s_instruction_frame); + f=input_route_observer_output(name); + fprintf(f,"# selected cache-fetch sites; retained=%u dropped=%u; cycles before fetch; effective clock includes unpublished charges\npc\tsequence\tcycle\teffective_cycle\tcache_tag\tcache_active\n",s_instruction_site_retained,s_instruction_site_dropped); + for(unsigned i=0;ipc,(unsigned long long)row->sequence,(unsigned long long)row->cycle,(unsigned long long)row->effective_cycle,row->cache_tag,row->cache_active); + } + if(fclose(f)){fprintf(stderr,"instruction sites close failed\n");exit(4);} + snprintf(name,sizeof(name),"%sinstruction-context-%06u.tsv",prefix,s_instruction_frame); + f=input_route_observer_output(name); + fprintf(f,"# passive context at selected pre-fetch sites; same sequence as instruction-sites; no device service\npc\tsequence\tprecise\tin_exception\ti_stat\ti_mask\tslice_block\tslice_committed\tslices\ttakes\n"); + for(unsigned i=0;ipc,(unsigned long long)site->sequence,row->precise,row->in_exception,row->istat,row->imask,row->last_block,row->last_committed,(unsigned long long)row->slices,(unsigned long long)row->takes); + } + if(fclose(f)){fprintf(stderr,"instruction context close failed\n");exit(4);} + } + snprintf(name,sizeof(name),"%sdma-at-%06u.json",prefix,s_instruction_frame); + f=input_route_observer_output(name); + debug_server_freeze_dump_dma_trace_json(f,128); + if(fclose(f)){fprintf(stderr,"histogram DMA context close failed\n");exit(4);} + snprintf(name,sizeof(name),"%ssio-at-%06u.json",prefix,s_instruction_frame); + f=input_route_observer_output(name); + debug_server_freeze_dump_sio_pc_json(f,128); + if(fclose(f)){fprintf(stderr,"histogram SIO context close failed\n");exit(4);} + g_input_instruction_histogram_active=0; + } + if(s_instruction_configured && frame>=s_instruction_first && frame<=s_instruction_last) { + memset(s_instruction_rows,0,sizeof(s_instruction_rows)); + s_instruction_frame=frame;s_instruction_events=s_instruction_dropped=0; + s_instruction_site_retained=s_instruction_site_dropped=0; + s_instruction_cycle=psx_cycle_count;g_input_instruction_histogram_active=1; + } +} diff --git a/runtime/include/input_route_field_clock.h b/runtime/include/input_route_field_clock.h new file mode 100644 index 000000000..15530365c --- /dev/null +++ b/runtime/include/input_route_field_clock.h @@ -0,0 +1,43 @@ +#ifndef PSX_INPUT_ROUTE_FIELD_CLOCK_H +#define PSX_INPUT_ROUTE_FIELD_CLOCK_H + +#include + +/* Cold-boot comparison clock, independent of guest memory and input contents. + * Behavioral constants: original Octoshock 2.2.2 GPU, NTSC hardware clock + * ratio 103896/65536; alternating 3412/3413 GPU clocks per scanline; + * 263 progressive lines, 263/262 interlaced lines. This is a field-duration + * profile, not its complete scanline/IRQ/CPU timing implementation. + * Independently written arithmetic; no reference implementation copied. + */ +typedef struct InputRouteFieldClock { + uint32_t remainder; + uint32_t line_phase; + uint32_t field; + uint32_t current_cycles; +} InputRouteFieldClock; + +static inline void input_route_field_clock_select(InputRouteFieldClock *s, + int interlaced) { + uint32_t lines = interlaced ? 263u - s->field : 263u; + uint32_t gpu_clocks = lines * 3412u + (lines + s->line_phase) / 2u; + uint64_t numerator = (uint64_t)gpu_clocks * 65536u; + /* Keep cumulative deadlines at ceil(total GPU clocks / clock ratio). + * Represent the remainder as unused fractional CPU-cycle credit. */ + numerator -= s->remainder; + s->current_cycles = (uint32_t)((numerator + 103895u) / 103896u); + s->remainder = (uint32_t)((uint64_t)s->current_cycles * 103896u - numerator); + s->line_phase = (s->line_phase + lines) & 1u; +} + +static inline void input_route_field_clock_reset(InputRouteFieldClock *s) { + s->remainder = s->line_phase = s->field = 0u; + input_route_field_clock_select(s, 0); +} + +static inline void input_route_field_clock_next(InputRouteFieldClock *s, + int interlaced) { + s->field = interlaced ? !s->field : 0u; + input_route_field_clock_select(s, interlaced); +} +#endif diff --git a/runtime/include/input_route_file.h b/runtime/include/input_route_file.h new file mode 100644 index 000000000..676d1a918 --- /dev/null +++ b/runtime/include/input_route_file.h @@ -0,0 +1,52 @@ +#ifndef PSX_INPUT_ROUTE_FILE_H +#define PSX_INPUT_ROUTE_FILE_H + +/* Bounded PSXRTI1 digital route reader. Source-owned, no retail data. + * Parse into caller staging storage; publish only after the complete file + * passes. No guest state, clocks or live input are touched by this parser. */ +#include +#include +#include + +#define INPUT_ROUTE_MAX_STEPS 4096u +#define INPUT_ROUTE_MAX_FRAMES 1000000u +typedef struct { uint32_t frames; uint16_t buttons; } InputRouteStep; + +static inline uint32_t input_route_le32(const unsigned char *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | + ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +static inline const char *input_route_read(FILE *f, InputRouteStep *steps, + uint32_t *step_count, + uint32_t *frame_count) +{ + unsigned char h[24], r[8]; + uint32_t n, count = 0; + *step_count = *frame_count = 0; + if (!f || fread(h, 1, sizeof(h), f) != sizeof(h)) return "short header"; + if (memcmp(h, "PSXRTI1\0", 8) || input_route_le32(h+8) != 1 || + input_route_le32(h+12) != 8) return "header identity"; + n = input_route_le32(h+16); + if (input_route_le32(h+20) || !n || n > INPUT_ROUTE_MAX_FRAMES) + return "frame count"; + for (uint32_t i = 0; i < n; ++i) { + uint16_t pad; + if (fread(r, 1, sizeof(r), f) != sizeof(r)) return "short record"; + if (input_route_le32(r) != i+1 || r[6] || r[7]) + return "record sequence/reserved"; + pad = (uint16_t)((unsigned)r[4] | ((unsigned)r[5] << 8)); + if (count && steps[count-1].buttons == pad) ++steps[count-1].frames; + else { + if (count == INPUT_ROUTE_MAX_STEPS) return "step capacity"; + steps[count].buttons = pad; + steps[count++].frames = 1; + } + } + if (fgetc(f) != EOF || ferror(f)) return "trailing bytes/read error"; + *step_count = count; + *frame_count = n; + return NULL; +} +#endif diff --git a/runtime/include/input_route_observer.h b/runtime/include/input_route_observer.h new file mode 100644 index 000000000..23453eaa2 --- /dev/null +++ b/runtime/include/input_route_observer.h @@ -0,0 +1,11 @@ +#ifndef PSX_INPUT_ROUTE_OBSERVER_H +#define PSX_INPUT_ROUTE_OBSERVER_H +#include +#include +FILE *input_route_observer_output(const char *name); +int input_route_observer_init(uint32_t total); +void input_route_observer_set_end(uint32_t total); +void input_route_observer_boundary(uint32_t completed, uint64_t runtime_frame); +void input_route_observer_input(uint16_t buttons); +void input_route_observer_applied(uint16_t buttons, int connected, int analog); +#endif diff --git a/runtime/include/input_route_raster_clock.h b/runtime/include/input_route_raster_clock.h new file mode 100644 index 000000000..cd2b0cc3e --- /dev/null +++ b/runtime/include/input_route_raster_clock.h @@ -0,0 +1,108 @@ +#ifndef PSX_INPUT_ROUTE_RASTER_CLOCK_H +#define PSX_INPUT_ROUTE_RASTER_CLOCK_H +#include +#include + +/* Independently expressed original Octoshock 2.2.2 NTSC raster timing. + * Optional comparison clock only: no GPU rendering, implicit timer synchronization, + * readiness flags, input policy or guest memory is implemented here. + * Source gpu.cpp constants/transition rules; no reference source text copied. + */ +typedef struct InputRouteRasterClock { + uint64_t cycle, last_rise; + uint32_t fraction, remaining, phase, alternate; + uint32_t scanline, lines, field, mode, start, end, blank; + uint32_t rises; + uint32_t y_start, y_offset, readout_y, readout_field; +} InputRouteRasterClock; + +static inline void input_route_raster_reset(InputRouteRasterClock *s) { + memset(s, 0, sizeof(*s)); + s->remaining=3212; s->lines=263; s->start=16; s->end=256; s->blank=1; +} + +typedef void (*InputRouteRasterEvent)(void *, uint64_t, unsigned, int); +/* Events: 1 = H-retrace rising edge, 2 = VBlank level change. The callback + * observes ordered edge deadlines. Null keeps the original scalar-only path. */ +static inline void input_route_raster_advance_observed(InputRouteRasterClock *s, uint32_t cycles, InputRouteRasterEvent event, void *context) { + uint64_t end_cycle=s->cycle+cycles; + uint64_t total=(uint64_t)s->fraction+(uint64_t)cycles*103896u; + uint64_t ticks=total>>16; + s->fraction=(uint32_t)(total&65535u); + while (ticks) { + uint32_t step=ticksremaining ? (uint32_t)ticks : s->remaining; + ticks-=step; s->remaining-=step; + if (s->remaining) continue; + s->phase^=1; + if (s->phase) { + if(event) event(context,end_cycle-(ticks*65536u+s->fraction)/103896u,1,s->blank); + s->remaining=200; continue; + } + s->remaining=3212+s->alternate; s->alternate^=1; + s->scanline=(s->scanline+1)%s->lines; + if (s->scanline==s->lines-1) s->field=(s->mode&32) ? !s->field : 0; + if (!s->scanline) { + if (s->mode&32) s->lines=263-s->field; + else { s->field=0; s->lines=263; } + } + uint32_t old_blank=s->blank; + if (s->scanline==s->end && !s->blank) { + s->blank=1; s->y_offset=0; + s->readout_field=(s->mode&0x24)==0x24 ? !s->field : 0; + } + if (s->scanline==s->start && s->blank) s->blank=0; + if(event && old_blank!=s->blank) + event(context,end_cycle-(ticks*65536u+s->fraction)/103896u,2,s->blank); + if (!old_blank && s->blank) { + s->rises++; + s->last_rise=end_cycle-(ticks*65536u+s->fraction)/103896u; + } + if ((s->mode&0x24)==0x24) + s->readout_y=(s->y_start+2*s->y_offset+(s->blank ? 0 : s->readout_field))&511u; + else s->readout_y=(s->y_start+s->y_offset)&511u; + if (!s->blank) s->y_offset=(s->y_offset+1)&511u; + } + s->cycle=end_cycle; +} + +static inline void input_route_raster_advance(InputRouteRasterClock *s, uint32_t cycles) { + input_route_raster_advance_observed(s,cycles,0,0); +} + +/* Call after the existing device-MMIO synchronization. Unknown non-raster + * GP1 commands leave this clock unchanged. PAL is an explicit rejection. + */ +static inline int input_route_raster_gp1(InputRouteRasterClock *s, uint32_t word) { + uint32_t command=(word>>24)&63u; + if (!command) { s->mode=0; s->start=16; s->end=256; s->y_start=0; } + else if (command==5) s->y_start=(word>>10)&511u; + else if (command==7) { s->start=word&1023u; s->end=(word>>10)&1023u; } + else if (command==8) { + if (word&8) return 0; + s->mode=word&255u; + } + return 1; +} + +/* Independent field and display-line parity bits from the raster state. + * This does not assert command/FIFO/DMA readiness or alter the raster clock. + */ +static inline uint32_t input_route_raster_status(const InputRouteRasterClock *s) { + return ((s->readout_y&1u)<<31) | ((!s->field)<<13); +} + +/* Deadline search over a copy. Cache this at the runtime boundary, invalidate + * on GP1 reset/range/mode writes and after a rising edge. No fabricated edge + * for an invalid/empty display range: UINT32_MAX means none within two fields. + */ +static inline uint32_t input_route_raster_until_rise(const InputRouteRasterClock *s) { + InputRouteRasterClock t=*s; + for (uint32_t i=0; i<1060; ++i) { + uint64_t numerator=(uint64_t)t.remaining*65536u-t.fraction; + uint32_t cycles=(uint32_t)((numerator+103895u)/103896u); + input_route_raster_advance(&t,cycles); + if (t.rises!=s->rises) return (uint32_t)(t.last_rise-s->cycle); + } + return UINT32_MAX; +} +#endif diff --git a/runtime/include/input_update_clock.h b/runtime/include/input_update_clock.h new file mode 100644 index 000000000..b1c2b7616 --- /dev/null +++ b/runtime/include/input_update_clock.h @@ -0,0 +1,134 @@ +#ifndef PSX_INPUT_UPDATE_CLOCK_H +#define PSX_INPUT_UPDATE_CLOCK_H +#include +/* Experimental two-stage controller clock. No guest state and no title data. + * The caller must verify the packet at its configured acceptance marker. + * A poll prepares one packet ahead; the later marker commits the older one. */ +typedef struct { + const uint16_t *words; + uint32_t count, accepted; + int primed, predicted, failed; + uint64_t last_poll, last_accept; + uint16_t queued_word; + const uint8_t *contexts; /* Optional source event types: 0 normal, 1 auxiliary. */ + int compatible_contexts, context_failed; + int neutral_refresh_stage; /* 1 hold poll, 2 held acceptance, 3 refresh poll. */ + uint64_t neutral_hold_frame; + uint32_t neutral_refreshes; + uint16_t protected_raw_mask; + uint16_t source_raw_hold, source_raw_press, native_raw_hold, native_raw_press; + uint8_t last_source_context, last_native_context; +} InputUpdateClock; +/* Optional prediction only. The later packet/context checks remain authoritative. + * A gate of one may be reset by a continuing main loop after the early poll. */ +static inline int input_update_predict(const InputUpdateClock *s, uint64_t frame, + uint32_t gate, int previous_accept) { + return gate==0 || (previous_accept && s->primed && gate==1 && + frame>s->last_accept && frame-s->last_accept==1); +} +static inline int input_update_init(InputUpdateClock *s, const uint16_t *w, uint32_t n) { + *s = (InputUpdateClock){0}; + if (!w || n < 2 || w[0] != 0xffff || w[1] != 0xffff) return 0; + s->words=w; s->count=n; + s->source_raw_hold=s->native_raw_hold=0xffff; return 1; +} +static inline int input_update_set_contexts(InputUpdateClock *s, const uint8_t *contexts) { + if (!contexts || s->primed || s->failed || contexts[0] || contexts[1]) return 0; + for (uint32_t i=0;icount;i++) if (contexts[i]>1) return 0; + s->contexts=contexts; return 1; +} +static inline int input_update_poll(InputUpdateClock *s, uint64_t frame, int open, uint16_t *word) { + *word=0xffff; + if (s->failed) return 0; + if (!s->primed || s->accepted == s->count) return 1; + /* A prediction may be wrong without changing the packet: equal adjacent + * held/neutral samples require no timing decision. Fail only when the + * packet already in flight would differ from the required sample. */ + if ((s->predicted && s->queued_word != s->words[s->accepted]) || + frame <= s->last_poll) { s->failed=1; return 0; } + s->last_poll=frame; s->predicted=!!open; + uint32_t next=s->accepted + (open ? 1u : 0u); + if (next < s->count) *word=s->words[next]; + s->queued_word=*word; + return 1; +} +/* Modes 2/3 are a declared pair, never a prediction heuristic. Only a + * neutral normal sample before a protected normal press can arm the hold. */ +static inline int input_update_poll_mode(InputUpdateClock *s,uint64_t frame,int mode,uint16_t *word) { + if(mode==2) { + uint16_t raw_difference=(uint16_t)((s->source_raw_hold^s->native_raw_hold)| + (s->source_raw_press^s->native_raw_press)); + if(s->neutral_refresh_stage || !s->primed || !s->compatible_contexts || + s->accepted+1>=s->count || s->words[s->accepted]!=0xffff || + s->contexts[s->accepted] || s->contexts[s->accepted+1] || + !((uint16_t)~s->words[s->accepted+1]&s->protected_raw_mask) || + (raw_difference & s->protected_raw_mask) || + (raw_difference && (s->last_source_context!=0 || s->last_native_context!=1))) {s->failed=1;return 0;} + /* The pending ordinary neutral can release a preceding held sample + * later in this frame. A just-accepted compatible auxiliary may leave + * unprotected raw fields different; the mandatory ordinary release + * reconciles those fields before the all-zero refresh checks. Other + * unequal predecessors and any protected difference still reject. */ + if(!input_update_poll(s,frame,0,word))return 0; + s->neutral_refresh_stage=1;s->neutral_hold_frame=frame;return 1; + } + if(mode==3) { + if(s->neutral_refresh_stage!=2 || frame!=s->neutral_hold_frame+1) {s->failed=1;return 0;} + if(!input_update_poll(s,frame,0,word))return 0; + s->neutral_refresh_stage=3;return 1; + } + if(s->neutral_refresh_stage || mode<0 || mode>1) {s->failed=1;return 0;} + return input_update_poll(s,frame,mode,word); +} +static inline int input_update_neutral_refresh(InputUpdateClock *s,uint64_t frame,uint16_t word, + uint32_t old_held,uint32_t new_held,uint32_t pressed) { + if(s->neutral_refresh_stage!=3 || frame!=s->last_poll || word!=0xffff || + old_held || new_held || pressed || s->source_raw_hold!=0xffff || + s->native_raw_hold!=0xffff || s->source_raw_press || s->native_raw_press) { + s->failed=1;return 0; + } + s->neutral_refresh_stage=0;++s->neutral_refreshes;return 1; +} +static inline int input_update_accept(InputUpdateClock *s, uint64_t frame, uint16_t word) { + if (s->failed || s->accepted == s->count || s->neutral_refresh_stage>=2) return 0; + if (!s->primed) { + if (word != 0xffff) { s->failed=1; return 0; } + s->primed=1; s->last_poll=frame; + } else if (frame != s->last_poll || frame == s->last_accept || + s->queued_word != (s->neutral_refresh_stage==1 ? 0xffff : + s->accepted + 1 < s->count ? s->words[s->accepted + 1] : 0xffff)) { + s->failed=1; return 0; + } + if (word != s->words[s->accepted]) { s->failed=1; return 0; } + if(s->neutral_refresh_stage==1) { + if(word!=0xffff || frame!=s->neutral_hold_frame) {s->failed=1;return 0;} + s->neutral_refresh_stage=2; + } + ++s->accepted; s->predicted=0; s->last_accept=frame; return 1; +} +static inline int input_update_auxiliary(InputUpdateClock *s, uint16_t word, uint32_t held) { + if (s->failed || word != 0xffff || held != 0) { s->failed=1; return 0; } + return 1; /* Declared neutral refresh; never advances the normal-update clock. */ +} +static inline int input_update_context_accept(InputUpdateClock *s, uint64_t frame, + uint16_t word, uint8_t context) { + if (!s->contexts || s->accepted==s->count || context>1 || + (!s->compatible_contexts && context!=s->contexts[s->accepted])) { + s->context_failed=s->failed=1; return 0; + } + if(s->neutral_refresh_stage==1 && context) {s->failed=1;return 0;} + uint8_t source=s->contexts[s->accepted]; + uint16_t sh=source ? s->source_raw_hold : word; + uint16_t sp=source ? s->source_raw_press : (uint16_t)(s->source_raw_hold & ~word); + uint16_t nh=context ? s->native_raw_hold : word; + uint16_t np=context ? s->native_raw_press : (uint16_t)(s->native_raw_hold & ~word); + if (s->compatible_contexts && (((sh^nh)|(sp^np)) & s->protected_raw_mask)) { + s->context_failed=s->failed=1; return 0; + } + if (!input_update_accept(s,frame,word)) return 0; + s->source_raw_hold=sh;s->source_raw_press=sp; + s->native_raw_hold=nh;s->native_raw_press=np; + s->last_source_context=source;s->last_native_context=context; + return 1; +} +#endif diff --git a/runtime/include/input_update_replay.h b/runtime/include/input_update_replay.h new file mode 100644 index 000000000..b262d78f8 --- /dev/null +++ b/runtime/include/input_update_replay.h @@ -0,0 +1,223 @@ +/* Included privately by debug_server.c after the ordinary route fields. + * Test-supplied configuration only; no game addresses are compiled in. */ +#include "input_update_clock.h" +#include "psx_sha256.h" +static InputUpdateClock s_update; +static uint32_t s_update_config[12]; +static int s_update_enabled; +static int s_update_previous_accept; +static uint32_t s_refresh_guard[4]; /* pressed RAM/PC, raw-held RAM, raw-pressed RAM */ +static int s_refresh_guard_enabled,s_refresh_wait_pressed; +static FILE *s_update_log; +static uint64_t s_update_last_accept; +static psx_sha256_ctx s_update_hash; +static uint32_t s_update_decision_frames[256],s_update_decision_open[256],s_update_decision_count; +static uint16_t update_peek16(uint32_t a) { + return (uint16_t)(g_psx_ram[a] | ((uint16_t)g_psx_ram[a+1]<<8)); +} +static void update_fail(const char *reason) { + if (s_update_log) { + fprintf(s_update_log,"{\"kind\":\"failure\",\"frame\":%llu,\"accepted\":%u,\"reason\":\"%s\"}\n", + (unsigned long long)s_frame_count,s_update.accepted,reason); + fflush(s_update_log); + /* Preserve the existing bounded recorder on fail-stop as well as EOF. + * This observes only test-configured fields and never resumes execution. */ + const uint32_t watched[] = {s_update_config[0],s_update_config[3], + s_update_config[4],s_update_config[5]}; + FILE *access = input_route_observer_output("update-failure-access.jsonl"); + if (access) { + debug_server_dump_watched_writes(access,watched,4); + fclose(access); + } + } + fprintf(stderr,"accepted-update replay failed: %s\n",reason); exit(4); +} +/* marker RAM, marker PC, marker RA, gate RAM(u32), packet RAM(u16), + * header RAM(u16), expected header, total VBlank watchdog, stalled watchdog. */ +static int update_configure(const InputRouteStep *steps,uint32_t count,uint32_t total) { + const char *p=getenv("PSX_INPUT_UPDATE_CLOCK"); + if (!p) return 1; + for (unsigned i=0;i<12;i++) { + char *end; if (*p<'0'||*p>'9') return 0; + unsigned long long v=strtoull(p,&end,0); + if (v>0xffffffffull || end==p || (i==11 ? *end!=0 : *end!=',')) return 0; + s_update_config[i]=(uint32_t)v; p=end+(i<11); + } + const unsigned ram_fields[]={0,3,4,5}; + for (unsigned i=0;i<4;i++) { + uint32_t a=s_update_config[ram_fields[i]]; + if (a>0x1ffffcu || (a&1u)) return 0; + } + if ((s_update_config[1]&3)||(s_update_config[2]&3)|| + (s_update_config[3]&3)||s_update_config[6]>0xffffu|| + !s_update_config[7]||s_update_config[7]>60000|| + !s_update_config[8]||s_update_config[8]>s_update_config[7]||s_update_config[9]>31) return 0; + uint16_t *words=malloc(total*sizeof(*words)); if (!words) return 0; + uint32_t n=0; + for (uint32_t i=0;i0xffff) return 0; + s_update.compatible_contexts=1;s_update.protected_raw_mask=(uint16_t)v; + } + const char *predictor=getenv("PSX_INPUT_UPDATE_PREDICTOR"); + if (predictor) { + if (strcmp(predictor,"previous-accept")) return 0; + s_update_previous_accept=1; + } + const char *guard=getenv("PSX_INPUT_UPDATE_REFRESH_GUARD"); + if(guard) { + if(!s_update.compatible_contexts)return 0; + for(unsigned i=0;i<4;i++) { + char *end;unsigned long v=strtoul(guard,&end,0); + if(end==guard || (i==3?*end!=0:*end!=',') || v>0xfffffffful || + (i==1 ? (v&3u)!=0 : (v>0x1ffffeu || (v&1u))))return 0; + s_refresh_guard[i]=(uint32_t)v;guard=end+(i<3); + } + s_refresh_guard_enabled=1; + } + const char *decisions=getenv("PSX_INPUT_UPDATE_DECISIONS"); + if (decisions) { + FILE *f=fopen(decisions,"rb"); char line[128]; uint32_t previous=0; + if (!f) return 0; + if (!fgets(line,sizeof(line),f)) {fclose(f);return 0;} + int version2=!strcmp(line,"PSX-UPDATE-DECISIONS2\n"); + if(!version2 && strcmp(line,"PSX-UPDATE-DECISIONS1\n")) {fclose(f);return 0;} + while (fgets(line,sizeof(line),f)) { + unsigned frame,open; char extra; + if (sscanf(line,"%u %u %c",&frame,&open,&extra)!=2 || frame<=previous || + frame>s_update_config[7] || open>(version2?3u:1u) || s_update_decision_count==256) {fclose(f);return 0;} + previous=frame; + s_update_decision_frames[s_update_decision_count]=frame; + s_update_decision_open[s_update_decision_count++]=open; + } + if(ferror(f)){fclose(f);return 0;} if(fclose(f))return 0; + } + for(uint32_t i=0;i=2 && !s_refresh_guard_enabled)return 0; + if(mode==2 && (i+1==s_update_decision_count || s_update_decision_open[i+1]!=3 || + s_update_decision_frames[i+1]!=s_update_decision_frames[i]+1))return 0; + if(mode==3 && (!i || s_update_decision_open[i-1]!=2 || + s_update_decision_frames[i]!=s_update_decision_frames[i-1]+1))return 0; + } + s_update_enabled=1; psx_sha256_init(&s_update_hash); return 1; +} +uint16_t debug_server_update_poll(int slot,uint16_t buttons,int analog) { + if (!s_update_enabled || slot!=0) return buttons; + if (analog) update_fail("non-digital P1 poll"); + uint32_t a=s_update_config[3]; + uint32_t gate=update_peek16(a)|((uint32_t)update_peek16(a+2)<<16); + uint16_t word; + int open=input_update_predict(&s_update,s_frame_count,gate,s_update_previous_accept),override=0; + for(uint32_t i=0;igpr[s_update_config[9]] : UINT32_MAX; + if (selector!=s_update_config[10]) { + fprintf(s_update_log,"{\"kind\":\"auxiliary_store\",\"frame\":%llu,\"word\":%u,\"old_held\":%u,\"new_held\":%u,\"selector\":%u}\n", + (unsigned long long)s_frame_count,word,old_val,new_val,selector); + fflush(s_update_log); + if (selector!=s_update_config[11]) update_fail("unexpected auxiliary context/input"); + if (!s_update.contexts) { + if (!input_update_auxiliary(&s_update,word,new_val)) update_fail("unexpected auxiliary context/input"); + return; + } + } + if(s_update.neutral_refresh_stage==3) { + if(!s_refresh_guard_enabled || s_refresh_wait_pressed || selector!=s_update_config[11] || + update_peek16(s_refresh_guard[2]) || update_peek16(s_refresh_guard[3]) || + !input_update_neutral_refresh(&s_update,s_frame_count,word,old_val,new_val,update_peek16(s_refresh_guard[0]))) + update_fail("declared neutral refresh mismatch"); + s_refresh_wait_pressed=1;return; + } + if(s_refresh_wait_pressed)update_fail("unexpected acceptance before refresh pressed store"); + uint8_t context=selector==s_update_config[10] ? 0u : 1u; + if (s_update.contexts && !s_update.compatible_contexts && s_update.contexts[s_update.accepted]!=context) + update_fail("source event context mismatch"); + if (!(s_update.contexts ? input_update_context_accept(&s_update,s_frame_count,word,context) : + input_update_accept(&s_update,s_frame_count,word))) { + if (s_update.primed && !s_update.context_failed && !s_update.predicted && s_update.last_poll==s_frame_count && word==s_update.words[s_update.accepted]) { + fprintf(s_update_log,"{\"kind\":\"decision_failure\",\"decision_frame\":%llu,\"observed_open\":1,\"accepted\":%u}\n", + (unsigned long long)s_frame_count,s_update.accepted);fflush(s_update_log); + } + if(s_refresh_guard_enabled && s_update.context_failed && context==1 && + s_update.accepted && s_update.last_accept+1==s_frame_count && + s_update.words[s_update.accepted-1]==0xffff && !s_update.contexts[s_update.accepted] && + ((uint16_t)~word&s_update.protected_raw_mask) && !old_val && + !update_peek16(s_refresh_guard[0]) && !update_peek16(s_refresh_guard[2]) && !update_peek16(s_refresh_guard[3])) { + fprintf(s_update_log,"{\"kind\":\"neutral_refresh_candidate\",\"hold_frame\":%llu,\"refresh_frame\":%llu,\"accepted\":%u}\n", + (unsigned long long)s_update.last_accept,(unsigned long long)s_frame_count,s_update.accepted);fflush(s_update_log); + } + update_fail(s_update.context_failed ? "protected raw input effect mismatch" : "accepted packet/prediction mismatch"); + } + uint8_t b[2]={(uint8_t)word,(uint8_t)(word>>8)}; + psx_sha256_update(&s_update_hash,b,2); s_update_last_accept=s_frame_count; + fprintf(s_update_log,"{\"kind\":\"accept\",\"frame\":%llu,\"sequence\":%u,\"word\":%u}\n", + (unsigned long long)s_frame_count,s_update.accepted,word); + if (s_update.contexts) + fprintf(s_update_log,"{\"kind\":\"accepted_context\",\"frame\":%llu,\"sequence\":%u,\"context\":%u}\n", + (unsigned long long)s_frame_count,s_update.accepted,context); + if (fflush(s_update_log)) update_fail("log flush"); + if (s_update.accepted==s_update.count) { + uint8_t hash[32]; char hex[65]; psx_sha256_final(&s_update_hash,hash); + for(unsigned i=0;i<32;i++) sprintf(hex+2*i,"%02x",hash[i]); + FILE *f=input_route_observer_output("update-input-end.json"); + fprintf(f,"{\"clock\":\"accepted-update-experimental\",\"frame\":%u,\"accepted_samples\":%u,\"accepted_words_sha256\":\"%s\"}\n", + s_input_route_consumed,s_update.accepted,hex); + if(fclose(f)) update_fail("end receipt close"); + input_route_observer_set_end(s_input_route_consumed); + } +} +static void update_boundary(void) { + if (!s_update_enabled || s_update.accepted==s_update.count) return; + if (s_frame_count>s_update_config[7] || + s_frame_count-s_update_last_accept>s_update_config[8]) update_fail("accepted-update watchdog"); +} diff --git a/runtime/include/interrupts.h b/runtime/include/interrupts.h index 5be1f54e3..ee35d762b 100644 --- a/runtime/include/interrupts.h +++ b/runtime/include/interrupts.h @@ -49,6 +49,9 @@ void psx_check_interrupts(struct CPUState* cpu); * raised by psx_advance_cycles(); this reports whether the comparatively * expensive delivery/scheduler path can have an architectural effect now. */ int psx_interrupt_delivery_needed(const struct CPUState* cpu); +/* Existing runtime compatibility delay, shared by precise take prediction. + * This reports policy state only; it does not advance or alter guest time. */ +int psx_interrupt_cooldown_active(void); void psx_interrupt_delivery_diag(uint64_t *need_defer, uint64_t *need_irq, uint64_t *skip_none, uint64_t *skip_sr, uint64_t *skip_cooldown, uint64_t *skip_nested); @@ -66,6 +69,10 @@ void psx_interrupt_check_path_diag(uint64_t *entry, uint64_t *fast_sr, /* Interrupt check with the compiled guest PC to resume if a game-installed * handler later RFEs to the sentinel outside the synchronous host window. */ void psx_check_interrupts_at(struct CPUState* cpu, uint32_t resume_pc); +/* Source-profile precise interpreter: accept a pending IRQ at a branch's + * unexecuted delay slot, fetching that slot while saving the branch as EPC. */ +int psx_check_interrupts_delay_slot(struct CPUState* cpu,uint32_t slot_pc, + uint32_t target,int taken,uint32_t instruction); int psx_interrupts_checked_at_current_cycle(uint32_t resume_pc); /* Dispatch-entry check for re-enterable compiled PCs. De-dupes an immediately * preceding check at the same guest PC/cycle so generated transfers can keep @@ -80,6 +87,10 @@ void psx_check_interrupts_dispatch_entry(struct CPUState* cpu, uint32_t resume_p void interrupts_advance_cycles(uint32_t cycles); void interrupts_service_scheduled_events(void); uint32_t interrupts_cycles_to_vblank(void); +/* Optional source raster comparison: called after an already synchronized GP1 + * write. Inactive profiles return immediately. No guest RAM is modified. */ +void interrupts_raster_gp1(uint32_t word); +int interrupts_raster_gpu_status(uint32_t *bits); /* While IRQ9 is enabled, expose the next 44.1-kHz sample as a first-class * device deadline so the CPU can observe and acknowledge an IRQ before the * following sample. UINT32_MAX means inactive. PSX_SPU_SAMPLE_EVENTS=0 is a @@ -101,6 +112,10 @@ uint32_t cycles_to_next_event(void); /* Query whether we are currently inside an exception handler dispatch. */ int psx_get_in_exception(void); +/* The source CPU's pending-interrupt opcode table still executes COP2. + * Unknown instruction addresses and the default profile remain eligible. */ +int psx_irq_opcode_eligible(uint32_t pc); + /* Most recent block-leader IRQ-check guest PC / compiled resume latch. * Used by the post-savestate freeze probe (vblank-time "where was the game"). */ uint32_t psx_last_irq_check_pc(void); diff --git a/runtime/include/overlay_api.h b/runtime/include/overlay_api.h index 2ceda1518..1633a9957 100644 --- a/runtime/include/overlay_api.h +++ b/runtime/include/overlay_api.h @@ -93,7 +93,9 @@ * host/DLL flavor mix — base-flavour DLLs and hosts are untouched, so * the version stays. The emit-content change (PGXP_*() macros in all * generated C) is covered by the codegen hash + CODEGEN_VER below. */ -#define PSX_OVERLAY_ABI_VERSION 21 +/* v22: instruction-boundary observation forwards to the host, including + * cached followers. DLL batches flush before observation; replay is excluded. */ +#define PSX_OVERLAY_ABI_VERSION 22 /* Process-lifetime overlay candidate capacity. Every accepted manifest F * record consumes one slot, even when another DLL carries an identical @@ -317,6 +319,10 @@ typedef struct { * load-bearing. The ABI version bump that arms this ships with the * emitter change (Phase 2 of ENHANCEMENTS.md G1 value propagation). */ const PGXPHooks *pgxp; + /* ABI v22. include_replay=1 queries installation for the fused-stub + * admission guard; zero queries whether a boundary must be observed now. */ + int (*cpu_step_boundary_enabled)(int include_replay); + void (*cpu_step_boundary)(CPUState *cpu, uint32_t address); } OverlayCallbacks; #ifdef __cplusplus diff --git a/runtime/include/overlay_dispatch_preamble.c.inc b/runtime/include/overlay_dispatch_preamble.c.inc index ac0e9fc5f..b55c85aa8 100644 --- a/runtime/include/overlay_dispatch_preamble.c.inc +++ b/runtime/include/overlay_dispatch_preamble.c.inc @@ -4,6 +4,11 @@ static OverlayCallbacks g_cbs; static uint32_t s_pending_cycles; +/* Generated fused-stub admission reads this as a predicate. Query the host's + * live installation state, including replay, rather than a DLL-local copy. */ +#define g_psx_cpu_step_boundary_callback \ + (g_cbs.cpu_step_boundary_enabled && g_cbs.cpu_step_boundary_enabled(1)) + #ifdef _WIN32 __declspec(dllexport) #else @@ -117,6 +122,13 @@ void psx_icache_fetch(CPUState *cpu, uint32_t addr) { overlay_flush_cycles(); if (g_cbs.icache_fetch) g_cbs.icache_fetch(cpu, addr); } +void psx_cpu_step_boundary(CPUState *cpu, uint32_t addr) { + if (g_cbs.cpu_step_boundary_enabled && + g_cbs.cpu_step_boundary_enabled(0) && g_cbs.cpu_step_boundary) { + overlay_flush_cycles(); + g_cbs.cpu_step_boundary(cpu, addr); + } +} void psx_muldiv_set(CPUState *cpu, uint32_t latency) { overlay_flush_cycles(); if (g_cbs.muldiv_set) g_cbs.muldiv_set(cpu, latency); diff --git a/runtime/include/psx_cyc.h b/runtime/include/psx_cyc.h index 4239c1236..3e8729139 100644 --- a/runtime/include/psx_cyc.h +++ b/runtime/include/psx_cyc.h @@ -216,6 +216,10 @@ extern uint8_t *g_psx_ram; extern int g_psx_load_delay; extern int g_ls_mode; extern volatile int g_ds_recording; +/* Active source upload DMA requires the host helper's live bus-read charge. */ +extern uint32_t g_dma_cpu_read_wait; +extern int g_ram_read_watch_active; +void debug_server_trace_ram_read_watch(uint32_t phys, uint32_t val); int psx_load_delay_enabled(void); uint32_t psx_cyc_load_word_slow(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask); uint16_t psx_cyc_load_half_slow(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask); @@ -235,7 +239,7 @@ static inline uint32_t psx_cyc_load_word(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) { #ifdef PSX_ENABLE_BLOCK_CYCLES uint32_t phys = addr & 0x1FFFFFFFu; - if (g_ls_mode == 0 && !g_ds_recording && phys < 0x00800000u) { + if (g_ls_mode == 0 && !g_ds_recording && !g_dma_cpu_read_wait && phys < 0x00800000u) { if (g_psx_load_delay < 0) (void)psx_load_delay_enabled(); if (g_psx_load_delay) { psx_cyc_base(cpu); @@ -251,6 +255,8 @@ static inline uint32_t psx_cyc_load_word(CPUState* cpu, uint32_t addr, } uint32_t value; memcpy(&value, g_psx_ram + (phys & 0x1FFFFFu), sizeof(value)); + if (g_ram_read_watch_active) + debug_server_trace_ram_read_watch(phys & 0x1FFFFFu, value); return value; } return psx_cyc_load_word_slow(cpu, addr, rt, reg_mask); @@ -265,7 +271,7 @@ static inline uint16_t psx_cyc_load_half(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) { #ifdef PSX_ENABLE_BLOCK_CYCLES uint32_t phys = addr & 0x1FFFFFFFu; - if (g_ls_mode == 0 && !g_ds_recording && phys < 0x00800000u) { + if (g_ls_mode == 0 && !g_ds_recording && !g_dma_cpu_read_wait && phys < 0x00800000u) { if (g_psx_load_delay < 0) (void)psx_load_delay_enabled(); if (g_psx_load_delay) { psx_cyc_base(cpu); @@ -281,6 +287,8 @@ static inline uint16_t psx_cyc_load_half(CPUState* cpu, uint32_t addr, } uint16_t value; memcpy(&value, g_psx_ram + (phys & 0x1FFFFFu), sizeof(value)); + if (g_ram_read_watch_active) + debug_server_trace_ram_read_watch(phys & 0x1FFFFFu, value); return value; } return psx_cyc_load_half_slow(cpu, addr, rt, reg_mask); diff --git a/runtime/include/psx_icache.h b/runtime/include/psx_icache.h index e1d1b722c..f26bd4364 100644 --- a/runtime/include/psx_icache.h +++ b/runtime/include/psx_icache.h @@ -2,6 +2,8 @@ #define PSXRECOMP_PSX_ICACHE_H #include "cpu_state.h" +#include "input_instruction_histogram.h" +#include "psx_cycles.h" #ifdef __cplusplus extern "C" { @@ -10,17 +12,40 @@ extern "C" { extern uint32_t g_psx_icache_tv[1024]; extern int g_psx_icache_active; extern int g_ls_replay_active; +/* Optional functional instruction-boundary consumer, independent of tracing. + * Called before a guest fetch (also for coalesced cached followers) after + * publishing completed prior instructions. Replay shadows never call it. */ +extern void (*g_psx_cpu_step_boundary_callback)(CPUState *,uint32_t,uint64_t); +#ifdef PSX_OVERLAY_DLL_BUILD +/* The DLL owns its pending cycle batch; its shim flushes that batch before + * forwarding to the host's observer. Host globals must not be copied. */ +void psx_cpu_step_boundary(CPUState *cpu,uint32_t address); +#else +static inline void psx_cpu_step_boundary(CPUState *cpu,uint32_t address) { + if(g_psx_cpu_step_boundary_callback && !g_ls_replay_active) { + psx_cyc_batch_flush(); + g_psx_cpu_step_boundary_callback(cpu,address,psx_get_cycle_count()); + } +} +#endif +void psx_cpu_step_boundary_fn(CPUState *cpu,uint32_t address); +int psx_cpu_step_boundary_enabled(int include_replay); void psx_icache_reset(void); void psx_icache_fetch(CPUState *cpu, uint32_t addr); void psx_icache_fetch_miss(CPUState *cpu, uint32_t addr); +/* Called only for guest stores with CP0.SR.IsC set. Updates timing tags for + * the BIOS tag-test flush; cache data and guest BIU-disabled fetches are not + * modeled by this helper. Address is the original virtual store address. */ +void psx_icache_isolated_store(uint32_t addr, uint32_t cache_control); /* Keep the interpreter's steady-state tag hit inside its translation unit. * Misses use the shared slow path, preserving exact cache evolution/timing. */ -static inline void psx_icache_fetch_interp(CPUState *cpu, uint32_t addr) { +static inline void psx_icache_fetch_interp_after_boundary(CPUState *cpu, uint32_t addr) { + if (g_input_instruction_histogram_active) input_instruction_histogram_sample(addr); #ifdef PSX_ENABLE_BLOCK_CYCLES if (g_ls_replay_active) return; if (g_psx_icache_active < 0) { - psx_icache_fetch(cpu, addr); + psx_icache_fetch_miss(cpu, addr); return; } if (!g_psx_icache_active) return; @@ -33,6 +58,11 @@ static inline void psx_icache_fetch_interp(CPUState *cpu, uint32_t addr) { #endif } +static inline void psx_icache_fetch_interp(CPUState *cpu, uint32_t addr) { + psx_cpu_step_boundary(cpu,addr); + psx_icache_fetch_interp_after_boundary(cpu,addr); +} + #ifdef __cplusplus } #endif diff --git a/runtime/include/psx_memory.h b/runtime/include/psx_memory.h index df150f66c..2880dcce6 100644 --- a/runtime/include/psx_memory.h +++ b/runtime/include/psx_memory.h @@ -24,6 +24,10 @@ extern "C" { #define PSX_MAIN_RAM_MASK (PSX_MAIN_RAM_BYTES - 1u) #define PSX_MAIN_RAM_WORD_MASK (PSX_MAIN_RAM_MASK & ~3u) +/* Read instruction bytes from the native RAM/BIOS backing only. No MMIO, + * data-read observers, device service or cycle charge. Returns 0 when invalid. */ +int memory_peek_instruction_word(uint32_t address, uint32_t *value); + /* Strip KUSEG/KSEG0/KSEG1 and canonicalize within the 8 MiB DRAM decode * window. Retail targets fold all four aliases; expanded targets preserve all * 23 address bits. */ diff --git a/runtime/include/source_cpu_block_bound.h b/runtime/include/source_cpu_block_bound.h new file mode 100644 index 000000000..9740c5482 --- /dev/null +++ b/runtime/include/source_cpu_block_bound.h @@ -0,0 +1,48 @@ +#ifndef PSX_SOURCE_CPU_BLOCK_BOUND_H +#define PSX_SOURCE_CPU_BLOCK_BOUND_H +/* A generated block's bcyc is its instruction count. It excludes cache + * refills, data waits and coprocessor interlocks, so it cannot alone prove + * that a pending IRQ lies beyond the block. This source-profile bound only + * selects the existing instruction interpreter; it never charges cycles. + * The caller restricts this to ordinary compiled RAM blocks without MMIO + * visibility side effects. Their bytes have already passed native-text checks. */ +static uint32_t source_cpu_block_bound(CPUState *cpu,uint32_t pc,uint32_t count,uint32_t deadline) { + uint64_t now=psx_get_cycle_count(); + uint64_t stalls=(cpu->gte_ts_done>now?cpu->gte_ts_done-now:0u)+ + (cpu->muldiv_ts_done>now?cpu->muldiv_ts_done-now:0u); + /* Per instruction: base1 + maximum cache refill7; data read maximum + * region36 (SPU word) + ReadFudge2 + completion2 + DMA steal200; + * newly issued GTE maximum43 and multiply/divide maximum37. Summing + * mutually exclusive costs is intentionally conservative. */ + uint64_t bound=(uint64_t)count*(8u+240u+43u+37u)+stalls; + if(bound0x80000u || phys>=0x200000u || (uint64_t)phys+4ull*count>0x200000u)return UINT32_MAX; + bound=(uint64_t)count*8u+stalls; + for(uint32_t i=0;i>26,fn=word&63u; + if((op>=0x20u && op<=0x26u) || op==0x32u)bound+=240u; + if(op==0x12u && (word&(1u<<25)))bound+=psx_gte_cmd_latency(word); + if(!op && fn>=0x18u && fn<=0x1bu)bound+=37u; + } + return bound>UINT32_MAX?UINT32_MAX:(uint32_t)bound; +} + +/* Existing generated MFC2/CFC2 bodies assign the GPR eagerly. Their block + * count includes branch delay slots, so inspect the validated live bytes and + * select the precise value owner before executing such a block. This also + * covers a move in JR's delay slot whose consumer is in the caller. Selection + * changes neither guest bytes nor cycles. CU2/profile admission is the caller's + * responsibility, just as for the timing bound above. */ +static int source_cpu_block_gte_value_delay(uint32_t pc,uint32_t count) { + uint32_t phys=pc&0x1fffffffu; + if(count>0x80000u || phys>=0x200000u || (uint64_t)phys+4ull*count>0x200000u)return 1; + for(uint32_t i=0;i>26,rs=(word>>21)&31u,rt=(word>>16)&31u; + if(op==0x12u && (rs==0u || rs==2u) && rt!=0u)return 1; + } + return 0; +} +#endif diff --git a/runtime/include/source_cpu_boundary_probe.h b/runtime/include/source_cpu_boundary_probe.h new file mode 100644 index 000000000..2a3b1353d --- /dev/null +++ b/runtime/include/source_cpu_boundary_probe.h @@ -0,0 +1,59 @@ +#ifndef PSX_SOURCE_CPU_BOUNDARY_PROBE_H +#define PSX_SOURCE_CPU_BOUNDARY_PROBE_H +#include +#include +/* One passive snapshot per original-model frontend return. Unlike input-route + * fields, these boundaries include the CPU instruction's event overshoot. */ +static void source_cpu_return_probe(CPUState *cpu,uint32_t pc,uint64_t cycle,unsigned frame) { + static int initialized,enabled; + static FILE *stream; + if(!initialized) { + initialized=1; + const char *setting=getenv("PSX_SOURCE_CPU_RETURN_PROBE"); + enabled=setting && setting[0]=='1' && !setting[1]; + } + if(!enabled)return; + if(!stream) { + char path[4096];const char *directory=getenv("PSX_INPUT_ROUTE_CAPTURE_DIR"); + if(!directory || snprintf(path,sizeof(path),"%s/cpu-return.tsv",directory)>=(int)sizeof(path))abort(); + stream=fopen(path,"wx");if(!stream)abort(); + fprintf(stream,"frame\tpc\tcycle\tsr\tcause\tepc"); + for(unsigned i=0;i<32;i++)fprintf(stream,"\tr%u",i); + fputc('\n',stream); + } + if(frame>20000u)abort(); + fprintf(stream,"%u\t%08X\t%llu\t%08X\t%08X\t%08X",frame,pc,(unsigned long long)cycle,cpu->cop0[12],cpu->cop0[13],cpu->cop0[14]); + for(unsigned i=0;i<32;i++)fprintf(stream,"\t%08X",cpu->gpr[i]); + fputc('\n',stream);fflush(stream); +} +/* Passive, bounded diagnostic at the existing functional pre-fetch boundary. + * It reads CPU/cache scalars only; never reads guest memory or services devices. */ +static void source_cpu_boundary_probe(CPUState *cpu,uint32_t pc,uint64_t cycle) { + static int initialized; + static unsigned long long low,high; + static unsigned rows; + static FILE *stream; + if(!initialized) { + initialized=1; + const char *range=getenv("PSX_SOURCE_CPU_BOUNDARY_WINDOW"); + if(range && (sscanf(range,"%llu,%llu",&low,&high)!=2 || high<=low || high-low>1000000u))abort(); + } + if(!high || cyclehigh) {if(stream){fclose(stream);stream=0;}high=0;return;} + if(!stream) { + char path[4096];const char *directory=getenv("PSX_INPUT_ROUTE_CAPTURE_DIR"); + if(!directory || snprintf(path,sizeof(path),"%s/cpu-boundary.tsv",directory)>=(int)sizeof(path))abort(); + stream=fopen(path,"wx");if(!stream)abort(); + fprintf(stream,"pc\tcycle\tcache_tag\tcache_active\tsr\tcause\tepc"); + for(unsigned i=0;i<32;i++)fprintf(stream,"\tr%u",i); + fprintf(stream,"\tread_fudge\tld_absorb\tld_which\tread_absorb_which\tdma_read_wait\tprecise\tdirty\ti_stat\ti_mask\tslice_pc\tslice_deadline\tslice_bound\tslice_cycle\tslice_takes\n"); + } + if(++rows>500000u)abort(); + fprintf(stream,"%08X\t%llu\t%08X\t%d\t%08X\t%08X\t%08X",pc,(unsigned long long)cycle,g_psx_icache_tv[(pc&0xffcu)>>2],g_psx_icache_active,cpu->cop0[12],cpu->cop0[13],cpu->cop0[14]); + for(unsigned i=0;i<32;i++)fprintf(stream,"\t%08X",cpu->gpr[i]); + extern int g_precise_mode,g_dirty_interp_active; + extern uint32_t i_stat,i_mask,g_slice_probe_pc,g_slice_probe_deadline,g_slice_probe_bound; + extern uint64_t g_slice_probe_cycle,g_slice_irq_taken; + fprintf(stream,"\t%u\t%u\t%u\t%u\t%u\t%d\t%d\t%08X\t%08X\t%08X\t%u\t%u\t%llu\t%llu\n",cpu->read_fudge,cpu->ld_absorb,cpu->ld_which_t,cpu->read_absorb_which,dma_cpu_read_penalty(),g_precise_mode,g_dirty_interp_active,i_stat,i_mask,g_slice_probe_pc,g_slice_probe_deadline,g_slice_probe_bound,(unsigned long long)g_slice_probe_cycle,(unsigned long long)g_slice_irq_taken); +} +#endif diff --git a/runtime/include/source_gpu_command_projection.h b/runtime/include/source_gpu_command_projection.h new file mode 100644 index 000000000..c7ea4ab21 --- /dev/null +++ b/runtime/include/source_gpu_command_projection.h @@ -0,0 +1,290 @@ +#ifndef PSX_SOURCE_GPU_COMMAND_PROJECTION_H +#define PSX_SOURCE_GPU_COMMAND_PROJECTION_H +#include +#include +#include "source_gpu_polygon_projection.h" + +/* Experimental, independently expressed Octoshock 2.2.2 command timing. + * This is NOT a renderer or a hardware timing claim. The synchronous renderer + * returns native texture-cache work, charged to this same queue before its + * next phase. Cache data and tags remain owned by the native renderer. Call update only at an + * explicitly qualified source GPU service event; reads do not advance it. + * Current scope: NOP/cache-clear, drawing environment, A0/C0 transfers,02 fills,80 copies, variable rectangles, + * observed untextured/textured polygons, including general opaque flat quads, + * with inclusive clipping. Vertex coordinates after offsets must stay in the + * signed 11-bit range; clipping stays inside VRAM. + * Interlaced row skipping requires explicitly supplied live readout parity. + * Other command families and state restoration remain unsupported. Ordinary GP1 reset + * preserves nonnegative credit and clamps debt, rather than granting new work. + * Original source: gpu.cpp ProcessFIFO/Update; gpu_polygon.cpp draw costs. + */ +enum { SOURCE_GPU_DISPATCH_NONE, SOURCE_GPU_DISPATCH_COMMAND, + SOURCE_GPU_DISPATCH_QUAD_FIRST, SOURCE_GPU_DISPATCH_QUAD_SECOND, + SOURCE_GPU_DISPATCH_UPLOAD_WORD }; +typedef struct SourceGPUCommandDispatch { + unsigned kind, count; + uint32_t words[12]; +} SourceGPUCommandDispatch; +typedef struct SourceGPUCommandProjection { + int32_t budget; + uint32_t queue[32], count, phase, command; + uint64_t last_update; + int clip_x0, clip_y0, clip_x1, clip_y1; + int offset_x,offset_y; + uint32_t draw_mode,texture_window,mask_bits,display_mode,dma_direction; + unsigned field_valid,skip_field; + unsigned first_triangles, second_triangles; + uint32_t polygon_words[12]; + uint32_t transfer_words; + SourceGPUCommandDispatch dispatch; + int error; +} SourceGPUCommandProjection; + +enum { SOURCE_GPU_COMMAND_UNSUPPORTED=1, SOURCE_GPU_COMMAND_OVERFLOW=2, + SOURCE_GPU_COMMAND_REVERSE_TIME=3 }; + +static inline void source_gpu_command_cold(SourceGPUCommandProjection *s) { + memset(s,0,sizeof(*s)); +} + +static inline unsigned source_gpu_polygon_stride(unsigned opcode) { + return 1u+!!(opcode&0x10)+!!(opcode&4); +} +static inline int source_gpu_polygon_supported(unsigned opcode) { + switch(opcode) { + case 0x20:case 0x21:case 0x28:case 0x29:case 0x30:case 0x38:case 0x3a: + case 0x24:case 0x2c:case 0x2d:case 0x2e:case 0x2f: + case 0x34:case 0x3c:case 0x3e:return 1; + default:return 0; + } +} +static inline unsigned source_gpu_polygon_setup(unsigned opcode) { + return opcode&4 ? (opcode&0x10 ? 450:180) : (opcode&0x10 ? 288:0); +} +static inline int source_gpu_block_supported(unsigned command) { + return command==2 || command==0x80 || command==0x60 || command==0x62 || command==0x64 || command==0x65; +} +static inline unsigned source_gpu_command_length(uint32_t word) { + unsigned command=word>>24; + if(source_gpu_block_supported(command))return command==0x80 || command==0x64 || command==0x65?4:3; + if(source_gpu_polygon_supported(command))return 1+3*source_gpu_polygon_stride(command)-!!(command&0x10); + return command==0xa0 || command==0xc0 ? 3u : 1u; +} +static inline unsigned source_gpu_command_feedback_length(uint32_t word) { + unsigned command=word>>24; + if(command==2 || (command>=0x60 && command<=0x65))return 3; + return command==0x80 || command==1 || command==0xa0 || command==0xc0 || command==0xe1 || command==0xe2 || command==0xe6 ? 2u : 1u; +} + +static inline int source_gpu_command_ready(const SourceGPUCommandProjection *s) { + if(s->error) return -1; + if(s->phase==2) return 0; + if(s->count && (s->phase==4 || s->phase==8))return 0; + /* Feedback length differs from packet length. Cache clear and ordinary + * environment commands admit one queued word while DMA-ready stays set. */ + return s->count==0 || s->countqueue[0]); +} + +static inline uint32_t source_gpu_command_pop(SourceGPUCommandProjection *s) { + uint32_t word=s->queue[0]; + --s->count; + memmove(s->queue,s->queue+1,s->count*sizeof(s->queue[0])); + return word; +} + +static inline int source_gpu_command_coord(uint32_t word,unsigned shift) { + unsigned v=(word>>shift)&2047u; + return (int)(v^1024u)-1024; +} + +static inline int source_gpu_command_polygon_cost(const SourceGPUCommandProjection *s,const uint32_t *words,int second) { + unsigned opcode=words[0]>>24,stride=source_gpu_polygon_stride(opcode); + int x[3],y[3]; + if(s->clip_y0>511 || s->clip_y1>511 || + ((s->display_mode&0x24)==0x24 && !(s->draw_mode&0x400) && !s->field_valid))return -1; + for(unsigned i=0;i<3;i++) { + unsigned v=i+(second?1:0); + x[i]=source_gpu_command_coord(words[1+stride*v],0)+s->offset_x; + y[i]=source_gpu_command_coord(words[1+stride*v],16)+s->offset_y; + } + return source_poly_cost(x,y,s->clip_x0,s->clip_y0,s->clip_x1,s->clip_y1, + !!(opcode&0x14),!!((opcode&2)||(s->mask_bits&2)), + (s->display_mode&0x24)==0x24 && !(s->draw_mode&0x400),s->skip_field); +} + +/* Sprite coordinates wrap after adding the drawing offset. Polygon vertex + * admission and fill/copy addressing have different rules. */ +static inline int source_gpu_sprite_origin(uint32_t word,unsigned shift,int offset) { + return source_gpu_command_coord((uint32_t)(source_gpu_command_coord(word,shift)+offset),0); +} +static inline int source_gpu_command_block_cost(const SourceGPUCommandProjection *s,const uint32_t *words) { + unsigned opcode=words[0]>>24; + if(opcode==0x80) { + unsigned w=words[3]&1023u,h=(words[3]>>16)&511u; + return 2*(w?w:1024)*(h?h:512); + } + int interlace=(s->display_mode&0x24)==0x24 && !(s->draw_mode&0x400); + if(interlace && !s->field_valid)return -1; + int cost=opcode==2?46:16; + if(opcode==2) { + unsigned w=((words[2]&1023u)+15u)&~15u,h=(words[2]>>16)&511u; + for(unsigned y=0;y>16))&511u; + if(!interlace || (row&1u)!=s->skip_field)cost+=(w>>3)+9; + } + return cost; + } + int x=source_gpu_sprite_origin(words[1],0,s->offset_x),y=source_gpu_sprite_origin(words[1],16,s->offset_y); + unsigned size=words[(opcode&4)?3:2]; + int right=x+(int)(size&1023u),bottom=y+(int)((size>>16)&511u); + if(xclip_x0)x=s->clip_x0;if(yclip_y0)y=s->clip_y0; + if(right>s->clip_x1+1)right=s->clip_x1+1;if(bottom>s->clip_y1+1)bottom=s->clip_y1+1; + int width=right-x; + if(width>0)for(int row=y;rowskip_field)continue; + cost+=width; + if((opcode&2) || (s->mask_bits&2))cost+=(((right+1)&~1)-(x&~1))/2; + } + return cost; +} + +static inline int source_gpu_command_process(SourceGPUCommandProjection *s) { + s->dispatch.kind=SOURCE_GPU_DISPATCH_NONE; + if(s->error) return 0; + if(!s->count) return 1; + if(s->phase==8)return 1; /* Queued work waits for ordinary GPUREAD. */ + if(s->phase==4) { + s->dispatch.kind=SOURCE_GPU_DISPATCH_UPLOAD_WORD;s->dispatch.count=1; + s->dispatch.words[0]=source_gpu_command_pop(s); + if(--s->transfer_words==0)s->phase=0; + return 1; /* Upload data bypasses draw debt and ordinary dispatch cost. */ + } + if(s->phase==2) { + if(s->budget<0) return 1; + unsigned stride=source_gpu_polygon_stride(s->command); + unsigned first=source_gpu_command_length(s->polygon_words[0]),total=first+stride; + if(s->countpolygon_words[first+i]=s->queue[i]; + int cost=source_gpu_command_polygon_cost(s,s->polygon_words,1); + if(cost<0){s->error=SOURCE_GPU_COMMAND_UNSUPPORTED;return 0;} + s->dispatch.kind=SOURCE_GPU_DISPATCH_QUAD_SECOND;s->dispatch.count=total; + memcpy(s->dispatch.words,s->polygon_words,total*sizeof(uint32_t)); + for(unsigned i=0;ibudget-=46+source_gpu_polygon_setup(s->command)+cost; + s->phase=0;++s->second_triangles;return 1; + } + + unsigned command=s->queue[0]>>24; + if(source_gpu_block_supported(command)) { + unsigned n=source_gpu_command_length(s->queue[0]); + if(s->budget<0 || s->countqueue); + if(cost<0){s->error=SOURCE_GPU_COMMAND_UNSUPPORTED;return 0;} + s->dispatch.kind=SOURCE_GPU_DISPATCH_COMMAND;s->dispatch.count=n; + for(unsigned i=0;idispatch.words[i]=source_gpu_command_pop(s); + s->budget-=2+cost;return 1; + } + if(source_gpu_polygon_supported(command)) { + unsigned n=source_gpu_command_length(s->queue[0]); + if(s->budget<0 || s->countqueue,0); + if(cost<0){s->error=SOURCE_GPU_COMMAND_UNSUPPORTED;return 0;} + s->dispatch.kind=(command&8)?SOURCE_GPU_DISPATCH_QUAD_FIRST:SOURCE_GPU_DISPATCH_COMMAND; + s->dispatch.count=n; + for(unsigned i=0;ipolygon_words[i]=s->dispatch.words[i]=source_gpu_command_pop(s); + s->budget-=84+source_gpu_polygon_setup(command)+cost; + if(command&4)s->draw_mode=(s->draw_mode&~0x1ffu)|((s->polygon_words[4+!!(command&0x10)]>>16)&0x1ffu); + if(command&8){s->phase=2;s->command=command;++s->first_triangles;} + return 1; + } + if(command!=0 && command!=1 && !(command>=0xe1 && command<=0xe6) && command!=0xa0 && command!=0xc0) { + s->error=SOURCE_GPU_COMMAND_UNSUPPORTED; return 0; + } + unsigned ordinary_environment=command==0xe1 || command==0xe2 || command==0xe6; + if(ordinary_environment && s->budget<0)return 1; + if(command==0xa0 || command==0xc0) { + if(s->budget<0 || s->count<3)return 1; + unsigned width=s->queue[2]&1023u; + unsigned height=(s->queue[2]>>16)&(command==0xa0?511u:1023u); + if(!width)width=1024; + if(command==0xa0 && !height)height=512; + if(command==0xc0 && height>512)height&=511u; + s->dispatch.kind=SOURCE_GPU_DISPATCH_COMMAND;s->dispatch.count=3; + for(unsigned i=0;i<3;i++)s->dispatch.words[i]=source_gpu_command_pop(s); + s->budget-=2; + s->transfer_words=(width*height+1u)/2u; + if(s->transfer_words)s->phase=command==0xa0?4u:8u; + } else if(command==1) { + if(s->budget<0)return 1; + s->dispatch.kind=SOURCE_GPU_DISPATCH_COMMAND;s->dispatch.count=1; + s->dispatch.words[0]=source_gpu_command_pop(s);s->budget-=2; + /* Cache invalidation is a synchronous native renderer side effect. */ + } else { + uint32_t word=source_gpu_command_pop(s); + s->dispatch.kind=SOURCE_GPU_DISPATCH_COMMAND;s->dispatch.count=1; + s->dispatch.words[0]=word; + if(ordinary_environment)s->budget-=2; + /* Original NULLCMD and clip commands are ss_cmd: neither block on + * negative work credit nor charge the ordinary dispatch cost. + * Draw mode, texture window and mask commands use ordinary dispatch. */ + if(command==0xe3) { s->clip_x0=word&1023; s->clip_y0=(word>>10)&1023; } + if(command==0xe4) { s->clip_x1=word&1023; s->clip_y1=(word>>10)&1023; } + if(command==0xe1) s->draw_mode=word&0x3fffu; + if(command==0xe2) s->texture_window=word&0xfffffu; + if(command==0xe5) {s->offset_x=source_gpu_command_coord(word,0);s->offset_y=source_gpu_command_coord(word,11);} + if(command==0xe6) s->mask_bits=word&3u; + } + return 1; +} + +static inline int source_gpu_command_gp1(SourceGPUCommandProjection *s,uint32_t word) { + s->dispatch.kind=SOURCE_GPU_DISPATCH_NONE; + if(s->error)return 0; + unsigned command=word>>24; + if(command==0 || command==1) { + if(s->budget<0)s->budget=0; + s->count=0;s->phase=0;s->transfer_words=0; + if(!command) { + s->clip_x0=s->clip_y0=s->clip_x1=s->clip_y1=0; + s->offset_x=s->offset_y=0;s->draw_mode=s->texture_window=s->mask_bits=0; + s->display_mode=s->dma_direction=0; + } + } else if(command==4)s->dma_direction=word&3u; + else if(command==8) { + if(word&8u){s->error=SOURCE_GPU_COMMAND_UNSUPPORTED;return 0;} + s->display_mode=word&255u; + } else if(command!=2 && command!=3 && command!=5 && command!=6 && command!=7 && command!=0x10) { + s->error=SOURCE_GPU_COMMAND_UNSUPPORTED;return 0; + } + return 1; +} + +static inline void source_gpu_command_read(SourceGPUCommandProjection *s) { + s->dispatch.kind=SOURCE_GPU_DISPATCH_NONE; + if(s->phase==8 && --s->transfer_words==0)s->phase=0; +} + +static inline int source_gpu_command_write(SourceGPUCommandProjection *s,uint32_t word) { + s->dispatch.kind=SOURCE_GPU_DISPATCH_NONE; + if(s->error) return 0; + /* Idle FIFO extension follows the front command's feedback length; + * active split quads have no extension. Overflow is a scope stop. */ + if(s->count>=16 && (s->phase || s->count-16>=source_gpu_command_feedback_length(s->queue[0]))) { + s->error=SOURCE_GPU_COMMAND_OVERFLOW; return 0; + } + s->queue[s->count++]=word; + return source_gpu_command_process(s); +} + +static inline int source_gpu_command_update(SourceGPUCommandProjection *s,uint64_t cycle) { + s->dispatch.kind=SOURCE_GPU_DISPATCH_NONE; + if(s->error) return 0; + if(cyclelast_update) { s->error=SOURCE_GPU_COMMAND_REVERSE_TIME; return 0; } + uint64_t elapsed=cycle-s->last_update; + if(!elapsed) return 1; + int64_t missing=256-(int64_t)s->budget; + s->budget=elapsed>=(uint64_t)((missing+1)/2) ? 256 : s->budget+(int32_t)(2*elapsed); + s->last_update=cycle; + return source_gpu_command_process(s); +} +#endif diff --git a/runtime/include/source_gpu_polygon_projection.h b/runtime/include/source_gpu_polygon_projection.h new file mode 100644 index 000000000..5f0b1da07 --- /dev/null +++ b/runtime/include/source_gpu_polygon_projection.h @@ -0,0 +1,76 @@ +#ifndef PSX_SOURCE_GPU_POLYGON_PROJECTION_H +#define PSX_SOURCE_GPU_POLYGON_PROJECTION_H +#include + +/* Independent work estimator for untextured source-profile triangles. + * Pixel values are intentionally absent. Directed edge traversal matters for + * entering-clip work and fixed-point rounding, even when coverage looks equal. */ +static inline int64_t source_poly_edge(int x) { + return (int64_t)x*INT64_C(4294967296)+INT64_C(4294965248); +} +static inline int64_t source_poly_slope(int dx,int dy) { + return ((int64_t)dx*INT64_C(4294967296)+(dx<0?-(dy-1):dx>0?dy-1:0))/dy; +} +typedef void (*SourcePolySpan)(void *,int,int,int); +static inline int source_poly_walk(const int *input_x,const int *input_y, + int clip_left,int clip_top,int clip_right,int clip_bottom, + int doubled,int masked_or_blended,int interlace,unsigned skip_field, + SourcePolySpan visit,void *context) { + struct {int x,y,original;} v[3]; + int core= input_x[1]<=input_x[0] ? (input_x[2]<=input_x[1]?2:1) : + input_x[2] 1023 || v[i].y < -1024 || v[i].y > 1023)return -1; + } + const unsigned pairs[3][2]={{1,2},{0,1},{1,2}}; + for(unsigned i=0;i<3;i++) { + unsigned a=pairs[i][0],b=pairs[i][1]; + if(v[a].y>v[b].y) { + int x=v[a].x,y=v[a].y,o=v[a].original; + v[a]=v[b];v[b].x=x;v[b].y=y;v[b].original=o; + } + } + for(int i=0;i<3;i++)if(v[i].original==core){core=i;break;} + if(v[2].y==v[0].y || v[2].y-v[0].y>=512)return 0; + for(int i=0;i<3;i++)for(int j=i+1;j<3;j++) + if(v[i].x-v[j].x>=1024 || v[j].x-v[i].x>=1024)return 0; + int area=(v[1].x-v[0].x)*(v[2].y-v[0].y)-(v[2].x-v[0].x)*(v[1].y-v[0].y); + if(!area)return 0; + int64_t long_step=source_poly_slope(v[2].x-v[0].x,v[2].y-v[0].y); + int right= v[1].y==v[0].y ? v[1].x>v[0].x : + source_poly_slope(v[1].x-v[0].x,v[1].y-v[0].y)>long_step; + int cost=0; + for(int half=0;half<2;half++) { + int lo=half,hi=half+1; + if(v[hi].y==v[lo].y)continue; + int down=half==0?core==0:core!=2; + int anchor=down?lo:hi; + int64_t short_step=source_poly_slope(v[hi].x-v[lo].x,v[hi].y-v[lo].y); + int begin=down?v[lo].y:v[hi].y-1,end=down?v[hi].y:v[lo].y-1,dy=down?1:-1; + for(int y=begin;y!=end;y+=dy) { + if(down?y>clip_bottom:yclip_bottom){cost+=2;continue;} + if(interlace && ((unsigned)y&1u)==skip_field)continue; + int lx=(int)((source_poly_edge(v[0].x)+long_step*(y-v[0].y))>>32); + int sx=(int)((source_poly_edge(v[anchor].x)+short_step*(y-v[anchor].y))>>32); + int left=right?lx:sx,bound=right?sx:lx; + int width=bound-left; + /* Source signs the left endpoint before applying its clip. */ + left=((left&2047)^1024)-1024; + if(leftclip_right+1)width=clip_right+1-left; + if(width>0) { + cost+=doubled?2*width:masked_or_blended?width+(width+1)/2:width; + if(visit)visit(context,y,left,width); + } + } + } + return cost; +} +static inline int source_poly_cost(const int *x,const int *y,int l,int t,int r,int b, + int doubled,int masked,int interlace,unsigned skip) { + return source_poly_walk(x,y,l,t,r,b,doubled,masked,interlace,skip,0,0); +} +#endif diff --git a/runtime/include/source_gpu_runtime.h b/runtime/include/source_gpu_runtime.h new file mode 100644 index 000000000..90027119c --- /dev/null +++ b/runtime/include/source_gpu_runtime.h @@ -0,0 +1,28 @@ +#ifndef PSX_SOURCE_GPU_RUNTIME_H +#define PSX_SOURCE_GPU_RUNTIME_H +#include "source_gpu_service_clock.h" +#include "source_gpu_command_projection.h" +#ifdef __cplusplus +extern "C" { +#endif +/* Experimental source-profile service bridge. Initialization is cold-only; + * default code remains inactive. Reads copy scalar state without servicing it. + * The GPU installs a sink so phase dispatch owns renderer/environment effects. */ +typedef int (*SourceGPUDispatchSink)(const SourceGPUCommandDispatch *); +void source_gpu_runtime_set_dispatch_sink(SourceGPUDispatchSink); +void source_gpu_runtime_init(void); +int source_gpu_runtime_active(void); +int source_gpu_runtime_ready(void); +uint32_t source_gpu_runtime_status_bits(void); +void source_gpu_runtime_advance(void); +uint32_t source_gpu_runtime_cycles_to_event(void); +void source_gpu_runtime_dma_write(void); +void source_gpu_runtime_gp0(uint32_t word); +void source_gpu_runtime_read(void); +void source_gpu_runtime_gp1(uint32_t word); +void source_gpu_runtime_copy(SourceGPUServiceClock *,SourceGPUCommandProjection *); +void source_gpu_runtime_copy_return(SourceGPUServiceClock *,SourceGPUCommandProjection *); +#ifdef __cplusplus +} +#endif +#endif diff --git a/runtime/include/source_gpu_service_clock.h b/runtime/include/source_gpu_service_clock.h new file mode 100644 index 000000000..7217301a3 --- /dev/null +++ b/runtime/include/source_gpu_service_clock.h @@ -0,0 +1,95 @@ +#ifndef PSX_SOURCE_GPU_SERVICE_CLOCK_H +#define PSX_SOURCE_GPU_SERVICE_CLOCK_H +#include "input_route_raster_clock.h" + +/* Standalone source event clock, not yet connected to runtime scheduling. + * Original DMA_Update services the GPU on its fixed 128-cycle cadence and + * before DMA register writes. GPU's own event reschedules from the earlier + * of 128 clocks and the next raster phase boundary. A DMA caller does not + * replace that own-event deadline. Only NTSC cold state is represented here. + * Placement of frame-end ForceEventUpdates, mode writes and reset/restore + * remain separate integration gates. Callback duplicates are intentional: + * the command projection must make zero-elapsed Update a no-op. + */ +typedef struct SourceGPUServiceClock { + InputRouteRasterClock raster; + uint64_t cycle, gpu_deadline, dma_deadline; + uint64_t frame_request_cycle; + unsigned zero_reached,frame_pending,frame_returns; +} SourceGPUServiceClock; +typedef void (*SourceGPUServiceEvent)(void *,uint64_t,unsigned); +enum { SOURCE_GPU_EVENT_OWN=1, SOURCE_GPU_EVENT_DMA=2, SOURCE_GPU_EVENT_WRITE=3, + SOURCE_GPU_EVENT_FRAME_END=4 }; +static inline void source_gpu_service_cold(SourceGPUServiceClock *s) { + memset(s,0,sizeof(*s)); input_route_raster_reset(&s->raster); + s->gpu_deadline=s->dma_deadline=128; +} +static inline uint64_t source_gpu_service_next(const SourceGPUServiceClock *s) { + return s->gpu_deadlinedma_deadline ? s->gpu_deadline : s->dma_deadline; +} +static inline uint32_t source_gpu_service_until_phase(const SourceGPUServiceClock *s) { + uint64_t clocks=(uint64_t)s->raster.remaining*65536u-s->raster.fraction; + return (uint32_t)((clocks+103895u)/103896u); +} +/* Original frontend requests its return only after passing scanline zero. + * Visible/final-line fallbacks and the >=232 VBlank rule are distinct from + * the hardware IRQ edge. The CPU boundary consumes the request later. */ +static inline void source_gpu_service_raster(SourceGPUServiceClock *s,uint32_t elapsed) { + uint32_t old_line=s->raster.scanline,old_blank=s->raster.blank; + input_route_raster_advance(&s->raster,elapsed); + if(s->raster.scanline==old_line) return; + uint32_t line=s->raster.scanline; + if(!line)s->zero_reached=1; + if(s->zero_reached && (line==256 || line==s->raster.lines-1 || + (!old_blank && s->raster.blank && line>=232))) { + if(!s->frame_pending)s->frame_request_cycle=s->raster.cycle; + s->frame_pending=1; + } +} +static inline int source_gpu_service_to(SourceGPUServiceClock *s,uint64_t now, + SourceGPUServiceEvent event,void *context) { + if(nowcycle) return 0; + uint64_t next; + while((next=source_gpu_service_next(s))<=now) { + uint64_t elapsed=next-s->cycle; + if(elapsed>UINT32_MAX) return 0; + source_gpu_service_raster(s,(uint32_t)elapsed); s->cycle=next; + if(next==s->gpu_deadline) { + event(context,next,SOURCE_GPU_EVENT_OWN); + uint32_t distance=source_gpu_service_until_phase(s); + s->gpu_deadline=next+(distance<128 ? distance : 128); + } + if(next==s->dma_deadline) { + event(context,next,SOURCE_GPU_EVENT_DMA); s->dma_deadline+=128; + } + } + uint64_t elapsed=now-s->cycle; + if(elapsed>UINT32_MAX) return 0; + source_gpu_service_raster(s,(uint32_t)elapsed); s->cycle=now; + return 1; +} +static inline int source_gpu_service_dma_write(SourceGPUServiceClock *s,uint64_t now, + SourceGPUServiceEvent event,void *context) { + if(!source_gpu_service_to(s,now,event,context)) return 0; + event(context,now,SOURCE_GPU_EVENT_WRITE); return 1; +} +/* Original frame-end ForceEventUpdates runs GPU then DMA at the returned CPU + * timestamp, and replaces the GPU event deadline even for a zero-time update. + * This absolute-clock representation needs no timestamp rebase. The caller + * must supply an independently qualified source-equivalent return boundary. */ +static inline int source_gpu_service_frame_end(SourceGPUServiceClock *s,uint64_t now, + SourceGPUServiceEvent event,void *context) { + if(!source_gpu_service_to(s,now,event,context)) return 0; + event(context,now,SOURCE_GPU_EVENT_FRAME_END); + uint32_t distance=source_gpu_service_until_phase(s); + s->gpu_deadline=now+(distance<128 ? distance : 128); + event(context,now,SOURCE_GPU_EVENT_DMA); + s->frame_pending=0;s->zero_reached=0;s->frame_returns++; + return 1; +} +static inline int source_gpu_service_cpu_boundary(SourceGPUServiceClock *s,uint64_t now, + SourceGPUServiceEvent event,void *context) { + if(!source_gpu_service_to(s,now,event,context)) return 0; + return !s->frame_pending || source_gpu_service_frame_end(s,now,event,context); +} +#endif diff --git a/runtime/include/source_gpu_texture.h b/runtime/include/source_gpu_texture.h new file mode 100644 index 000000000..9191ef3d9 --- /dev/null +++ b/runtime/include/source_gpu_texture.h @@ -0,0 +1,17 @@ +#ifndef PSX_SOURCE_GPU_TEXTURE_H +#define PSX_SOURCE_GPU_TEXTURE_H +#include +/* Parameters for the opt-in, native software source-comparison renderer. + * These are command attributes, never imported source-emulator state. */ +typedef struct SourceGPUTexture { + uint32_t uv[3],window; + uint16_t page,clut; + int raw,load_clut; +} SourceGPUTexture; +typedef struct SourceGPUBlock { + const uint32_t *words; + uint32_t draw_mode,texture_window; + int x,y,interlace,clip_left,clip_top,clip_right,clip_bottom; + unsigned skip_field; +} SourceGPUBlock; +#endif diff --git a/runtime/include/source_ram_page_probe.h b/runtime/include/source_ram_page_probe.h new file mode 100644 index 000000000..7ac6569da --- /dev/null +++ b/runtime/include/source_ram_page_probe.h @@ -0,0 +1,66 @@ +#ifndef PSX_SOURCE_RAM_PAGE_PROBE_H +#define PSX_SOURCE_RAM_PAGE_PROBE_H +#include +#include +#include +#include + +/* Passive diagnostic index. A hash mismatch locates bytes to inspect; hashes + * are not a substitute for comparing the selected raw RAM snapshots. */ +static uint64_t source_ram_page_hash(const uint8_t *bytes, unsigned count) { + uint64_t hash = UINT64_C(14695981039346656037); + for (unsigned i = 0; i < count; ++i) + hash = (hash ^ bytes[i]) * UINT64_C(1099511628211); + return hash; +} + +static void source_ram_page_probe(unsigned frame, uint64_t cycle) { + static int initialized, enabled; + static FILE *stream; + static unsigned snapshots[32], snapshot_count; + if (!initialized) { + initialized = 1; + const char *setting = getenv("PSX_SOURCE_RAM_PAGE_PROBE"); + enabled = setting && strcmp(setting, "1") == 0; + const char *cursor = getenv("PSX_SOURCE_RAM_SNAPSHOT_FRAMES"); + while (enabled && cursor && *cursor) { + if (*cursor < '0' || *cursor > '9') abort(); + char *end; + unsigned long value = strtoul(cursor, &end, 10); + if (end == cursor || value < 1 || value > 20000 || snapshot_count == 32 || + (*end && *end != ',')) abort(); + for (unsigned i = 0; i < snapshot_count; ++i) + if (snapshots[i] == value) abort(); + snapshots[snapshot_count++] = (unsigned)value; + cursor = *end ? end + 1 : end; + if (*end && !*cursor) abort(); + } + } + if (!enabled) return; + if (frame < 1 || frame > 20000) abort(); + extern uint8_t *memory_get_ram_ptr(void); + const uint8_t *ram = memory_get_ram_ptr(); + char path[4096]; + const char *directory = getenv("PSX_INPUT_ROUTE_CAPTURE_DIR"); + if (!ram || !directory) abort(); + if (!stream) { + if (snprintf(path, sizeof(path), "%s/ram-pages.tsv", directory) >= (int)sizeof(path)) abort(); + stream = fopen(path, "wx"); + if (!stream) abort(); + fputs("# psx-ram-pages-v1 page_bytes=4096 ram_bytes=2097152 hash=fnv1a64\nframe\tcycle", stream); + for (unsigned page = 0; page < 512; ++page) fprintf(stream, "\t%06X", page * 4096); + fputc('\n', stream); + } + fprintf(stream, "%u\t%llu", frame, (unsigned long long)cycle); + for (unsigned page = 0; page < 512; ++page) + fprintf(stream, "\t%016llX", (unsigned long long)source_ram_page_hash(ram + page * 4096, 4096)); + fputc('\n', stream); + if (fflush(stream) != 0 || ferror(stream)) abort(); + for (unsigned i = 0; i < snapshot_count; ++i) if (snapshots[i] == frame) { + if (snprintf(path, sizeof(path), "%s/ram-frame-%06u.bin", directory, frame) >= (int)sizeof(path)) abort(); + FILE *snapshot = fopen(path, "wbx"); + if (!snapshot) abort(); + if (fwrite(ram, 1, 2097152, snapshot) != 2097152 || fclose(snapshot) != 0) abort(); + } +} +#endif diff --git a/runtime/include/timer1_source_clock.h b/runtime/include/timer1_source_clock.h new file mode 100644 index 000000000..14dabf1b9 --- /dev/null +++ b/runtime/include/timer1_source_clock.h @@ -0,0 +1,73 @@ +#ifndef PSX_TIMER1_SOURCE_CLOCK_H +#define PSX_TIMER1_SOURCE_CLOCK_H +#include +#include +/* Optional original-Octoshock timer 1 state machine. Independently expressed + * from documented transitions; only IRQ-disabled modes are admitted. The + * caller supplies ordered CPU, H-retrace and VBlank events. No guest memory. */ +typedef struct PsxTimer1Source { + uint32_t mode, counter, target; + int counting, blank; +} PsxTimer1Source; +static inline void timer1_source_reset(PsxTimer1Source *s) { + memset(s,0,sizeof(*s)); s->blank=1; +} +static inline void timer1_source_match(PsxTimer1Source *s) { + s->mode|=0x800; + if(s->mode&8) s->counter%=s->target ? s->target : 1; +} +static inline void timer1_source_count(PsxTimer1Source *s,uint32_t n) { + if((s->mode&8) && !s->target && !s->counter) { + timer1_source_match(s); return; + } + if(s->counting<=0 || !n) return; + uint32_t before=s->counter; + s->counter+=n; + if((beforetarget && s->counter>=s->target) || s->counter>=s->target+65536u) + timer1_source_match(s); + if(s->counter>=65536u) {s->mode|=0x1000; s->counter&=65535u;} +} +static inline void timer1_source_cpu(PsxTimer1Source *s,uint32_t n) { + if(!(s->mode&0x100)) timer1_source_count(s,n); +} +static inline void timer1_source_hblank(PsxTimer1Source *s,uint32_t n) { + if(s->mode&0x100) timer1_source_count(s,n); +} +static inline void timer1_source_blank(PsxTimer1Source *s,int blank) { + unsigned sync=s->mode&7; + if(sync==1) s->counting=!blank; + if(sync==5) s->counting=blank; + if((sync==3 || sync==5) && s->blank && !blank) { + s->counter=0; + if(!s->target) timer1_source_match(s); + } + if(sync==7) { + if(s->counting<0 && !s->blank && blank) s->counting=0; + else if(!s->counting && s->blank && !blank) s->counting=1; + } + s->blank=blank; +} +static inline int timer1_source_write(PsxTimer1Source *s,unsigned reg,uint16_t v) { + if(reg==4 && (v&0x30)) return 0; /* no IRQ phase claim */ + if(reg==0) s->counter=v; + if(reg==4) { + s->mode=(s->mode&0x1c00)|(v&0x3ff);s->counter=0;s->counting=1; + if((v&7)==1) s->counting=!s->blank; + if((v&7)==5) s->counting=s->blank; + if((v&7)==7) s->counting=-1; + } + if(reg==8) s->target=v; + if(s->counter==s->target) timer1_source_match(s); + return 1; +} +static inline uint32_t timer1_source_read(PsxTimer1Source *s,unsigned reg) { + if(reg==0) return s->counter; + if(reg==8) return s->target; + if(reg==4) { + uint32_t value=s->mode;s->mode&=~0x1000u; + if(s->counter!=s->target) s->mode&=~0x800u; + return value; + } + return 0; +} +#endif diff --git a/runtime/include/timer2_source_clock.h b/runtime/include/timer2_source_clock.h new file mode 100644 index 000000000..f6b4b23e1 --- /dev/null +++ b/runtime/include/timer2_source_clock.h @@ -0,0 +1,78 @@ +#ifndef PSX_TIMER2_SOURCE_CLOCK_H +#define PSX_TIMER2_SOURCE_CLOCK_H +#include +#include +/* Explicit original-Octoshock compatibility state. Independent expression of + * the retained source transitions, qualified against an external scalar oracle. + * This is not a claim that every original-core quirk describes PS1 hardware. + * No guest memory, title addresses, input words or selected timing constants. */ +typedef struct PsxTimer2Source { + uint32_t counter,mode,target,divider; + int irq_done,counting; +} PsxTimer2Source; +static inline void timer2_source_reset(PsxTimer2Source *s){memset(s,0,sizeof(*s));} +static inline int timer2_source_target(PsxTimer2Source *s,unsigned *pulses){ + s->mode|=0x800; + if(s->mode&8) s->counter%=s->target?s->target:1; + if(!(s->mode&0x10) || s->irq_done) return 0; + s->irq_done=1;(*pulses)++; + return !s->counter || s->counter==s->target; +} +static inline unsigned timer2_source_cpu(PsxTimer2Source *s,uint32_t cycles){ + unsigned pulses=0; + /* Original TIMER_Update skips this encoding even for timer 2. */ + if(s->mode&0x100) return 0; + if(s->counting<=0) cycles=0; + uint32_t divided=(s->divider+cycles)>>3; + s->divider=(s->divider+cycles)&7; + if(s->mode&0x200) cycles=divided; + if(s->mode&1) cycles=0; + if((s->mode&8) && !s->target && !s->counter){ + (void)timer2_source_target(s,&pulses);return pulses; + } + if(!cycles) return 0; + uint32_t before=s->counter; + s->counter+=cycles; + if(s->mode&0x40) s->irq_done=0; + int exact=0; + if((beforetarget && s->counter>=s->target) || s->counter>=s->target+65536u) + exact=timer2_source_target(s,&pulses); + if(s->counter>=65536u){ + s->mode|=0x1000;s->counter&=65535u; + if((s->mode&0x20) && !s->irq_done){ + exact|=!s->counter;s->irq_done=1;pulses++; + } + } + if((s->mode&0x40) && !exact) s->irq_done=0; + return pulses; +} +static inline unsigned timer2_source_write(PsxTimer2Source *s,unsigned reg,uint16_t v){ + unsigned pulses=0; + if(reg==0){s->counter=v;s->irq_done=0;} + if(reg==4){ + s->mode=(s->mode&0x1c00)|(v&0x3ff);s->counter=0;s->irq_done=0; + s->counting=1; /* Divider deliberately survives mode writes. */ + } + if(reg==8) s->target=v; + if(s->counter==s->target)(void)timer2_source_target(s,&pulses); + return pulses; +} +static inline uint32_t timer2_source_read(PsxTimer2Source *s,unsigned reg){ + if(reg==0)return s->counter; + if(reg==8)return s->target; + if(reg!=4)return 0; + uint32_t value=s->mode;s->mode&=~0x1000u; + if(s->counter!=s->target)s->mode&=~0x800u; + return value; +} +static inline uint32_t timer2_source_next(const PsxTimer2Source *s){ + /* Source periodic update bound, independent of CPU interrupt masking. */ + if(!(s->mode&0x30))return 1024; + if((s->mode&8) && !s->counter && !s->target && !s->irq_done)return 1; + if((s->mode&1) || s->counting<=0)return 1024; + uint32_t target=((s->mode&0x18) && s->countertarget)?s->target:65536u; + uint32_t clocks=target-s->counter; + if(s->mode&0x200)clocks=clocks*8-s->divider; + return clocks<1024?clocks:1024; +} +#endif diff --git a/runtime/include/timers.h b/runtime/include/timers.h index 3f67ff7c4..281536fe9 100644 --- a/runtime/include/timers.h +++ b/runtime/include/timers.h @@ -10,6 +10,12 @@ extern "C" { #define TIMER_BASE 0x1F801100 void timers_init(void); +int timers_source_raster_enabled(void); +/* HBlank-driven counter reads see GPU events before the final bus wait. + * CPU-clock counter modes still sample after that wait. Physical address. */ +int timers_source_hblank_counter_read(uint32_t addr); +void timers_source_raster_event(void *context, uint64_t cycle, unsigned event, int blank); +void timers_source_raster_finish(uint64_t cycle); void timers_get_snapshot(uint16_t counter[3], uint32_t mode[3], uint16_t target[3], int32_t irq_line[3], uint32_t frac[3]); diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 2526ded97..d1f7dbdb1 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -244,6 +244,7 @@ if(PSX_RECOMP_UI AND (NOT RECOMP_UI_ROOT OR RECOMP_UI_ROOT STREQUAL "")) endif() set(PSXRECOMP_RUNTIME_SOURCES + ${PSXRECOMP_ROOT}/runtime/src/input_route_observer.c ${PSXRECOMP_ROOT}/runtime/src/main.cpp ${PSXRECOMP_ROOT}/runtime/src/psx_window_icon.cpp ${PSXRECOMP_ROOT}/runtime/src/psx_sdl_audio.cpp @@ -251,6 +252,7 @@ set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/memory.c ${PSXRECOMP_ROOT}/runtime/src/guest_tty.c ${PSXRECOMP_ROOT}/runtime/src/gpu.c + ${PSXRECOMP_ROOT}/runtime/src/source_gpu_runtime.c ${PSXRECOMP_ROOT}/runtime/src/ws_ui_group.c ${PSXRECOMP_ROOT}/runtime/src/ws_aspect_cone_math.c ${PSXRECOMP_ROOT}/runtime/src/gpu_sw_renderer.c diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 7d100e8da..7f6a0e828 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -651,6 +651,16 @@ static int sector_delay_cycles(void) { } static int initial_read_delay_cycles(void) { + /* Explicit source-core comparison profile, not a hardware timing claim. + * Octoshock 2.2.2 fills two pipeline slots before presenting the oldest + * sector on its third fetch (cdc.h SectorPipe_Count / HandlePlayRead). + * This reproduces that start deadline only; seek jitter, command latency, + * and rotating drive state are separate, still-unqualified differences. */ + const char *profile = getenv("PSX_CD_READ_START_MODEL"); + if (profile && strcmp(profile, "octoshock-2.2.2-pipeline") == 0) + return apply_read_speed(((mode_reg & 0x80) + ? CDROM_SINGLE_SPEED_SECTOR_CYCLES / 2 + : CDROM_SINGLE_SPEED_SECTOR_CYCLES) * 3); /* Beetle/PCSX model an additional read-start latency after ReadN/ReadS. * In double-speed mode the first sector still waits one 1x sector period; * subsequent sectors use the steady-state 2x cadence above. */ @@ -749,8 +759,11 @@ static CDROMTraceEntry cdrom_trace[CDROM_TRACE_CAP]; static uint64_t cdrom_trace_seq; static void trace_cdrom(uint8_t kind, uint32_t addr, uint32_t val, uint8_t width) { + extern uint32_t debug_guest_ra(void); CDROMTraceEntry *e = &cdrom_trace[cdrom_trace_seq % CDROM_TRACE_CAP]; e->seq = cdrom_trace_seq++; + e->cycle = psx_cycle_count; + e->guest_ra = debug_guest_ra(); e->kind = kind; e->addr = addr; e->val = val; @@ -779,6 +792,15 @@ static void trace_cdrom(uint8_t kind, uint32_t addr, uint32_t val, uint8_t width e->read_delay = read_delay; } +#include "cdrom_random_tape.h" +static CdRandomTape s_source_clock_tape; +static int s_source_clock; +/* Independent drive operation, not a command second response. */ +static uint64_t s_source_reset_due; +static uint64_t s_source_command_due, s_source_ready_due; +static uint32_t s_source_clock_calls; +static int s_source_command_phase, s_source_args_remaining; + static void record_command_history(uint8_t kind, uint8_t cmd, const uint8_t* params, int count) { CDROMCommandHistoryEntry *e = @@ -815,6 +837,9 @@ static void record_command_history(uint8_t kind, uint8_t cmd, e->pending_pending = (uint8_t)(pending.pending ? 1 : 0); e->queued_cmd = queued_cmd.cmd; e->queued_pending = (uint8_t)(queued_cmd.pending ? 1 : 0); + e->source_clock=(uint8_t)s_source_clock; + e->source_random_cursor=s_source_clock_tape.cursor; + e->source_random_calls=s_source_clock_calls; } static int xa_is_audio_realtime(const CDROMSectorDelivery *d) { @@ -879,6 +904,41 @@ static int has_disc(void) { #define CDSTAT_SEEKERR 0x04 #define CDSTAT_IDERROR 0x08 #define CDSTAT_SHELL 0x10 + +/* Explicit cold-start source compatibility only. Hardware identity is not + * inferred from the main BIOS. Defaults retain the existing PU-7 response. + * This does not reproduce the source drive's seek/rotation or swap lifecycle. */ +static int s_source_firmware_model; +static int s_source_cold_status_model; +static int s_source_toc_seek_model; +static int s_source_explicit_seek_model; +/* Source timing profile only: PAUSED and STANDBY have the same public status + * bits but different restart costs. Kept separate from visible READ/SEEK. */ +static uint8_t s_source_seek_paused; +/* Native MSF names the next delivered sector. The source drive has two + * additional sectors in its pipeline. Remember the stream origin so Pause + * can rewind up to four physical reads even before that pipeline is full. */ +static int s_source_read_start_lba; +static uint32_t source_clock_random(uint32_t maximum) { + uint32_t value; + if (!cd_tape_bounded(&s_source_clock_tape,maximum,&value)) { + fprintf(stderr,"[CDROM] Source clock random tape exhausted at word %u\n",s_source_clock_tape.cursor); + exit(2); + } + s_source_clock_calls++; + return value; +} +static int source_clock_receive_ready(void) { + return irq_flag==0 && (!s_source_clock || psx_cycle_count>=s_source_ready_due); +} +static int source_boot_model(const char *name) { + const char *value=getenv(name); + if(!value || !*value || strcmp(value,"default")==0) return 0; + if(strcmp(value,"octoshock-2.2.2")==0) return 1; + fprintf(stderr,"[CDROM] Unsupported %s=%s\n",name,value); + exit(2); +} + #define CDSTAT_READ 0x20 #define CDSTAT_SEEK 0x40 #define CDSTAT_PLAY 0x80 @@ -907,11 +967,18 @@ static void set_irq(int type) { * so re-arm the latch (the delayed present below raises it once). */ cdrom_irq_generation++; cdrom_intc_request_latched = 0; - /* Arm the presentation latency: this response must NOT be presented to INTC - * synchronously inside the guest store that triggered it (see the - * CDROM_IRQ_PRESENT_DELAY note). Absolute due — not a slice-relative - * countdown (see pending.due_cyc). */ - cdrom_irq_present_due = psx_cycle_count + (uint64_t)CDROM_IRQ_PRESENT_DELAY; + /* A command response must not appear inside the command/ack store that + * produced it. Asynchronous INT1 already comes from a sector deadline (or + * the separately scheduled pending-data-ready release). Its readable IRQ + * flag and INTC line must become visible together. Holding INTC for another + * 5000 cycles lets the next command's preparatory ACK erase a new sector + * notification before the CPU has ever received it. This preserves the + * single-outstanding latch and command queue; it changes no buffer owner. + * Octoshock 2.2.2 CheckAIP/WriteIRQ likewise publishes async flag+line together. + */ + cdrom_irq_present_due = (type == CDIRQ_DATA_READY || s_source_clock) ? psx_cycle_count + : psx_cycle_count + (uint64_t)CDROM_IRQ_PRESENT_DELAY; + if (s_source_clock) s_source_ready_due=0; /* 2000 clocks begin after IRQ acknowledgement. */ trace_cdrom('I', 0, (uint32_t)type, 0); /* DEQUEUE: CD response/data event fired (aux = CD irq type). */ event_ring_record_aux(EV_DEQ, (uint8_t)SRC_CD_IRQ, (uint32_t)type); @@ -1559,7 +1626,82 @@ static int read_continues_current_stream(void) { return 1; } +/* A pending Setloc is also a seek when consumed by ReadN/ReadS. The source + * comparison for this model is Octoshock 2.2.2 CalcSeekTime/ReadBase: + * https://github.com/TASEmulators/BizHawk/blob/2.2.2/psx/octoshock/psx/cdc.cpp + * Its deterministic component models travel across a 72-minute disc in one + * second, a 300ms long-seek settle, and a simplified paused-drive restart. + * This independently expressed lower bound omits its 0..25000-cycle jitter; + * it is an emulator timing model, not a hardware-calibrated exact guarantee. + * Command response and sector pipeline delays remain separate below. + */ +static int source_seek_lower_bound(int origin,int target,int motor_on,int paused,uint8_t mode) { + int64_t cycles=0; + if(!motor_on) {origin=0;cycles=33868800;} + int64_t distance=llabs((int64_t)target-origin); + int64_t travel=distance*33868800/(72*60*75); + cycles+=travel>20000?travel:20000; + if(distance>=2250)cycles+=10160640; + else if(paused)cycles+=(mode&0x80)?1237952:2475904; + return cycles>INT32_MAX?INT32_MAX:(int)cycles; +} +static int implicit_read_seek_cycles(void) { + if (s_source_clock) { + int origin=msf_to_lba(read_min,read_sec,read_sect); + int target=setloc_pending?s_setloc_lba:origin; + if(origin<0)origin=0; + if(target<0)target=0; + int delay=source_seek_lower_bound(origin,target,!!(stat_reg&CDSTAT_MOTOR),s_source_seek_paused,mode_reg); + uint32_t jitter=source_clock_random(25000); + return delay>INT32_MAX-(int)jitter?INT32_MAX:delay+(int)jitter; + } + if (!setloc_pending) return 0; + int origin=last_sector_lba>=0?last_sector_lba:0; + return apply_speed(source_seek_lower_bound(origin,s_setloc_lba,(stat_reg&CDSTAT_MOTOR)!=0, + !reading&&!(stat_reg&CDSTAT_PLAY),mode_reg)); +} +/* This optional comparison adds only the independently expressed source seek + * lower bound. The source's global PRNG jitter and physical drive-head position + * are not recreated. The first cold ReadTOC's paused/zero position is measured. */ +static int source_toc_seek_cycles(void) { + int origin=last_sector_lba>=0?last_sector_lba:0; + /* Pause rewinds the source drive head without changing the last sector + * delivered to the host. ReadTOC seeks from that stopped head, just as + * a subsequent explicit seek or resumed ReadN does. */ + if(s_source_clock && s_source_seek_paused && !reading) { + origin=msf_to_lba(read_min,read_sec,read_sect); + if(origin<0)origin=0; + } + int motor=(stat_reg&CDSTAT_MOTOR)!=0; + int paused=motor&&!reading&&!(stat_reg&(CDSTAT_READ|CDSTAT_SEEK|CDSTAT_PLAY)); + int delay=source_seek_lower_bound(origin,0,motor,paused,mode_reg); + uint32_t jitter=s_source_clock?source_clock_random(25000):0; + return delay>INT32_MAX-(int)jitter?INT32_MAX:delay+(int)jitter; +} + +static int source_explicit_seek_cycles(uint8_t cmd) { + /* Independently expressed deterministic part of original Octoshock 2.2.2 + * Command_SeekL/P. The native next-sector cursor approximates physical + * position; source jitter/rotation/pipeline head state remain unmodeled. */ + int origin = msf_to_lba(read_min, read_sec, read_sect); + int target = msf_to_lba(seek_min, seek_sec, seek_sect); + if (origin < 0) origin = 0; + if (target < 0) target = 0; + int seek = source_seek_lower_bound(origin, target, + !!(stat_reg & CDSTAT_MOTOR), s_source_seek_paused, mode_reg); + if(s_source_clock) { + uint32_t jitter=source_clock_random(25000); + seek=seek>INT32_MAX-(int)jitter?INT32_MAX:seek+(int)jitter; + } + int header_period = cmd == 0x15 ? + CDROM_SINGLE_SPEED_SECTOR_CYCLES / ((mode_reg & 0x80) ? 2 : 1) : 0; + return seek > INT32_MAX - header_period ? INT32_MAX : seek + header_period; +} + static void start_read_stream(uint8_t cmd) { + int source_target = setloc_pending ? s_setloc_lba : + msf_to_lba(read_min, read_sec, read_sect); + int seek_cycles = implicit_read_seek_cycles(); cdda_playing = 0; cdda_track = 0; cdda_delay = 0; @@ -1577,14 +1719,20 @@ static void start_read_stream(uint8_t cmd) { read_min = seek_min; read_sec = seek_sec; read_sect = seek_sect; + if (s_source_clock) { + lba_to_msf(source_target, 150, &read_min, &read_sec, &read_sect); + s_source_read_start_lba = source_target; + } read_cmd = cmd; - read_delay = initial_read_delay_cycles(); + read_delay = seek_cycles + initial_read_delay_cycles(); s_cd_probe_read_start_count++; s_cd_probe_read_start_cycles += (uint64_t)read_delay; s_cd_timing_next_due = psx_cycle_count + (uint64_t)read_delay; s_cd_timing_stream_starts++; reading = 1; - stat_reg |= CDSTAT_READ; + s_source_seek_paused = 0; + stat_reg &= (uint8_t)~(CDSTAT_SEEK | CDSTAT_READ | CDSTAT_PLAY); + stat_reg |= seek_cycles ? CDSTAT_SEEK : CDSTAT_READ; /* ENQUEUE: sector-read stream scheduled (due in read_delay cycles). A * content load that happens in OFF but not ON shows up as a missing * SRC_CD_READ enqueue here. */ @@ -1625,6 +1773,7 @@ static void stop_cdda_playback(void) { cdda_delay = 0; cdda_data_end_pending = 0; stat_reg &= (uint8_t)~CDSTAT_PLAY; + s_source_seek_paused = 1; } static void deliver_cdda_data_end(void) { @@ -1798,7 +1947,7 @@ static int data_fifo_ready(void) { static uint64_t s_dataready_fires; /* INT1 (data-ready) raised per streamed sector — FMV dispatch probe */ uint64_t cdrom_get_dataready_fires(void) { return s_dataready_fires; } -static int deliver_read_sector(void) { +static int deliver_read_sector(uint64_t timing_seq) { int delivered = read_sector_at(read_min, read_sec, read_sect); advance_msf(&read_min, &read_sec, &read_sect); if (!delivered) return 0; @@ -1807,6 +1956,7 @@ static int deliver_read_sector(void) { /* Delivered immediately: this INT1 announces the slot just filled. */ s_ring_read = s_ring_write; set_irq(CDIRQ_DATA_READY); + cd_timing_arm_irq(timing_seq); fire_cdrom_irq(); s_dataready_fires++; return 1; @@ -1921,6 +2071,16 @@ static void cd_bisect_cmd_log(const char *kind, uint8_t cmd, static void try_execute_queued_command(void) { if (!queued_cmd.pending || irq_flag != 0) return; + if(s_source_clock && (psx_cycle_count0) { + s_source_args_remaining--;s_source_command_phase=0; + s_source_command_due=psx_cycle_count+1815u; + } else { + s_source_command_phase=1;s_source_command_due=psx_cycle_count+8500u; + } + return; + } uint8_t cmd = queued_cmd.cmd; int count = queued_cmd.param_count; @@ -1939,6 +2099,26 @@ static void try_execute_queued_command(void) { } static void queue_or_exec_command(uint8_t cmd) { + if(s_source_clock) { + /* Source reception: 12315+jitter, then 1815 per argument and 8500. + * Capture the already-written argument packet. Post-command argument + * writes are outside this first clock profile's qualification. */ + if(param_count<0 || param_count>PARAM_FIFO_SIZE) { + fprintf(stderr,"[CDROM] Invalid source clock argument count\n");exit(2); + } + if(cmd==0x03) { + fprintf(stderr,"[CDROM] Source clock CDDA Play seek is not qualified\n");exit(2); + } + s_source_command_due=psx_cycle_count+12315u+source_clock_random(3000); + s_source_command_phase=-1;s_source_args_remaining=param_count; + queued_cmd.cmd=cmd;queued_cmd.param_count=param_count;queued_cmd.pending=1; + memcpy(queued_cmd.params,param_fifo,(size_t)param_count); + pending.pending=0; /* Source reception replaces an outstanding second response. */ + param_count=0; + trace_cdrom('Q',0,cmd,0); + record_command_history('Q',cmd,queued_cmd.params,queued_cmd.param_count); + return; + } if (irq_flag == 0) { exec_command(cmd); return; @@ -1977,6 +2157,15 @@ static int pause_complete_delay_cycles(void) { return 5000; int lba = reading ? msf_to_lba(read_min, read_sec, read_sect) : last_sector_lba; + if (s_source_clock && reading) { + /* Source Command_Pause rewinds min(4, physical reads). With its + * two-sector pipe this is max(stream origin, next delivery - 2). + * Before any delivery, all physical reads are rewound to the origin. + * Preserve that stopped head for a subsequent seek or resumed read. */ + lba -= 2; + if (lba < s_source_read_start_lba) lba = s_source_read_start_lba; + lba_to_msf(lba, 150, &read_min, &read_sec, &read_sect); + } if (lba < 0) lba = 0; int64_t cycles = 1124584 + (int64_t)lba * 42596 / (75 * 60); if (!(mode_reg & 0x80)) @@ -2035,6 +2224,9 @@ static void exec_command(uint8_t cmd) { memcpy(cmd_params, param_fifo, (size_t)cmd_param_count); response_clear(); + if(s_source_clock && s_source_reset_due && cmd!=0x01 && cmd!=0x0A) { + fprintf(stderr,"[CDROM] Source command %02X during drive reset is not qualified\n",cmd);exit(2); + } switch (cmd) { case 0x01: /* GetStat */ if (has_disc()) { @@ -2043,6 +2235,7 @@ static void exec_command(uint8_t cmd) { stat_reg |= CDSTAT_SHELL; } response_push(stat_reg); + if(s_source_cold_status_model && has_disc()) stat_reg &= (uint8_t)~CDSTAT_SHELL; set_irq(CDIRQ_ACK); break; @@ -2070,8 +2263,8 @@ static void exec_command(uint8_t cmd) { break; } if (read_continues_current_stream()) break; /* ACKed inside */ - start_read_stream(cmd); response_push(stat_reg); + start_read_stream(cmd); set_irq(CDIRQ_ACK); break; @@ -2114,6 +2307,7 @@ static void exec_command(uint8_t cmd) { * timeout) and never advances past its first content load. */ stop_read_stream(); stop_cdda_playback(); + s_source_seek_paused = 0; xa_reset_decode(); spu_cd_audio_reset(); stat_reg &= ~(CDSTAT_READ | CDSTAT_PLAY | CDSTAT_SEEK); @@ -2128,13 +2322,17 @@ static void exec_command(uint8_t cmd) { break; case 0x09: { /* Pause */ + /* Source Command_Pause queues MakeStatus before changing the drive + * state. Its ACK describes the active stream; completion describes + * the paused drive. Preserve the default response image. */ + uint8_t source_ack_status = stat_reg; int complete_delay = pause_complete_delay_cycles(); stop_read_stream(); stop_cdda_playback(); xa_reset_decode(); spu_cd_audio_reset(); - stat_reg &= ~(CDSTAT_READ | CDSTAT_PLAY); - response_push(stat_reg); + stat_reg &= ~(CDSTAT_READ | CDSTAT_PLAY | CDSTAT_SEEK); + response_push(s_source_clock ? source_ack_status : stat_reg); set_irq(CDIRQ_ACK); pending_arm(0x09, complete_delay, 1); s_cd_probe_pause_count++; @@ -2142,26 +2340,33 @@ static void exec_command(uint8_t cmd) { break; } - case 0x0A: /* Init */ + case 0x0A: /* Init (named Command_Reset in original Octoshock 2.2.2) */ + if(s_source_clock) { + if(reading || cdda_playing || pending_dataready) { + fprintf(stderr,"[CDROM] Source reset from an active stream is not qualified\n");exit(2); + } + /* ACK reports the old state. Repeated reset does not restart the + * drive timer. Empty/paused stream is the qualified source scope. */ + response_push(stat_reg); + set_irq(CDIRQ_ACK); + if(!s_source_reset_due) { + s_source_reset_due=psx_cycle_count+1136000u; + stat_reg=has_disc()?CDSTAT_MOTOR:CDSTAT_SHELL; + } + break; + } stop_read_stream(); stop_cdda_playback(); spu_cd_audio_reset(); xa_reset_decode(); stat_reg = has_disc() ? CDSTAT_MOTOR : CDSTAT_SHELL; + s_source_seek_paused = has_disc() ? 1 : 0; response_push(stat_reg); set_irq(CDIRQ_ACK); - /* The old 1136000-cycle (34 ms) value was borrowed from Beetle's - * Command_RESET — a different command. Init(0x0A) on an already- - * spinning drive completes far faster on real hardware, and OpenBIOS - * depends on it: its cdromInnerInit spin-waits only ~30000 loop - * iterations (single-digit ms) for the INT2 completion, then - * re-issues Init — which re-armed our 34 ms clock every retry, a - * permanent livelock (Init spam at 2/frame, stalling CD boot). - * Sony's driver never noticed: it waits on the completion event with - * no short timeout. 131072 cycles (~3.9 ms; an arbitrary power of - * two, not a measured constant) sits inside OpenBIOS's window and - * within real-hardware quick-init behavior; cross-check against - * Beetle's exact PS_CDC::Command_Init figure when refining. */ + /* Retained default OpenBIOS timeout accommodation. This arbitrary + * 131072-cycle delay is not a measured hardware constant. Original + * Octoshock's Command_Reset is this same opcode, despite its name; + * the explicit source clock above uses that core's drive deadline. */ pending_arm(0x0A, 131072, 1); break; @@ -2337,6 +2542,7 @@ static void exec_command(uint8_t cmd) { } } } + s_source_seek_paused = 0; response_push(stat_reg); set_irq(CDIRQ_ACK); break; @@ -2349,14 +2555,24 @@ static void exec_command(uint8_t cmd) { set_irq(CDIRQ_ERROR); break; } + { + int lat = s_source_explicit_seek_model ? source_explicit_seek_cycles(cmd) : + seek_complete_delay_cycles(); + /* Plain seeks ACK the old state, cancel the stream, then own SEEK. + * PSX-SPX SeekL/P; upstream cd55e8a stops/retargets the read producer. + * The already admitted guest data FIFO is independent and survives. */ + response_push(stat_reg); + set_irq(CDIRQ_ACK); + stop_read_stream(); xa_reset_decode(); spu_cd_audio_reset(); stop_cdda_playback(); - stat_reg |= CDSTAT_SEEK; - response_push(stat_reg); - set_irq(CDIRQ_ACK); - { - int lat = seek_complete_delay_cycles(); + s_source_seek_paused = 0; /* source STANDBY after plain seek */ + read_min = seek_min; + read_sec = seek_sec; + read_sect = seek_sect; + stat_reg = (stat_reg & ~(CDSTAT_READ | CDSTAT_PLAY)) | + CDSTAT_MOTOR | CDSTAT_SEEK; pending_arm(cmd, lat, 1); /* 0x15/0x16 — completed in process_pending */ s_cd_probe_seek_count++; s_cd_probe_seek_cycles += (uint64_t)lat; @@ -2381,8 +2597,8 @@ static void exec_command(uint8_t cmd) { break; } if (read_continues_current_stream()) break; /* ACKed inside */ - start_read_stream(cmd); response_push(stat_reg); + start_read_stream(cmd); set_irq(CDIRQ_ACK); break; @@ -2392,24 +2608,23 @@ static void exec_command(uint8_t cmd) { /* Beetle PS_CDC::Command_ReadTOC: ~30M cycles (a near-second TOC * re-scan; Beetle adds a seek term on top — we keep the dominant * constant). Unscaled — authentic-latency class. */ - pending_arm(0x1E, 30000000, 1); + { + int seek=s_source_toc_seek_model?source_toc_seek_cycles():0; + int delay=seek>INT32_MAX-30000000?INT32_MAX:30000000+seek; + pending_arm(0x1E, delay, 1); + s_source_seek_paused = 1; + } break; case 0x19: /* Test */ if (param_count >= 1 && param_fifo[0] == 0x20) { - /* CD controller firmware version (BCD date + region). This BIOS is - * SCPH-1001, whose sub-CPU reports the 1994 controller: 94/09/19 C0. - * The value must be < 0x95 in the high byte — the shell's CD-init - * (func at ROM 0x1DF50) sets kernel flag [0xA000DFFC]=1 when the - * version byte >= 0x95, which later makes the boot CD-open - * (0xBFC0D570) issue a spurious ReadTOC that wedges the game's - * streaming reads. Beetle hardcodes the PSone-era 0x97 regardless of - * BIOS, which is wrong for SCPH-1001; matching the real 1994 - * controller keeps the flag clear (Kula World demo-load wedge). */ - response_push(0x94); - response_push(0x09); - response_push(0x19); - response_push(0xC0); + /* Two documented HC05 identities: PU-7 default, PU-18 source. + * The old default also suppresses BIOS ReadTOC; retain it without + * claiming that firmware selection proves main-BIOS compatibility. */ + static const uint8_t version[2][4] = { + {0x94,0x09,0x19,0xC0}, {0x97,0x01,0x10,0xC2} + }; + for(int i=0;i<4;i++) response_push(version[s_source_firmware_model][i]); set_irq(CDIRQ_ACK); } else { response_push(stat_reg); @@ -2432,6 +2647,22 @@ static void exec_command(uint8_t cmd) { cd_bisect_cmd_log("ISSUE", cmd, cmd_params, cmd_param_count); } +static void process_source_reset(void) { + if(!s_source_clock || !s_source_reset_due || psx_cycle_counts_source_ready_due?s_source_command_due:s_source_ready_due; + best=(uint32_t)cycles_until_due(due); + } /* Armed response awaiting presentation (raises bit2 at present_due). */ if (cdrom_irq_mask_matches_reason(irq_enable, irq_flag)) { int rem = irq_present_rem_cycles(); @@ -2986,6 +3252,10 @@ uint32_t cdrom_cycles_to_irq(uint32_t i_mask) { /* Pending second response: slice to due_cyc while the drive clock runs. * Once due, presentation waits on irq_flag clear (CPU ack) — do not * advertise a deliverable CD IRQ until the FIFO is free. */ + if(s_source_clock && s_source_reset_due) { + uint32_t d=(uint32_t)cycles_until_due(s_source_reset_due); + if(d= pending_present_due) { pending_present_due = 0; - if (pending_dataready && irq_flag == 0) + if (pending_dataready && source_clock_receive_ready()) present_pending_dataready(); + else if(s_source_clock && pending_dataready && irq_flag==0) + pending_present_due=s_source_ready_due; } process_read_stream(cycles); process_cdda_stream(cycles); + /* A command admitted at this interval's end must not age its new stream + * by the interval that elapsed before admission. */ + if(s_source_clock)try_execute_queued_command(); refresh_cdrom_irq_line(); } @@ -3030,13 +3306,15 @@ void cdrom_tick(void) { cdrom_advance(33868u); } -uint32_t cdrom_dma_read(void) { +static uint32_t cdrom_dma_read_internal(int padded) { uint32_t val = 0; int got = 0; if ((request_reg & CDROM_REQUEST_BFRD) && rb_available() && - RB_.pos + 4 <= RB_.size) { - memcpy(&val, RB_.data + RB_.pos, 4); - RB_.pos += 4; + (padded || RB_.pos + 4 <= RB_.size)) { + int count=RB_.size-RB_.pos; + if(count>4)count=4; + memcpy(&val, RB_.data + RB_.pos, (size_t)count); + RB_.pos += count; got = 1; } /* Per-word DMA data reads flood the CD trace ring (hundreds per sector) and @@ -3054,6 +3332,9 @@ uint32_t cdrom_dma_read(void) { return val; } +uint32_t cdrom_dma_read(void) { return cdrom_dma_read_internal(0); } +uint32_t cdrom_dma_read_padded(void) { return cdrom_dma_read_internal(1); } + int cdrom_dma_ready(void) { if (request_reg & CDROM_REQUEST_BFRD) ring_note_starved(); return (request_reg & CDROM_REQUEST_BFRD) && rb_available() && @@ -3324,6 +3605,18 @@ static int cdrom_snap_emit(PstW *w) { W8(pending.cmd); WI(pending.pending); WI(pending_rem_cycles()); WI(pending.phase); W8(queued_cmd.cmd); WB(queued_cmd.params); WI(queued_cmd.param_count); WI(queued_cmd.pending); W8(pending_dataready); W8(pending_dataready_stat); + /* This explicit private profile adds its timing state to the CD section. + * Default bytes stay unchanged. Full-machine/cross-profile restore remains + * unqualified; matching-profile controller state is not reconstructed. */ + if (s_source_explicit_seek_model) W8(s_source_seek_paused); + if(s_source_clock) { + WI(s_source_read_start_lba); + WU(0x33434c43u);WB(s_source_clock_tape.sha256); + WU(s_source_clock_tape.count);WU(s_source_clock_tape.cursor);WU(s_source_clock_calls); + WI(s_source_command_phase);WI(s_source_args_remaining); + WI(cycles_until_due(s_source_command_due));WI(cycles_until_due(s_source_ready_due)); + WI(setloc_pending);WI(s_source_reset_due!=0);WI(cycles_until_due(s_source_reset_due)); + } #undef W8 #undef WI #undef WU @@ -3388,6 +3681,19 @@ static int cdrom_snap_parse(PstR *r) { R8(pending.cmd); RI(pending.pending); RI(pending_rem); RI(pending.phase); R8(queued_cmd.cmd); RB(queued_cmd.params); RI(queued_cmd.param_count); RI(queued_cmd.pending); R8(pending_dataready); R8(pending_dataready_stat); + if (s_source_explicit_seek_model) R8(s_source_seek_paused); + if(s_source_clock) { + uint8_t identity[32];uint32_t signature,count; + int command_rem,ready_rem,reset_active,reset_rem; + RI(s_source_read_start_lba); + RU(signature);RB(identity);RU(count);RU(s_source_clock_tape.cursor);RU(s_source_clock_calls); + RI(s_source_command_phase);RI(s_source_args_remaining); + RI(command_rem);RI(ready_rem);RI(setloc_pending);RI(reset_active);RI(reset_rem); + (void)signature;(void)count; /* Identity and range checks precede any parse mutation. */ + s_source_command_due=psx_cycle_count+(uint32_t)command_rem; + s_source_ready_due=psx_cycle_count+(uint32_t)ready_rem; + s_source_reset_due=reset_active?psx_cycle_count+(uint32_t)reset_rem:0; + } #undef R8 #undef RI #undef RU @@ -3416,6 +3722,21 @@ void cdrom_snapshot_write(uint8_t *p) { int cdrom_snapshot_read(const uint8_t *p, uint32_t len) { PstR r; if (len != cdrom_snapshot_bytes()) return 0; + uint32_t clock_bytes=s_source_clock?80u:0u; + if (s_source_explicit_seek_model && p[len-clock_bytes-1] > 1) return 0; + if(s_source_clock) { + const uint8_t *clock=p+len-clock_bytes+4; + int32_t phase=(int32_t)cd_tape_le32(clock+48); + if(cd_tape_le32(clock)!=0x33434c43u || memcmp(clock+4,s_source_clock_tape.sha256,32) || + cd_tape_le32(clock+36)!=s_source_clock_tape.count || + cd_tape_le32(clock+40)>s_source_clock_tape.count || + cd_tape_le32(clock+44)>cd_tape_le32(clock+40) || + phase < -1 || phase > 1 || cd_tape_le32(clock+52)>PARAM_FIFO_SIZE || + cd_tape_le32(clock+56)>INT32_MAX || cd_tape_le32(clock+60)>2000u || + cd_tape_le32(clock+64)>1u || cd_tape_le32(clock+68)>1u || + cd_tape_le32(clock+72)>1136000u || + (!cd_tape_le32(clock+68) && cd_tape_le32(clock+72))) return 0; + } pst_r_init(&r, p, len); if (!cdrom_snap_parse(&r)) return 0; diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 659cc725b..8758f68bb 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -282,7 +282,8 @@ static inline void rec_event(uint8_t kind, uint32_t addr, uint32_t val, void debug_server_trace_ram_read_watch(uint32_t phys, uint32_t val) { if (phys >= s_rwatch_lo && phys < s_rwatch_hi) - rec_event(REC_KIND_RAM_R, phys, val, 0, 0); + rec_event(REC_KIND_RAM_R, phys, val, 0, + debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0); } /* ---- CPU state pointer (set at init) ---- */ @@ -322,16 +323,54 @@ static uint8_t s_axis_st[4] = { 0x80, 0x80, 0x80, 0x80 }; * queue in debug_server_get_input_override() avoids host/TCP timing gaps * between short presses and remains deterministic while turbo loads are active. */ -#define INPUT_ROUTE_MAX_STEPS 4096 -typedef struct { - uint32_t frames; - uint16_t buttons; -} InputRouteStep; +#include "input_route_file.h" +#include "input_route_observer.h" static PSX_BSS InputRouteStep s_input_route[INPUT_ROUTE_MAX_STEPS]; static uint32_t s_input_route_count = 0; static uint32_t s_input_route_index = 0; static uint32_t s_input_route_remaining = 0; static int s_input_route_active = 0; +static int s_input_route_file_mode = 0; +static uint32_t s_input_route_consumed = 0; +#include "input_update_replay.h" +#include "input_instruction_histogram_impl.h" + +/* Called on the emulator thread before guest execution. Fail closed; a bad + * file never exposes a partial route. Existing TCP routes remain unchanged. */ +int debug_server_preload_input_route(const char *path) +{ + InputRouteStep *staged; + uint32_t count = 0, frames = 0; + FILE *f; + const char *error; + if (s_frame_count || s_input_route_active || s_input_route_count) return 0; + f = fopen(path, "rb"); + if (!f) return 0; + staged = (InputRouteStep *)calloc(INPUT_ROUTE_MAX_STEPS, sizeof(*staged)); + if (!staged) { fclose(f); return 0; } + error = input_route_read(f, staged, &count, &frames); + if (fclose(f) != 0 && !error) error = "close error"; + if (error) { + fprintf(stderr, "input route rejected: %s\n", error); + free(staged); + return 0; + } + memcpy(s_input_route, staged, count * sizeof(*staged)); + free(staged); + if (!update_configure(s_input_route,count,frames)) return 0; + if (!input_route_observer_init(s_update_enabled ? s_update_config[7] : frames)) return 0; + if (!input_instruction_histogram_configure()) return 0; + if (s_update_enabled) s_update_log=input_route_observer_output("update-clock.jsonl"); + s_input_override = -1; s_input_frames = 0; s_axis_override = 0; + s_input_route_count = count; s_input_route_index = 0; + s_input_route_remaining = s_input_route[0].frames; + s_input_route_active = 1; + s_input_route_file_mode = 1; + s_input_route_consumed = 0; + fprintf(stdout, "input_route_preloaded: start_frame=0 frames=%u steps=%u\n", + (unsigned)frames, (unsigned)count); + return 1; +} /* ---- Frontend turbo override ---- */ static volatile int s_turbo_enabled = 0; @@ -9486,6 +9525,7 @@ static void handle_card_trace_dump(int id, const char *json) void debug_server_trace_write_check(uint32_t phys, uint32_t old_val, uint32_t new_val, uint8_t width) { + update_accept_write(phys,width,g_debug_last_store_pc,debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0,old_val,new_val); #ifdef PSX_NO_DEBUG_TOOLS (void)phys; (void)old_val; (void)new_val; (void)width; return; @@ -12864,6 +12904,52 @@ static void handle_overlay_dump(int id, const char *json) * (very rare at dump time) cannot make us walk off the end. * ==================================================================== */ +void debug_server_dump_watched_writes(FILE *f, const uint32_t *addresses, uint32_t count) +{ + if (!f || !addresses || count > 32) return; + uint64_t total = s_wtrace_all_seq; + uint32_t avail = total < WRITE_TRACE_ALL_CAP ? (uint32_t)total : WRITE_TRACE_ALL_CAP; + uint32_t start = total < WRITE_TRACE_ALL_CAP ? 0 : s_wtrace_all_head; + fprintf(f, "{\"kind\":\"coverage\",\"total_writes\":%llu,\"retained_writes\":%u}\n", + (unsigned long long)total, s_wtrace_all ? avail : 0); + if (s_rec_frame >= 0) { + fprintf(f, "{\"kind\":\"recorded_frame_coverage\",\"frame\":%lld,\"count\":%u,\"overflow\":%u}\n", + (long long)s_rec_frame,s_rec_count,s_rec_overflow); + for (uint32_t i = 0; i < s_rec_count; ++i) { + const RecEntry *e = &s_rec_buf[i]; + if (e->kind == REC_KIND_MMIO_W && e->addr == 0x1F801040u) { + fprintf(f, "{\"kind\":\"recorded_sio_write\",\"i\":%u,\"frame\":%lld,\"cycle\":%llu,\"new\":%u,\"pc\":%u,\"ra\":%u}\n", + i,(long long)s_rec_frame,(unsigned long long)e->cyc,e->val,e->pc,e->ra); + } + if (e->kind == REC_KIND_MMIO_W || e->kind == REC_KIND_MMIO_R) { + fprintf(f, "{\"kind\":\"%s\",\"i\":%u,\"frame\":%lld,\"cycle\":%llu,\"addr\":%u,\"new\":%u,\"pc\":%u,\"ra\":%u}\n", + e->kind == REC_KIND_MMIO_R ? "recorded_mmio_read" : "recorded_mmio_write", + i,(long long)s_rec_frame,(unsigned long long)e->cyc,e->addr,e->val,e->pc,e->ra); + } + if (e->kind != REC_KIND_RAM_W && e->kind != REC_KIND_RAM_R) continue; + for (uint32_t j = 0; j < count; ++j) { + if (e->addr == addresses[j]) { + fprintf(f, "{\"kind\":\"%s\",\"i\":%u,\"frame\":%lld,\"cycle\":%llu,\"addr\":%u,\"new\":%u,\"pc\":%u,\"ra\":%u}\n", + e->kind == REC_KIND_RAM_R ? "recorded_read" : "recorded_write", + i,(long long)s_rec_frame,(unsigned long long)e->cyc,e->addr,e->val,e->pc,e->ra); + break; + } + } + } + } + if (!s_wtrace_all) return; + for (uint32_t i = 0; i < avail; ++i) { + const WriteTraceAllEntry *e = &s_wtrace_all[(start + i) % WRITE_TRACE_ALL_CAP]; + for (uint32_t j = 0; j < count; ++j) { + if (e->addr < addresses[j] + 2 && addresses[j] < e->addr + e->w) { + fprintf(f, "{\"seq\":%llu,\"addr\":%u,\"new\":%u,\"pc\":%u,\"ra\":%u,\"frame\":%u,\"width\":%u}\n", + (unsigned long long)e->seq,e->addr,e->new_val,e->pc,e->ra,e->frame,e->w); + break; + } + } + } +} + void debug_server_freeze_dump_wtrace_all_json(FILE *f, uint32_t max_count) { if (!f) return; @@ -13963,6 +14049,13 @@ static void process_command(const char *line) int id = json_get_int(line, "id", 0); + /* A preloaded evidence run cannot be edited or rescheduled over TCP. */ + if (s_input_route_file_mode && strcmp(cmd, "input_route_status") != 0 && + strcmp(cmd, "pad_status") != 0) { + send_err(id, "file replay permits only input_route_status and pad_status"); + return; + } + for (const CmdEntry *e = s_commands; e->name; e++) { if (strcmp(cmd, e->name) == 0) { /* Suppress lockstep memory recording for the WHOLE handler. @@ -14689,10 +14782,28 @@ int debug_server_is_connected(void) return s_client != SOCK_INVALID; } +void debug_server_note_input_applied(void) +{ + if (s_input_route_file_mode) + input_route_observer_applied(sio_get_pad_buttons_slot(0), + sio_get_pad_connected(0), sio_get_pad_analog(0)); +} + int debug_server_get_input_override(void) { + update_boundary(); + input_instruction_histogram_boundary(s_frame_count); + if (s_input_route_file_mode) + input_route_observer_boundary(s_input_route_consumed, s_frame_count); + if (s_update_enabled) { + input_route_observer_input(0xFFFF); ++s_input_route_consumed; return 0xFFFF; + } if (s_input_route_active && s_input_route_index < s_input_route_count) { int current = (int)s_input_route[s_input_route_index].buttons; + if (s_input_route_file_mode) { + input_route_observer_input((uint16_t)current); + ++s_input_route_consumed; + } if (s_input_route_remaining > 0 && --s_input_route_remaining == 0) { s_input_route_index++; if (s_input_route_index < s_input_route_count) { @@ -14704,6 +14815,12 @@ int debug_server_get_input_override(void) } return current; } + /* A file route never releases P1 to physical input at EOF. */ + if (s_input_route_file_mode) { + input_route_observer_input(0xFFFF); + ++s_input_route_consumed; + return 0xFFFF; + } int current = s_input_override; if (s_input_override >= 0 && s_input_frames > 0) { if (--s_input_frames == 0) diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 1fd738b2d..121094354 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -39,6 +39,8 @@ #include #include #include +#include "source_gpu_runtime.h" +#include "source_cpu_block_bound.h" uint64_t g_dirty_ram_blocks_run = 0; uint64_t g_dirty_ram_insns_run = 0; @@ -92,7 +94,7 @@ extern int g_ls_replay_active; /* defined in the lockstep section; used by e extern uint64_t g_psx_cycle_fast_limit; extern int g_event_step_conservative; -static inline void interp_cyc_step(CPUState *cpu, uint32_t reg_mask) { +static inline void interp_cyc_step(CPUState *cpu, uint32_t reg_mask, uint32_t load_rt) { uint8_t w = cpu->read_absorb_which; if (cpu->read_absorb[w]) { cpu->read_absorb[w]--; @@ -106,11 +108,15 @@ static inline void interp_cyc_step(CPUState *cpu, uint32_t reg_mask) { } } psx_cyc_deps(cpu, reg_mask); + if (load_rt < 32u && cpu->ld_which_t == load_rt) cpu->ld_which_t = 0u; psx_cyc_lds(cpu); } #else -static inline void interp_cyc_step(CPUState *cpu, uint32_t reg_mask) { - psx_cyc_step(cpu, reg_mask); +static inline void interp_cyc_step(CPUState *cpu, uint32_t reg_mask, uint32_t load_rt) { + psx_cyc_base(cpu); + psx_cyc_deps(cpu, reg_mask); + if (load_rt < 32u && cpu->ld_which_t == load_rt) cpu->ld_which_t = 0u; + psx_cyc_lds(cpu); } #endif @@ -134,6 +140,24 @@ static int interp_exception(CPUState *cpu, uint32_t exc_code, return 1; } +/* Synchronous arithmetic faults use the executing opcode and, when present, + * its branch owner. Unlike a native dispatcher return, this is an actual + * guest exception boundary and retires the older load before the handler. */ +static struct { uint32_t pc,target;int taken,active; } arithmetic_slot; +static uint64_t arithmetic_exceptions; +static int interp_arithmetic_overflow(CPUState *cpu,uint32_t pc,uint32_t insn) { + uint32_t pending=cpu->cop0[13]&0x0000ff00u; + dirty_ram_ld_delay_flush(cpu); + interp_exception(cpu,12u,cpu->cop0[8],pc); + cpu->cop0[13]=pending|(12u<<2)|((insn<<2)&0x30000000u); + if(arithmetic_slot.active && arithmetic_slot.pc==pc) { + cpu->cop0[14]=pc-4u;cpu->cop0[6]=arithmetic_slot.target; + cpu->cop0[13]|=0x80000000u|(arithmetic_slot.taken?0x40000000u:0u); + } + arithmetic_exceptions++; + return 1; +} + #ifdef PSX_COSIM static int g_cosim_exec_one_hooked = 0; static void cosim_exec_one_begin(void) { g_cosim_exec_one_hooked = 0; } @@ -149,6 +173,9 @@ static int cosim_exec_one_did_hook(void) { return 0; } static void cosim_exec_one_transfer_hook(uint32_t pc) { (void)pc; } #endif uint64_t g_slice_fired = 0; /* diagnostic: slices actually run */ +/* Passive snapshots of an already-computed block gate; no extra device query. */ +uint32_t g_slice_probe_pc, g_slice_probe_deadline, g_slice_probe_bound; +uint64_t g_slice_probe_cycle; uint64_t g_slice_irq_taken = 0; /* diagnostic: IRQs taken inside precise-mode */ /* First-divergence trace for the precise slice (PRECISE_IRQ_SLICE.md Task #4). */ uint32_t g_slice_last_block = 0; /* block_addr the guard fired on */ @@ -322,9 +349,11 @@ int psx_exec_phase(void) { return g_exec_phase; } * 128-byte memory-card frame was stored to one address (cards read as * permanently unformatted; the send side transmitted one byte 128x). * - * Contract: on a same-register conflict the LOAD wins (it retires later), - * matching the compiled emitter. Pending state is flushed on interpreter exit - * and before exception delivery, where the pipeline would have drained. */ + * The original source commits the older load before an ordinary successor's + * write, so that successor wins a same-register write conflict. A second load + * to the same destination cancels the older load instead. Kernel transfers + * retain this owner through the actual guest load-delay instruction. Mixed + * compiled/interpreted handoffs outside that region remain separately scoped. */ static uint32_t s_ld_pend_rt = 0; static uint32_t s_ld_pend_val = 0; static uint32_t s_ld_pend_age = 0; /* 0 = armed; 1 = delay slot has run */ @@ -510,6 +539,12 @@ static inline uint32_t target26 (uint32_t i) { return i & 0x03FFFFFFu /* Read a 32-bit instruction word from kernel RAM at the given physical addr. * Caller has already verified the address is in dirty kernel RAM. */ static inline uint32_t fetch_word(uint32_t phys) { + if (phys >= 0x1FC00000u && phys < 0x1FC80000u) { + /* Source-profile precision slices may enter the selected BIOS ROM. + * This is a byte fetch; instruction timing remains in exec_one. */ + extern uint32_t psx_read_word(uint32_t); + return psx_read_word(phys); + } /* Main RAM is a process-lifetime static allocation. Cache its address so * instruction fetch does not cross translation units for every guest op. */ static const uint8_t *ram; @@ -1392,16 +1427,49 @@ static int interp_enter_compiled(CPUState *cpu, uint32_t target) { * Branches encode their delay slot themselves before returning 1. */ static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t *next_pc_out); +static int exec_one_fetched_context(CPUState *cpu, uint32_t pc, uint32_t insn, + uint32_t *next_pc_out, int in_slot, + uint32_t target, int taken); +static int precise_irq_deliverable(CPUState *cpu); +static int precise_irq_before(CPUState *cpu,uint32_t pc) { + return precise_irq_deliverable(cpu) && psx_irq_opcode_eligible(pc); +} +static int source_dirty_irq_before(CPUState *cpu,uint32_t pc) { + /* Ordinary dirty/kernel interpretation owns real instruction boundaries + * even when the compiled-block precision slicer is inactive. A pending + * source-model IRQ must preempt this opcode, not a later dispatch target. */ + if(!g_precise_mode && source_gpu_runtime_active() && precise_irq_before(cpu,pc)) { + extern uint64_t g_irq_deliver_count; + uint64_t before=g_irq_deliver_count; + uint32_t previous=g_dirty_safe_resume_pc; + cpu->pc=pc;g_dirty_safe_resume_pc=pc; + dirty_ram_ld_delay_flush(cpu); + psx_check_interrupts(cpu); + g_dirty_safe_resume_pc=previous; + if(g_irq_deliver_count!=before) { + if(!cpu->pc)cpu->pc=pc; + return 1; + } + } + return 0; +} static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) { + if(source_dirty_irq_before(cpu,pc))return 1; return exec_one_fetched(cpu, pc, fetch_word(pc & 0x1FFFFFFFu), next_pc_out); } /* Forward: helper for delay-slot execution on jumps/branches. */ -static void exec_delay_slot(CPUState *cpu, uint32_t pc) { +static int exec_delay_slot(CPUState *cpu,uint32_t pc,uint32_t target,int taken) { /* Delay-slot instruction at pc must NOT be a control transfer. * Recursively interpret as a single non-branching instruction. */ uint32_t ds_phys = pc & 0x1FFFFFFFu; uint32_t insn = fetch_word(ds_phys); + if(source_gpu_runtime_active() && precise_irq_before(cpu,pc)) { + dirty_ram_ld_delay_flush(cpu); + if(psx_check_interrupts_delay_slot(cpu,pc,target,taken,insn)) { + g_slice_irq_taken++;return 1; + } + } uint32_t opc = op_field(insn); uint32_t fnt = funct_field(insn); /* Reject branches/jumps in delay slots — undefined on R3000A and our @@ -1414,15 +1482,28 @@ static void exec_delay_slot(CPUState *cpu, uint32_t pc) { opc == 0x01 /*regimm*/ || (opc == 0x00 && (fnt == 0x08 /*jr*/ || fnt == 0x09 /*jalr*/))) { (void)abort_unsupported(pc, insn, "control-transfer in delay slot"); - return; + return 0; } uint32_t dummy_next = 0; - (void)exec_one_fetched(cpu, pc, insn, &dummy_next); + const int arithmetic=opc==8u || (opc==0u && (fnt==0x20u || fnt==0x22u)); + uint64_t faults=arithmetic_exceptions; + /* Only these non-branching arithmetic opcodes can own this context; + * RFE/host escape paths never leave an active context behind. */ + if(arithmetic) { + arithmetic_slot.pc=pc;arithmetic_slot.target=target;arithmetic_slot.taken=taken; + arithmetic_slot.active=1; + } + uint64_t slot_takes=g_slice_irq_taken; + (void)exec_one_fetched_context(cpu,pc,insn,&dummy_next,1,target,taken); + if(arithmetic)arithmetic_slot.active=0; g_dirty_ram_insns_run++; + if(g_slice_irq_taken!=slot_takes)return 1; + if(arithmetic_exceptions!=faults)return 1; /* CYCLE MODEL: the delay-slot instruction is a real retired R3000A instruction * and is charged its own per-instruction interlock INSIDE exec_one (top-of-fn * §1+deps+DO_LDS, or psx_cyc_load_* for a load delay slot) — so a branch+slot * pair costs both, matching hardware. No separate charge here. */ + return 0; } /* Load-delay shim around the decoder (see dirty_ram_ld_delay_flush above). @@ -1432,8 +1513,76 @@ static void exec_delay_slot(CPUState *cpu, uint32_t pc) { static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t *next_pc_out); +static uint32_t delayed_value_writer(CPUState *cpu,uint32_t insn) { + uint32_t op=op_field(insn),fn=funct_field(insn),rs=rs_field(insn); + /* Faulting arithmetic has no destination write; exception entry still + * owns retirement of the older pending load. Evaluate with slot operands. */ + if(op==8u || (!op && (fn==0x20u || fn==0x22u))) { + int64_t a=(int32_t)cpu->gpr[rs]; + int64_t b=op==8u?(int16_t)insn:(int32_t)cpu->gpr[rt_field(insn)]; + int64_t result=!op && fn==0x22u?a-b:a+b; + if(resultINT32_MAX)return 0u; + } + if(!op) { + if(fn==0u || (fn>=2u && fn<=4u) || fn==6u || fn==7u || fn==9u || fn==0x10u || fn==0x12u || + (fn>=0x20u && fn<=0x27u) || fn==0x2au || fn==0x2bu) + return rd_field(insn); + } else if(op==3u || (op==1u && (rt_field(insn)==0x10u || rt_field(insn)==0x11u)))return 31u; + else if((op>=8u && op<=0xfu) || (op>=0x20u && op<=0x26u))return rt_field(insn); + else if(op==0x10u && rs==0u && rd_field(insn)<16u && ((1u<cop0[12]&0x40000000u) && (rs==0u || rs==2u))return rt_field(insn); + return 0u; +} + static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t *next_pc_out) { + return exec_one_fetched_context(cpu,pc,insn,next_pc_out,0,0,0); +} + +static int exec_one_fetched_context(CPUState *cpu, uint32_t pc, uint32_t insn, + uint32_t *next_pc_out, int in_slot, + uint32_t target, int taken) { + if(source_gpu_runtime_active()) { + /* The boundary may wait through a DMA halt. An IRQ raised in that + * interval preempts this opcode, before its load cancellation or + * register effects. The IRQ path owns its fetch and ordinary step. + * Source COP2 bypasses the halt/interrupt opcode table. */ + psx_cpu_step_boundary(cpu,pc); + if(op_field(insn)!=0x12u && precise_irq_deliverable(cpu)) { + extern uint64_t g_irq_deliver_count; + uint64_t before=g_irq_deliver_count; + dirty_ram_ld_delay_flush(cpu); + if(in_slot) { + arithmetic_slot.active=0; + (void)psx_check_interrupts_delay_slot(cpu,pc,target,taken,insn); + } else { + uint32_t previous=g_dirty_safe_resume_pc; + cpu->pc=pc;g_dirty_safe_resume_pc=pc; + psx_check_interrupts(cpu); + g_dirty_safe_resume_pc=previous; + if(!cpu->pc)cpu->pc=pc; + } + if(g_irq_deliver_count!=before) { + g_slice_irq_taken++; + return 1; + } + } + } + const uint32_t ld_op = op_field(insn); + const uint32_t ld_rt = rt_field(insn); + const uint32_t pc_phys = pc & 0x1FFFFFFFu; + const int in_bios_rom = pc_phys >= 0x1FC00000u && pc_phys < 0x1FC80000u; + const int in_bios_kernel_ram = pc_phys < 0x00010000u; + /* Source COP2-to-GPR moves use the CPU's delayed value slot as well. + * Precise execution retains ownership while that slot is armed, including + * a move in a branch delay slot followed by a compiled block leader. + * Ordinary high-RAM mixed dispatch still has the historical eager-value + * contract; it cannot yet carry a pending value across every host return. */ + const int source_gte_value = source_gpu_runtime_active() && + (g_precise_mode || in_bios_rom || in_bios_kernel_ram) && + ld_op==0x12u && (rs_field(insn)==0u || rs_field(insn)==2u) && + (cpu->cop0[12]&0x40000000u); + /* A load's writeback becomes visible to the instruction AFTER its delay * slot: load at N, hidden from N+1, visible from N+2. s_ld_pend_age tracks * that: 0 = armed by the instruction just executed, 1 = the delay slot has @@ -1464,39 +1613,43 @@ static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn, cpu->gpr[0] = 0; } else { s_ld_pend_age = 1u; /* this instruction IS the delay slot: stay hidden */ + /* Its operands still see the old GPR. An ordinary write by this + * successor then supersedes the pending write. Loads retain the + * separate same-destination cancellation below. */ + const int successor_load=(nx_op>=0x20u && nx_op<=0x26u) || + (nx_op==0x10u && rs_field(insn)==0u) || source_gte_value; + if(!successor_load && delayed_value_writer(cpu,insn)==s_ld_pend_rt) + s_ld_pend_armed=0; } } /* op 0x20..0x26 = LB/LH/LWL/LW/LBU/LHU/LWR. LWC2 (GTE, 0x32) targets a COP2 * register, not a GPR, so it needs no deferral here. */ - const uint32_t ld_op = op_field(insn); - const uint32_t ld_rt = rt_field(insn); - const uint32_t pc_phys = pc & 0x1FFFFFFFu; /* OpenBIOS executes cardfasttrack both from ROM and from its low-RAM * kernel copy (for example 0x3554..0x36D4). Its hand-written dependent * load requires the real R3000A value delay. Game-owned dirty RAM crosses * mixed compiled/interpreted boundaries that do not carry this pending * writeback yet, so preserve the historical eager-value contract there. */ - const int in_bios_rom = - pc_phys >= 0x1FC00000u && pc_phys < 0x1FC80000u; - const int in_bios_kernel_ram = pc_phys < 0x00010000u; - const int is_ld = (in_bios_rom || in_bios_kernel_ram) && - (ld_op >= 0x20u && ld_op <= 0x26u) && + const int mfc0_value = ld_op==0x10u && rs_field(insn)==0u && + rd_field(insn)<16u && ((1u<= 0x20u && ld_op <= 0x26u) || mfc0_value)) || + source_gte_value) && (ld_rt != 0u); const uint32_t ld_before = is_ld ? cpu->gpr[ld_rt] : 0u; const int rv = exec_one_fetched_inner(cpu, pc, insn, next_pc_out); - if (is_ld) { + if (is_ld && rv==0) { const uint32_t loaded = cpu->gpr[ld_rt]; - if (loaded != ld_before) { + { /* Back-to-back loads: retire the older writeback before reusing the * slot, otherwise its register write would be dropped entirely. */ if (s_ld_pend_armed && s_ld_pend_rt != ld_rt && s_ld_pend_rt != 0u) cpu->gpr[s_ld_pend_rt] = s_ld_pend_val; /* Undo the eager write; it becomes visible one instruction later. - * On a same-register conflict this naturally makes the LOAD win, - * matching what the compiled backend emits for a dependent pair. */ + * A same-register second load cancels the older pending write, + * including when the new loaded bytes equal the original GPR. */ cpu->gpr[ld_rt] = ld_before; s_ld_pend_rt = ld_rt; s_ld_pend_val = loaded; @@ -1526,7 +1679,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, /* Instruction FETCH cost (I-cache) — charged FIRST, before the §1 base, exactly * like Beetle ReadInstruction precedes the per-instruction base (cpu.cpp). HIT=+0, * KSEG1=+4, cached miss=+3+refill; a miss also clears the load give-back. */ - psx_icache_fetch_interp(cpu, pc); + if(source_gpu_runtime_active())psx_icache_fetch_interp_after_boundary(cpu,pc); + else psx_icache_fetch_interp(cpu, pc); /* Per-instruction R3000A load-delay interlock (single-source: psx_cyc.h, shared * with both static emitters). §1 base + GPR_DEPRES + DO_LDS run HERE, before the @@ -1534,8 +1688,16 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, * (Beetle order). CPU loads (op 0x20-0x26) are skipped here — psx_cyc_load_* runs * their full interlock inside the body (and arms LDWhich=rt). This replaces the * old flat per-instruction psx_advance_cycles(psx_instr_base_cycles). */ - if (!(opc >= 0x20u && opc <= 0x26u)) - interp_cyc_step(cpu, psx_cyc_dep_res_mask(insn)); + if (source_gpu_runtime_active() && opc == 0x12u && + (rs == 0u || rs == 2u) && (cpu->cop0[12] & 0x40000000u)) { + /* MFC2/CFC2 cancel an older load to the same GPR before DO_LDS. + * The canceled load's timing credit belongs to the zero slot, just + * as for an ordinary CPU load. Moving it to the destination instead + * loses that credit when a later cache miss clears the active slot. + * COP2 reads have no GPR dependency/result clear in the source CPU. */ + interp_cyc_step(cpu, 0u, rt); + } else if (!(opc >= 0x20u && opc <= 0x26u)) + interp_cyc_step(cpu, psx_cyc_dep_res_mask(insn), 32u); #endif /* Widescreen far-backdrop column PRELOAD (auto_backdrop). At a detected @@ -1649,7 +1811,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, case 0x08: { /* JR rs */ uint32_t target = cpu->gpr[rs]; if (target & 3) return interp_exception(cpu, 4, target, pc); /* LoadAddressError */ - exec_delay_slot(cpu, pc + 4); + if(exec_delay_slot(cpu,pc+4,target,1))return 1; cosim_exec_one_transfer_hook(pc + 4); /* crossing (if target is compiled) is counted at the block-loop * tail-transfer site (interp_enter_compiled, §18) — not here, to @@ -1672,7 +1834,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, * leaks psx_dispatch_call frames. */ if (rd != 0) cpu->gpr[rd] = return_pc; cpu->gpr[0] = 0; - exec_delay_slot(cpu, pc + 4); + if(exec_delay_slot(cpu,pc+4,target,1))return 1; cosim_exec_one_transfer_hook(pc + 4); uint32_t site_sp = cpu->gpr[29]; /* call contract: sp at the call */ #ifndef PSX_NO_DEBUG_TOOLS @@ -1812,17 +1974,23 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, psx_muldiv_set(cpu, 37u); /* DIVU completion deadline (fixed) */ #endif return 0; - case 0x20: /* ADD - overflow traps are delegated if they occur. */ + case 0x20: /* ADD */ case 0x21: { /* ADDU rd, rs, rt */ uint32_t a = cpu->gpr[rs], b = cpu->gpr[rt]; + int64_t signed_result=(int64_t)(int32_t)a+(int64_t)(int32_t)b; + if(fnt==0x20u && (signed_resultINT32_MAX)) + return interp_arithmetic_overflow(cpu,pc,insn); cpu->gpr[rd] = a + b; psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b); cpu->gpr[0] = 0; return 0; } - case 0x22: /* SUB - overflow traps are delegated if they occur. */ + case 0x22: /* SUB */ case 0x23: { /* SUBU */ uint32_t a = cpu->gpr[rs], b = cpu->gpr[rt]; + int64_t signed_result=(int64_t)(int32_t)a-(int64_t)(int32_t)b; + if(fnt==0x22u && (signed_resultINT32_MAX)) + return interp_arithmetic_overflow(cpu,pc,insn); if (rs == 0 && psx_ws_is_cull_negsub_site(pc)) cpu->gpr[rd] = 0u - b - (uint32_t)psx_ws_x_margin(); else @@ -1877,7 +2045,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, case 0x02: { /* J target */ uint32_t target = ((pc + 4) & 0xF0000000u) | (target26(insn) << 2); - exec_delay_slot(cpu, pc + 4); + if(exec_delay_slot(cpu,pc+4,target,1))return 1; cosim_exec_one_transfer_hook(pc + 4); /* crossing counted at the block-loop tail-transfer site (§18). */ cpu->pc = target; @@ -1887,7 +2055,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t target = ((pc + 4) & 0xF0000000u) | (target26(insn) << 2); uint32_t return_pc = pc + 8; cpu->gpr[31] = return_pc; - exec_delay_slot(cpu, pc + 4); + if(exec_delay_slot(cpu,pc+4,target,1))return 1; cosim_exec_one_transfer_hook(pc + 4); uint32_t site_sp = cpu->gpr[29]; /* call contract: sp at the call */ #ifndef PSX_NO_DEBUG_TOOLS @@ -1942,28 +2110,28 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, } case 0x04: { /* BEQ rs, rt, simm */ int taken = (cpu->gpr[rs] == cpu->gpr[rt]); - exec_delay_slot(cpu, pc + 4); + if(exec_delay_slot(cpu,pc+4,taken?(pc+4+(simm<<2)):(pc+8),taken))return 1; cosim_exec_one_transfer_hook(pc + 4); cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8); return 1; } case 0x05: { /* BNE */ int taken = (cpu->gpr[rs] != cpu->gpr[rt]); - exec_delay_slot(cpu, pc + 4); + if(exec_delay_slot(cpu,pc+4,taken?(pc+4+(simm<<2)):(pc+8),taken))return 1; cosim_exec_one_transfer_hook(pc + 4); cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8); return 1; } case 0x06: { /* BLEZ */ int taken = ((int32_t)cpu->gpr[rs] <= 0); - exec_delay_slot(cpu, pc + 4); + if(exec_delay_slot(cpu,pc+4,taken?(pc+4+(simm<<2)):(pc+8),taken))return 1; cosim_exec_one_transfer_hook(pc + 4); cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8); return 1; } case 0x07: { /* BGTZ */ int taken = ((int32_t)cpu->gpr[rs] > 0); - exec_delay_slot(cpu, pc + 4); + if(exec_delay_slot(cpu,pc+4,taken?(pc+4+(simm<<2)):(pc+8),taken))return 1; cosim_exec_one_transfer_hook(pc + 4); cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8); return 1; @@ -1987,15 +2155,18 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, cpu->gpr[31] = pc + 8; break; default: return abort_unsupported(pc, insn, "REGIMM rt"); } - exec_delay_slot(cpu, pc + 4); + if(exec_delay_slot(cpu,pc+4,taken?(pc+4+(simm<<2)):(pc+8),taken))return 1; cosim_exec_one_transfer_hook(pc + 4); cpu->pc = taken ? (pc + 4 + (simm << 2)) : (pc + 8); return 1; } - case 0x08: /* ADDI rt, rs, simm — same as ADDIU, sans overflow trap (we don't model traps here) */ + case 0x08: /* ADDI rt, rs, simm */ { uint32_t widened = 0; uint32_t a = cpu->gpr[rs]; + int64_t signed_result=(int64_t)(int32_t)a+(int64_t)simm; + if(signed_resultINT32_MAX) + return interp_arithmetic_overflow(cpu,pc,insn); if (psx_ws_angle_site(pc, insn, &widened)) cpu->gpr[rt] = widened; else @@ -2505,6 +2676,9 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr) { * boundary? Mirrors the gate in psx_check_interrupts: a pending+unmasked I_STAT * bit, COP0 IEc + IM2 set, and not already inside the exception handler. */ static int precise_irq_deliverable(CPUState *cpu) { + /* A caller may own a generated block/local cycle batch. Publish the + * completed instruction before inspecting the device IRQ line. */ + psx_cyc_batch_flush(); extern uint32_t i_stat; uint32_t sr = cpu->cop0[12]; /* COP0 software interrupts (CAUSE.IP0/IP1 & SR.IM0/IM1): guest-raised via @@ -2512,6 +2686,10 @@ static int precise_irq_deliverable(CPUState *cpu) { uint32_t sw_pending = cpu->cop0[13] & sr & 0x0300u; if ((i_stat & i_mask) == 0 && sw_pending == 0) return 0; if (psx_get_in_exception()) return 0; + /* The actual delivery routine can decline during its inherited cooldown. + * Predicting a take here would return at the same PC with no guest cycles, + * so that cycle-based cooldown could never expire. */ + if (psx_interrupt_cooldown_active()) return 0; if (!(sr & 0x1u)) return 0; /* IEc: interrupts globally enabled */ /* INTC needs IM2; a pending software interrupt is deliverable without it. */ if (!(sr & (1u << 10)) && sw_pending == 0) return 0; @@ -2531,6 +2709,10 @@ static int precise_irq_deliverable(CPUState *cpu) { * clean-text PC: it keeps interpreting (exec_one handles arbitrary mid-function * flow) until cpu->pc satisfies this predicate. */ static int precise_pc_dispatchable(uint32_t pc) { + /* Generated entries do not carry the interpreter's deferred GPR value. + * Keep its owner through the actual load-delay instruction; an eager + * boundary flush would change an immediate consumer's visible operand. */ + if (s_ld_pend_armed) return 0; #ifdef PSX_HAS_GAME_DISPATCH uint32_t phys = pc & 0x1FFFFFFFu; if (psx_game_address_in_text(pc) && !dirty_ram_is_dirty(phys)) @@ -2573,10 +2755,18 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) { g_slice_exit_in_text = 0; #endif g_slice_exit_want = 0; - int irq_taken = 0; /* one take per slice (avoid re-taking an unacked IRQ) */ + int irq_taken = 0; /* default profile limits takes; also requests safe exit */ enum { MAX_PRECISE_INSNS = 200000 }; - for (int i = 0; i < MAX_PRECISE_INSNS; i++) { - if (!irq_taken && precise_irq_deliverable(cpu)) { + /* A host instruction budget cannot retire a pending guest load or create a + * generated entry. In the source profile retain ownership until the safe + * exit below, even when a branch-slot load spans every lap of a long loop. + * Each instruction still advances devices and the normal frontend hooks. + * Keep the legacy guard outside this explicit profile. Saturate the + * diagnostic iteration count rather than overflowing on a guest spin. */ + const int source_owned_slice = source_gpu_runtime_active(); + for (uint32_t i = 0; source_owned_slice || i < MAX_PRECISE_INSNS; + i += i != UINT32_MAX) { + if ((source_owned_slice || !irq_taken) && precise_irq_before(cpu,pc)) { uint32_t committed = pc; extern uint32_t i_stat; g_slice_last_committed = committed; @@ -2588,6 +2778,7 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) { g_cosim_dirty_pump_site = 7; g_dirty_safe_resume_pc = committed; s_last_dirty_irq_pump_insns = g_dirty_ram_insns_run; + dirty_ram_ld_delay_flush(cpu); /* accepted IRQ retires the prior load */ psx_check_interrupts(cpu); if (cpu->pc == 0u && committed != 0u) { cpu->pc = committed; @@ -2599,7 +2790,9 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) { irq_taken = 1; pc = cpu->pc ? cpu->pc : committed; cpu->pc = pc; - if (precise_pc_dispatchable(cpu->pc)) { + uint32_t resume_phys = cpu->pc & 0x1FFFFFFFu; + if (precise_pc_dispatchable(cpu->pc) && + !(resume_phys >= 0x1FC00000u && resume_phys < 0x1FC80000u)) { g_slice_exit_reason = 1; g_slice_exit_iter = (uint32_t)i; g_slice_exit_want = 1; @@ -2608,10 +2801,24 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) { continue; } + /* Only RAM and the selected BIOS ROM are valid instruction regions. + * ROM admission belongs to the explicit original-source profile. */ + uint32_t instruction_phys = pc & 0x1FFFFFFFu; + if (instruction_phys >= 0x00200000u && + !(source_gpu_runtime_active() && instruction_phys >= 0x1FC00000u && + instruction_phys < 0x1FC80000u)) { + extern void psx_fatal_halt(const char *); + cpu->pc = pc; + psx_fatal_halt("precise slice requires unsupported non-RAM continuation"); + break; + } uint32_t next_pc = 0; g_unsupported_seen = 0; cosim_exec_one_begin(); + uint64_t slot_takes=g_slice_irq_taken; int transferred = exec_one(cpu, pc, &next_pc); /* charges its own interlock */ + int slot_irq_taken = g_slice_irq_taken!=slot_takes; + if(slot_irq_taken)irq_taken=1; g_dirty_ram_insns_run++; #ifdef PSX_COSIM if (!cosim_exec_one_did_hook()) { extern void cosim_instr(uint32_t); cosim_instr(pc); } @@ -2637,11 +2844,11 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) { * clean game text. */ int want_exit = 0; - /* Exact take-point: an interrupt deliverable on THIS instruction's cycle is - * taken before the next instruction retires — once per slice. Re-checking - * after a take re-fires an IRQ the handler has not acked yet (an - * 8-takes-in-11-insns storm), so gate on !irq_taken. */ - if (!irq_taken && precise_irq_deliverable(cpu)) { + /* Source RFE can re-enable an unacknowledged IRQ while this slice + * still owns a mid-block return target. SR/in_exception determine + * eligibility at every boundary; a previous take cannot permit one + * extra opcode before the next IRQ. Retain the default take limit. */ + if ((source_owned_slice || !irq_taken) && precise_irq_before(cpu,committed)) { extern uint32_t i_stat; g_slice_last_committed = committed; g_slice_last_istat = i_stat; @@ -2652,6 +2859,7 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) { g_cosim_dirty_pump_site = 7; g_dirty_safe_resume_pc = committed; /* real EPC for exception entry */ s_last_dirty_irq_pump_insns = g_dirty_ram_insns_run; + dirty_ram_ld_delay_flush(cpu); /* before the handler's saved-GPR snapshot */ psx_check_interrupts(cpu); /* takes it; runs handler; restores GPRs */ g_dirty_safe_resume_pc = 0; g_cosim_dirty_pump_site = prev_site; @@ -2680,7 +2888,13 @@ static void psx_run_precise(CPUState *cpu, uint32_t bcyc, int deadline_entry) { /* Hand back ONLY at a dispatchable PC. Otherwise (mid-function clean text) * keep interpreting until one is reached. */ - if (want_exit && precise_pc_dispatchable(cpu->pc)) { + /* A ROM exception may return to a branch opcode inside a generated + * block. Continue through that real branch before handing back; its + * interrupted delay slot is not a completed control transfer. */ + uint32_t resume_phys = cpu->pc & 0x1FFFFFFFu; + int rom_resume = resume_phys >= 0x1FC00000u && resume_phys < 0x1FC80000u; + if (want_exit && precise_pc_dispatchable(cpu->pc) && + (!rom_resume || (transferred && !slot_irq_taken))) { g_slice_exit_reason = 1; g_slice_exit_iter = (uint32_t)i; g_slice_exit_want = 1; @@ -2728,6 +2942,10 @@ int psx_slice_block_impl(CPUState *cpu, uint32_t block_addr, uint32_t bcyc, int * drift / faithful per-instruction cycle model — see CLAUDE.md Rule -1). */ if (!g_psx_precise_slice) return 0; + uint32_t block_phys = block_addr & 0x1FFFFFFFu; + if (block_phys >= 0x1FC00000u && block_phys < 0x1FC80000u && + !source_gpu_runtime_active()) return 0; + /* No nested slicing: a handler dispatched from inside precise-mode, and any * block executed while in_exception, run compiled (interrupts are gated during * exception handling anyway). Keeps re-entrancy structurally impossible. */ @@ -2745,12 +2963,37 @@ int psx_slice_block_impl(CPUState *cpu, uint32_t block_addr, uint32_t bcyc, int if (s_slice_margin < 0) s_slice_margin = 0; } + /* Relative device countdowns describe their last serviced clock. The + * source CPU may have advanced since then, including deferred block + * charges. Bring devices to the entry clock before deciding that an + * entire compiled block can safely run past the next IRQ boundary. */ + if(source_gpu_runtime_active()) { + psx_cyc_batch_flush(); + psx_devices_service_to_now(); + } uint32_t deadline = cycles_to_next_event(); - uint32_t budget = bcyc + (uint32_t)s_slice_margin; - if (budget < bcyc) budget = 0xFFFFFFFFu; + uint32_t block_bound=bcyc; + if(!side_effects && source_gpu_runtime_active() && (block_addr&0x1fffffffu)<0x200000u) + block_bound=source_cpu_block_bound(cpu,block_addr,bcyc,deadline); + uint32_t budget = block_bound + (uint32_t)s_slice_margin; + if (budget < block_bound) budget = 0xFFFFFFFFu; int entry_deliverable = precise_irq_deliverable(cpu); - int has_deadline = s_slice_always || entry_deliverable || (deadline <= budget); - if (!has_deadline && !side_effects) return 0; /* fast path: no event in this block */ + /* A DMA halt can consume the entire deadline before the block's first + * opcode. Retain instruction ownership through that wait, even when the + * block itself is shorter than the currently pending event countdown. */ + extern int dma_cpu_source_halted(void); + int source_halted=source_gpu_runtime_active() && dma_cpu_source_halted(); + int has_deadline = s_slice_always || source_halted || entry_deliverable || (deadline <= budget); + g_slice_probe_pc=block_addr;g_slice_probe_deadline=deadline; + g_slice_probe_bound=budget;g_slice_probe_cycle=psx_get_cycle_count(); + /* A distant device event does not prove value-pipeline safety. Existing + * generated COP2 reads write eagerly, including across a guest return. + * Use the same precise owner for those RAM blocks; it retains the delayed + * value through the consumer before returning to compiled execution. */ + int gte_value_delay = !has_deadline && !side_effects && + source_gpu_runtime_active() && (cpu->cop0[12]&0x40000000u) && + block_phys<0x200000u && source_cpu_block_gte_value_delay(block_addr,bcyc); + if (!has_deadline && !side_effects && !gte_value_delay) return 0; g_slice_entry_deliverable = (uint32_t)entry_deliverable; g_slice_fired++; @@ -3062,7 +3305,8 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ uint32_t before_s3 = cpu->gpr[19]; #endif cosim_exec_one_begin(); - int transferred = exec_one_fetched(cpu, pc, insn, &next_pc); + int transferred = source_dirty_irq_before(cpu,pc) ? 1 : + exec_one_fetched(cpu, pc, insn, &next_pc); #ifndef PSX_NO_DEBUG_TOOLS /* Armed iff the window is NON-EMPTY, same as the callret ring: a `lo` * test made address 0 a silent off switch, so lo=0 with a real hi @@ -3239,6 +3483,14 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ } #endif uint32_t target_phys = target & 0x1FFFFFFFu; + if(s_ld_pend_armed && target!=0u && target!=stop_addr && + target_phys<0x10000u && dirty_ram_is_dirty(target_phys)) { + /* A host dispatcher return is not a guest pipeline boundary. + * Preserve a load in a branch delay slot through the next + * instruction when its low-RAM kernel owner is available. */ + pc=target;current_page=target_phys>>12;current_page_dirty=1; + continue; + } if (allow_local_dirty_flow && target != 0 && target != stop_addr && phys_is_overlay_flow_region(target_phys) && diff --git a/runtime/src/dma.c b/runtime/src/dma.c index 2bb30e50f..51fb392dc 100644 --- a/runtime/src/dma.c +++ b/runtime/src/dma.c @@ -13,6 +13,7 @@ */ #include "dma.h" +#include "source_gpu_runtime.h" #include "cdrom.h" #include "crash_trace.h" #include "dirty_ram_interp.h" @@ -23,6 +24,7 @@ #include "spu.h" #include "audio_trace.h" #include "event_ring.h" +#include "psx_cycles.h" #include #include #include @@ -62,6 +64,47 @@ typedef struct { static DMAAsyncChannel mdec_async[2]; static DMAAsyncChannel cdrom_async; +/* Optional source-core experiment. This deliberately models Octoshock 2.2.2 + * OTC service granularity and whole-CPU waiting, not PS1 bus arbitration. + * Facts consulted: original dma.cpp RunChannelI/DMA_Update/DMA_Write. + * No reference implementation text is incorporated. */ +static int otc_source_model; +static struct { + uint32_t remaining, address; + uint64_t last_cycle, next_cycle; +} otc_source; +/* Optional manual CD DMA timing only. Register conventions remain native. + * Source facts: 64 initial clocks, 9 clocks/word, 128-clock service and CPU + * halt. These are original-core compatibility rules, not bus measurements. */ +static int cd_source_model; +static struct { + int32_t budget; + uint64_t last_cycle, next_cycle; +} cd_source; +/* Optional source request timing for an already active VRAM upload only. */ +static int gpu_upload_source_model; +static int gpu_ll_source_model; +static struct { + uint32_t active, address, remaining, nodes; + int32_t budget; + uint64_t last_cycle, next_cycle; +} gpu_ll_source; +uint32_t g_dma_cpu_read_wait; +static uint32_t gpu_upload_live_read_wait; +static int cpu_read_wait_latched; +static struct { + uint32_t remaining, block_size, in_block, address; + int32_t budget; + uint64_t last_cycle, next_cycle; +} gpu_upload_source; +/* Source SPU request DMA:64 initial clocks,48 per word, global128-clock + * updates. Payload and register progress belong to those service calls. */ +static struct { + uint32_t remaining, block_size, in_block, address, total_words, start_addr; + int32_t budget; + uint64_t last_cycle, next_cycle; +} spu_source; + /* ---- CD DMA transfer log ---- */ /* Every forward CH3 DMA that lands below 0x1C0000 (game data region) records * (setloc_lba, dest_addr, size). Transfers to 0x1C0000+ are FMV/streaming @@ -321,6 +364,7 @@ static uint32_t transfer_word_count(int ch) { uint32_t block_count = (channels[ch].bcr >> 16) & 0xFFFFu; if (ch == 3 && block_size == 0) { + if (cd_source_model) return 0x10000u; return cdrom_dma_sector_word_count(); } @@ -362,7 +406,10 @@ static void complete_transfer(int ch) { uint32_t dicr_before = dicr; uint32_t i_stat_before = i_stat; channels[ch].chcr &= ~((1u << 24) | (1u << 28)); - if (channel_irq_flag_armed(ch)) { + /* Source channel4 latches its enabled completion flag even when the + * master IRQ output is disabled; the master gate belongs to IRQ output. */ + if ((ch==4 && source_gpu_runtime_active()) ? + ((dicr>>(16+ch))&1u) : channel_irq_flag_armed(ch)) { dicr |= (1u << (24 + ch)); raise_dma_irq_on_master_edge(dicr_before); } @@ -410,6 +457,11 @@ static void schedule_delayed_complete(int ch, uint32_t total_words, delayed_complete[ch].total_words = total_words; delayed_complete[ch].cycles_remaining = (uint32_t)cycles; event_ring_record_aux(EV_DMA_SCHED, (uint8_t)ch, channels[ch].chcr); + /* The MMIO pre-barrier cached deadlines before this channel was armed. + * Refresh through the scheduler owner so a later cached event cannot hide + * this completion. This publishes no new CPU cycles; nested service is + * guarded and its outer pass recomputes the deadline. */ + psx_devices_service_to_now(); } static void advance_delayed_complete(int ch, uint32_t cycles) { @@ -653,6 +705,185 @@ static void execute_ch1_mdec_out(void) { complete_transfer(1); } +static void source_gpu_upload_words(void) { + int queued=source_gpu_runtime_active(); + unsigned to_gpu=channels[2].chcr&1u; + g_dma_cur_ch=2;g_dma_initiator_pc=s_dma_ch_initiator_pc[2]; + g_dma_cur_madr=channels[2].madr;g_dma_cur_bcr=channels[2].bcr; + while(gpu_upload_source.remaining && gpu_upload_source.budget>0) { + if(!queued && gpu_dma_vram_upload_words()0)gpu_upload_source.budget=0; + uint32_t n=gpu_upload_source.block_size?gpu_upload_source.block_size-1u:0u; + int can= !queued || !to_gpu || gpu_dma_source_ll_ready()==1; + gpu_upload_live_read_wait=gpu_upload_source.remaining && can?(n>200u?200u:n):0u; + if(!cpu_read_wait_latched)g_dma_cpu_read_wait=gpu_upload_live_read_wait; +} +static void start_source_gpu_upload(void) { + uint32_t bs=channels[2].bcr&0xffffu,bc=channels[2].bcr>>16; + uint64_t total=(uint64_t)bs*bc; + unsigned mode=channels[2].chcr&0x00ffffffu; + int queued=source_gpu_runtime_active(); + if((queued?(mode&~3u)!=0x200u:mode!=0x201u) || !bs || !bc || + (channels[2].madr&0x800000u) || (!queued && total>gpu_dma_vram_upload_words())) { + fprintf(stderr,"[dma-model] unsupported source GPU request: CHCR=%08X BCR=%08X MADR=%08X upload=%u\n",channels[2].chcr,channels[2].bcr,channels[2].madr,gpu_dma_vram_upload_words());exit(2); + } + gpu_upload_source.remaining=(uint32_t)total; + gpu_upload_source.block_size=bs;gpu_upload_source.in_block=0; + gpu_upload_source.budget=64; + gpu_upload_source.last_cycle=psx_cycle_count; + gpu_upload_source.next_cycle=psx_cycle_count+128u-(psx_cycle_count&127u); + source_gpu_upload_words(); +} +static void advance_source_gpu(void) { + while(gpu_upload_source.remaining && psx_cycle_count>=gpu_upload_source.next_cycle) { + gpu_upload_source.budget+=(int32_t)(gpu_upload_source.next_cycle-gpu_upload_source.last_cycle); + gpu_upload_source.last_cycle=gpu_upload_source.next_cycle; + gpu_upload_source.next_cycle+=128; + source_gpu_upload_words(); + } +} +/* Bounded source compatibility experiment. Independently implemented from + * observed original-core facts: 64 startup clocks, 15/10 per header, one per + * payload, ready checked at a node boundary, 128-clock subsequent service. + * Only NOP/environment payloads and the qualified parser-ready states are + * admitted. General draw/FIFO timing, forced stop and restore are excluded. */ +static void source_gpu_ll_words(void) { + g_dma_cur_ch=2;g_dma_initiator_pc=s_dma_ch_initiator_pc[2]; + while(gpu_ll_source.active && gpu_ll_source.budget>0) { + if(!gpu_ll_source.remaining) { + int ready=gpu_dma_source_ll_ready(); + if(ready<0) { + fprintf(stderr,"[dma-model] unsupported source linked-list GPU parser state\n");exit(2); + } + if(!ready)break; + if(channels[2].madr&0x800000u) { + fprintf(stderr,"[dma-model] unsupported source linked-list address %08X\n",channels[2].madr);exit(2); + } + uint32_t at=channels[2].madr&0x1ffffcu; + uint32_t header=psx_read_word(at); + gpu_ll_source.remaining=header>>24; + gpu_ll_source.address=(channels[2].madr+4u)&0xffffffu; + channels[2].madr=header&0xffffffu; + gpu_set_gp0_linked_list_node(at,gpu_ll_source.nodes++); + gpu_ll_source.budget-=gpu_ll_source.remaining?15:10; + } else { + if(gpu_ll_source.address&0x800000u) { + fprintf(stderr,"[dma-model] unsupported source linked-list payload address\n");exit(2); + } + uint32_t at=gpu_ll_source.address&0x1ffffcu; + uint32_t value=psx_read_word(at),opcode=value>>24; + if(!source_gpu_runtime_active() && (gpu_dma_source_ll_ready()!=1 || + !(opcode==0 || opcode==1 || (opcode>=0xe1 && opcode<=0xe6)))) { + fprintf(stderr,"[dma-model] unsupported source linked-list payload %08X at %08X\n",value,at);exit(2); + } + gpu_set_gp0_source(at);gpu_write_gp0(value); + gpu_ll_source.address=(gpu_ll_source.address+4u)&0xffffffu; + gpu_ll_source.remaining--;gpu_ll_source.budget--; + } + if(!gpu_ll_source.remaining && channels[2].madr==0xffffffu) { + gpu_ll_source.active=0;complete_transfer(2); + } + } + /* Source discards unused positive clocks at a ready stall/completion. */ + if(gpu_ll_source.budget>0)gpu_ll_source.budget=0; +} +static void start_source_gpu_ll(void) { + if((channels[2].chcr&0x00ffffffu)!=0x401u) { + fprintf(stderr,"[dma-model] unsupported source linked-list CHCR=%08X\n",channels[2].chcr);exit(2); + } + memset(&gpu_ll_source,0,sizeof(gpu_ll_source)); + gpu_ll_source.active=1;gpu_ll_source.budget=64; + gpu_ll_source.last_cycle=psx_cycle_count; + gpu_ll_source.next_cycle=psx_cycle_count+128u-(psx_cycle_count&127u); + source_gpu_ll_words(); + psx_devices_service_to_now(); +} +static void advance_source_gpu_ll(void) { + while(gpu_ll_source.active && psx_cycle_count>=gpu_ll_source.next_cycle) { + gpu_ll_source.budget+=(int32_t)(gpu_ll_source.next_cycle-gpu_ll_source.last_cycle); + gpu_ll_source.last_cycle=gpu_ll_source.next_cycle; + gpu_ll_source.next_cycle+=128u;source_gpu_ll_words(); + } +} +static void advance_source_cdrom(void); +static void source_cdrom_words(void); +static void advance_source_otc(void); +static void advance_source_otc_words(uint32_t budget); +static void advance_source_spu(void); +static void source_spu_words(void); +void dma_source_gpu_service_at(uint64_t cycle) { + if(cycle!=psx_cycle_count) { + fprintf(stderr,"[dma-model] source GPU service outside scheduler cycle\n");exit(2); + } + g_dma_exec_depth++; + advance_source_gpu(); + if(gpu_upload_source.remaining && cycle>gpu_upload_source.last_cycle) { + gpu_upload_source.budget+=(int32_t)(cycle-gpu_upload_source.last_cycle); + gpu_upload_source.last_cycle=cycle;source_gpu_upload_words(); + } + advance_source_gpu_ll(); + if(gpu_ll_source.active && cycle>gpu_ll_source.last_cycle) { + gpu_ll_source.budget+=(int32_t)(cycle-gpu_ll_source.last_cycle); + gpu_ll_source.last_cycle=cycle;source_gpu_ll_words(); + } + /* Original DMA_Update services every channel on register writes and + * frontend returns, including the fraction since the last 128-clock + * event. Preserve that global event phase and charge each interval once. */ + advance_source_cdrom(); + if(cd_source_model && cdrom_async.active && cycle>cd_source.last_cycle) { + cd_source.budget+=(int32_t)(cycle-cd_source.last_cycle); + cd_source.last_cycle=cycle;source_cdrom_words(); + } + advance_source_spu(); + if(spu_source.remaining && cycle>spu_source.last_cycle) { + spu_source.budget+=(int32_t)(cycle-spu_source.last_cycle); + spu_source.last_cycle=cycle;source_spu_words(); + } + advance_source_otc(); + if(otc_source_model && otc_source.remaining && cycle>otc_source.last_cycle) { + uint32_t elapsed=(uint32_t)(cycle-otc_source.last_cycle); + otc_source.last_cycle=cycle;advance_source_otc_words(elapsed); + } + g_dma_exec_depth--; +} +uint32_t dma_cpu_read_penalty(void) { + return g_dma_cpu_read_wait; +} +void dma_cpu_read_wait_boundary(void) { + /* Source RunReal does not service an event crossed by fetch/base cycles + * until the instruction's memory access (MMIO) or retirement (RAM/ROM). + * Its ReadMemory therefore sees the DMA steal from instruction entry. + * Our scheduler may already have completed DMA during the fetch refill; + * preserve that entry wait, including for compiled RAM fast-path routing. */ + cpu_read_wait_latched=1; + g_dma_cpu_read_wait=gpu_upload_live_read_wait; +} + static uint32_t execute_ch2_gpu(void) { uint32_t chcr = channels[2].chcr; uint32_t direction = chcr & 1; /* 0=to RAM, 1=from RAM (to device) */ @@ -822,16 +1053,74 @@ static uint32_t execute_ch2_gpu(void) { return actual_words; } +static void source_cdrom_words(void) { + DMAAsyncChannel *a=&cdrom_async; + uint32_t addr=channels[3].madr&0x1FFFFCu; + int32_t step=(channels[3].chcr&2u)?-4:4; + if(a->remaining_words==a->total_words) { + uint32_t bytes=a->total_words*4u; + if(bytes>0x200000u-addr)bytes=0x200000u-addr; + if(addr<0x1C0000u)overlay_capture_before_dma(addr,bytes); + } + while(a->active && a->remaining_words && cd_source.budget>0) { + uint32_t word=cdrom_dma_read_padded(); + g_dma_cur_ch=3;g_dma_cur_madr=addr;g_dma_cur_bcr=channels[3].bcr; + g_dma_initiator_pc=s_dma_ch_initiator_pc[3]; + psx_write_word(addr,word);record_cdrom_dma_word(word); + dirty_ram_mark_executable_range(addr,4); + addr=(addr+step)&0x1FFFFCu; + a->remaining_words--;cd_source.budget-=9; + channels[3].madr=addr; + if(!a->remaining_words)finish_async_cdrom_transfer(addr); + } +} + +static void start_source_cdrom(void) { + cd_source.budget=64;cd_source.last_cycle=psx_cycle_count; + cd_source.next_cycle=psx_cycle_count+128u-(psx_cycle_count&127u); + source_cdrom_words(); +} + +static void advance_source_cdrom(void) { + while(cd_source_model && cdrom_async.active && psx_cycle_count>=cd_source.next_cycle) { + cd_source.budget+=(int32_t)(cd_source.next_cycle-cd_source.last_cycle); + cd_source.last_cycle=cd_source.next_cycle;cd_source.next_cycle+=128u; + source_cdrom_words(); + } +} + static void execute_ch3_cdrom(void) { uint32_t chcr = channels[3].chcr; uint32_t direction = chcr & 1; /* 0=to RAM, 1=from RAM */ + if(cd_source_model && ((chcr&0x601u)!=0 || (channels[3].madr&0x800000u))) { + fprintf(stderr,"[dma-model] source CD timing requires valid manual to-RAM transfer: frame=%llu cycle=%llu CHCR=%08X MADR=%08X BCR=%08X\n", + (unsigned long long)s_frame_count,(unsigned long long)psx_cycle_count, + chcr,channels[3].madr,channels[3].bcr); + exit(2); + } if (direction != 0) { channels[3].chcr &= ~((1u << 24) | (1u << 28)); return; } start_async_cdrom_transfer(); + if(cd_source_model) { + start_source_cdrom(); + /* Source's CPU owns fetch attempts during a manual DMA halt, just as + * for OTC. Let the CHCR store retire before that boundary runs. + * Standalone controller callers retain their synchronous contract. */ + if(source_gpu_runtime_active())return; + while(cdrom_async.active && !(chcr&0x100u)) { + uint32_t wait=cd_source.next_cycle>psx_cycle_count? + (uint32_t)(cd_source.next_cycle-psx_cycle_count):1u; + psx_advance_cycles(wait); + /* The MMIO pre-barrier can cache a later device deadline before + * this transfer is armed. Service each new DMA boundary even when + * that cached deadline has not expired, as the OTC halt does. */ + psx_devices_service_to_now(); + } + } } /* Returns the number of words moved so the caller can schedule a faithful @@ -875,7 +1164,112 @@ static uint32_t execute_ch4_spu(void) { return total_words; } +static void source_spu_words(void) { + g_dma_cur_ch=4;g_dma_initiator_pc=s_dma_ch_initiator_pc[4]; + while(spu_source.remaining && spu_source.budget>0) { + if(!spu_source.in_block) { + spu_source.in_block=spu_source.block_size; + spu_source.address=channels[4].madr&0xffffffu; + channels[4].bcr-=1u<<16; + } + if(spu_source.address&0x800000u) { + fprintf(stderr,"[dma-model] source SPU request payload address unsupported\n");exit(2); + } + uint32_t at=spu_source.address&0x1ffffcu; + g_dma_cur_madr=spu_source.address;g_dma_cur_bcr=channels[4].bcr; + if(channels[4].chcr&1u)spu_dma_write(psx_read_word(at)); + else psx_write_word(at,spu_dma_read()); + spu_source.address=(spu_source.address+((channels[4].chcr&2u)?-4u:4u))&0xffffffu; + spu_source.budget-=48;spu_source.in_block--;spu_source.remaining--; + if(!spu_source.in_block)channels[4].madr=spu_source.address; + if(!spu_source.remaining) { + audio_trace_event((channels[4].chcr&1u)?AUDIO_EV_DMA_WRITE:AUDIO_EV_DMA_READ, + spu_source.total_words,spu_source.start_addr); + complete_transfer(4); + } + } + if(spu_source.budget>0)spu_source.budget=0; +} +static void start_source_spu(void) { + uint32_t bs=channels[4].bcr&0xffffu,bc=channels[4].bcr>>16; + if((channels[4].chcr&0x00fffffcu)!=0x200u || !bs || !bc || + (channels[4].madr&0x800000u)) { + fprintf(stderr,"[dma-model] source SPU request unsupported CHCR=%08X BCR=%08X MADR=%08X\n", + channels[4].chcr,channels[4].bcr,channels[4].madr);exit(2); + } + spu_source.remaining=spu_source.total_words=bs*bc; + spu_source.block_size=bs;spu_source.in_block=0;spu_source.budget=64; + spu_source.start_addr=channels[4].madr&0x1ffffcu; + spu_source.last_cycle=psx_cycle_count; + spu_source.next_cycle=psx_cycle_count+128u-(psx_cycle_count&127u); + source_spu_words(); +} +static void advance_source_spu(void) { + while(spu_source.remaining && psx_cycle_count>=spu_source.next_cycle) { + spu_source.budget+=(int32_t)(spu_source.next_cycle-spu_source.last_cycle); + spu_source.last_cycle=spu_source.next_cycle;spu_source.next_cycle+=128; + source_spu_words(); + } +} + +static void advance_source_otc_words(uint32_t budget) { + while (budget-- && otc_source.remaining) { + if (otc_source.address & 0x800000u) { + otc_source.remaining = 0; + channels[6].chcr &= ~((1u << 24) | (1u << 28)); + uint32_t before = dicr; + dicr |= 0x8000u; + raise_dma_irq_on_master_edge(before); + return; + } + uint32_t value = otc_source.remaining == 1u ? 0xFFFFFFu : + (otc_source.address - 4u) & 0xFFFFFFu; + g_dma_cur_ch = 6; g_dma_cur_madr = otc_source.address; + g_dma_cur_bcr = channels[6].bcr; + g_dma_initiator_pc = s_dma_ch_initiator_pc[6]; + psx_write_word(otc_source.address & 0x1FFFFCu, value); + otc_source.address = (otc_source.address - 4u) & 0xFFFFFFu; + if (--otc_source.remaining == 0) complete_transfer(6); + } +} + +static void start_source_otc(void) { + otc_source.remaining = channels[6].bcr & 0xFFFFu; + if (!otc_source.remaining) otc_source.remaining = 65536u; + otc_source.address = channels[6].madr & 0xFFFFFCu; + otc_source.last_cycle = psx_cycle_count; + otc_source.next_cycle = psx_cycle_count + 128u - (psx_cycle_count & 127u); + advance_source_otc_words(64u); +} + +static void advance_source_otc(void) { + while (otc_source.remaining && psx_cycle_count >= otc_source.next_cycle) { + uint32_t elapsed = (uint32_t)(otc_source.next_cycle - otc_source.last_cycle); + otc_source.last_cycle = otc_source.next_cycle; + otc_source.next_cycle += 128u; + advance_source_otc_words(elapsed); + } +} + static void execute_ch6_otc(void) { + if (otc_source_model) { + start_source_otc(); + /* The source CPU fetches while halted. Its functional instruction + * boundary owns that overlap when the source CPU/GPU profile is on. + * Standalone controller callers retain the synchronous contract. */ + if(source_gpu_runtime_active())return; + /* The ordinary CHCR store does not return to guest execution until + * the source-style halt ends. All devices keep advancing. Source's + * next instruction fetch may overlap this halt; this adapter does + * not claim fetch/cache phase or physical write-queue equivalence. */ + while (otc_source.remaining) { + uint32_t wait = otc_source.next_cycle > psx_cycle_count ? + (uint32_t)(otc_source.next_cycle - psx_cycle_count) : 1u; + psx_advance_cycles(wait); + psx_devices_service_to_now(); + } + return; + } /* OTC (Ordering Table Clear): writes a backward-linked list to RAM. * Node N = address of node N-1, node 0 = 0xFFFFFF (end marker). * Direction is always to-RAM, step is always backward. @@ -900,6 +1294,16 @@ static void execute_ch6_otc(void) { complete_transfer(6); } +int dma_cpu_otc_halted(void) { + return otc_source_model && otc_source.remaining!=0u; +} + +int dma_cpu_source_halted(void) { + return dma_cpu_otc_halted() || + (cd_source_model && cdrom_async.active && + !(channels[3].chcr&0x100u)); +} + static void execute_ch5_pio(void) { /* PIO (Parallel I/O) — used for expansion port / parallel port transfers. * Very simple: just move words directly to/from RAM with no device interaction. @@ -937,7 +1341,11 @@ static void try_execute(int ch) { if (!((chcr >> 24) & 1)) return; if (!channel_enabled(ch)) return; - channels[ch].chcr &= ~(1u << 28); + /* Source CD and SPU retain the trigger in their live registers until + * completion, observable while the CPU is allowed to keep running. + * Default and standalone controller callers retain their old image. */ + if(!((ch==4 || (ch==3 && cd_source_model)) && source_gpu_runtime_active())) + channels[ch].chcr &= ~(1u << 28); trace_dma('S', ch, transfer_word_count(ch), dicr, i_stat); event_ring_record_aux(EV_DMA_KICK, (uint8_t)ch, channels[ch].chcr); event_ring_record_aux(EV_ENQ, (uint8_t)(SRC_DMA0 + ch), transfer_word_count(ch)); @@ -959,15 +1367,20 @@ static void try_execute(int ch) { start_async_mdec_transfer(1); break; case 2: - schedule_delayed_complete(2, execute_ch2_gpu(), - DMA_GPU_CYCLES_PER_WORD); + if(gpu_upload_source_model && ((channels[2].chcr>>9)&3u)==1u) + start_source_gpu_upload(); + else if(gpu_ll_source_model && ((channels[2].chcr>>9)&3u)==2u) + start_source_gpu_ll(); + else schedule_delayed_complete(2, execute_ch2_gpu(), + DMA_GPU_CYCLES_PER_WORD); break; case 3: execute_ch3_cdrom(); break; case 4: - schedule_delayed_complete(4, execute_ch4_spu(), - DMA_SPU_CYCLES_PER_WORD); + if(source_gpu_runtime_active())start_source_spu(); + else schedule_delayed_complete(4, execute_ch4_spu(), + DMA_SPU_CYCLES_PER_WORD); break; case 5: execute_ch5_pio(); @@ -1005,9 +1418,31 @@ int dma_cdrom_transfer_active(void) { * already accumulated (always <= the true remaining, since per-word cost >= 1), * and the exact countdown for delayed-complete channels. Over-slicing on a * channel whose DICR completion is masked is safe. See PRECISE_IRQ_SLICE.md. */ +static uint32_t source_gpu_dma_irq_bound(void) { + uint32_t best=0xFFFFFFFFu; + if(gpu_ll_source.active) + best=gpu_ll_source.next_cycle>psx_cycle_count? + (uint32_t)(gpu_ll_source.next_cycle-psx_cycle_count):1u; + if(gpu_upload_source.remaining) { + uint32_t d=gpu_upload_source.next_cycle>psx_cycle_count? + (uint32_t)(gpu_upload_source.next_cycle-psx_cycle_count):1u; + if(dpsx_cycle_count? + (uint32_t)(spu_source.next_cycle-psx_cycle_count):1u; + if(dpsx_cycle_count? + (uint32_t)(gpu_ll_source.next_cycle-psx_cycle_count):1u; + } + if(gpu_upload_source.remaining) { + best=gpu_upload_source.next_cycle>psx_cycle_count? + (uint32_t)(gpu_upload_source.next_cycle-psx_cycle_count):1u; + } + if (otc_source.remaining) { + uint32_t d = otc_source.next_cycle > psx_cycle_count ? + (uint32_t)(otc_source.next_cycle - psx_cycle_count) : 1u; + if(dpsx_cycle_count? + (uint32_t)(cd_source.next_cycle-psx_cycle_count):1u; + if(dpsx_cycle_count? + (uint32_t)(spu_source.next_cycle-psx_cycle_count):1u; + if(d> 24) & 1u) && channel_enabled(3)) { if ((channels[3].chcr & 1u) != 0) { return 1u; /* unsupported RAM->CD direction cancels next tick */ @@ -1073,7 +1531,12 @@ uint32_t dma_cycles_to_internal_event(void) { uint32_t dma_cycles_to_deliverable_irq(uint32_t i_mask) { if (!(i_mask & (1u << 3))) return 0xFFFFFFFFu; - uint32_t best = 0xFFFFFFFFu; + uint32_t best = channel_irq_flag_armed(2)?source_gpu_dma_irq_bound():0xFFFFFFFFu; + if(spu_source.remaining && channel_irq_flag_armed(4)) { + uint32_t d=spu_source.next_cycle>psx_cycle_count? + (uint32_t)(spu_source.next_cycle-psx_cycle_count):1u; + if(d> 1) & 1u; @@ -1161,11 +1629,38 @@ void dma_advance(uint32_t cycles) { } void dma_init(void) { + g_dma_cpu_read_wait=0; + gpu_upload_live_read_wait=0;cpu_read_wait_latched=0; + memset(&gpu_ll_source,0,sizeof(gpu_ll_source)); + memset(&gpu_upload_source,0,sizeof(gpu_upload_source)); + memset(&spu_source,0,sizeof(spu_source)); + const char *gpu_model=getenv("PSX_GPU_DMA_MODEL"); + gpu_upload_source_model=gpu_model && *gpu_model; + gpu_ll_source_model=gpu_model && (!strcmp(gpu_model,"octoshock-2.2.2-bounded-linked-list") || !strcmp(gpu_model,"octoshock-2.2.2-bounded-quad")); + if(gpu_upload_source_model && ((!gpu_ll_source_model && strcmp(gpu_model,"octoshock-2.2.2-vram-upload")) || !getenv("PSX_INPUT_ROUTE_FILE"))) { + fprintf(stderr,"[dma-model] invalid source GPU upload model or missing route\n");exit(2); + } + memset(&otc_source, 0, sizeof(otc_source)); + memset(&cd_source,0,sizeof(cd_source)); + const char *cd_model=getenv("PSX_CD_DMA_MODEL"); + cd_source_model=cd_model && *cd_model; + if(cd_source_model && (strcmp(cd_model,"octoshock-2.2.2") || !getenv("PSX_INPUT_ROUTE_FILE"))) { + fprintf(stderr,"[dma-model] invalid source CD model or missing route\n");exit(2); + } + const char *model = getenv("PSX_INPUT_ROUTE_DMA_MODEL"); + otc_source_model = model && *model; + if (otc_source_model && + (strcmp(model, "octoshock-2.2.2-otc") || !getenv("PSX_INPUT_ROUTE_FILE"))) { + fprintf(stderr, "[dma-model] invalid source OTC model or missing input route\n"); + exit(2); + } memset(channels, 0, sizeof(channels)); memset(mdec_async, 0, sizeof(mdec_async)); memset(&cdrom_async, 0, sizeof(cdrom_async)); memset(delayed_complete, 0, sizeof(delayed_complete)); - dpcr = 0x07654321u; /* default: priorities set, no channels enabled */ + /* Original2.2.2 Power clears DMAControl. Keep that source-profile cold + * value distinct from the default hardware-style priority reset image. */ + dpcr = gpu_ll_source_model ? 0u : 0x07654321u; dicr = 0; dma_debug_clear_trace(); dma_debug_clear_cdrom_history(); @@ -1208,6 +1703,55 @@ uint32_t dma_read(uint32_t addr) { } void dma_write_masked(uint32_t addr, uint32_t val, uint32_t mask) { + source_gpu_runtime_dma_write(); + if(spu_source.remaining && + ((addr>=0x1f8010c0u && addr<=0x1f8010cbu) || + (addr==0x1f8010f0u && ((dpcr^val)&mask&0xf0000u)))) { + fprintf(stderr,"[dma-model] active source SPU request register replacement unsupported\n");exit(2); + } + if(gpu_ll_source.active) { + advance_source_gpu_ll(); + if(gpu_ll_source.active && psx_cycle_count>gpu_ll_source.last_cycle) { + gpu_ll_source.budget+=(int32_t)(psx_cycle_count-gpu_ll_source.last_cycle); + gpu_ll_source.last_cycle=psx_cycle_count;source_gpu_ll_words(); + } + /* Source DMA_Write services elapsed work first. With no payload left, + * clearing start stops before the next header: no GPU FIFO flush, no + * completion IRQ, and no retained positive DMA credit. Other active + * register changes and partial-payload cancellation remain guarded. */ + if(gpu_ll_source.active && !gpu_ll_source.remaining && + addr==0x1f8010a8u && mask==0xffffffffu && + channels[2].chcr==0x01000401u && val==0x00000401u) { + gpu_ll_source.active=0; + gpu_ll_source.budget=0; + } + if(gpu_ll_source.active && + ((addr>=0x1f8010a0u && addr<=0x1f8010abu) || + (addr==0x1f8010f0u && ((dpcr^val)&mask&0xf00u)))) { + SourceGPUCommandProjection state;source_gpu_runtime_copy(0,&state); + fprintf(stderr,"[dma-model] active source linked-list register replacement unsupported at %llu addr=%08X value=%08X mask=%08X pc=%08X MADR=%08X BCR=%08X CHCR=%08X payload=%08X remaining=%u nodes=%u budget=%d last=%llu next=%llu GPU=%d,%u,%u,%u,%08X\n", + (unsigned long long)psx_cycle_count,addr,val,mask,g_debug_last_store_pc, + channels[2].madr,channels[2].bcr,channels[2].chcr,gpu_ll_source.address, + gpu_ll_source.remaining,gpu_ll_source.nodes,gpu_ll_source.budget, + (unsigned long long)gpu_ll_source.last_cycle,(unsigned long long)gpu_ll_source.next_cycle, + state.budget,state.phase,state.count,state.command,state.count?state.queue[0]:0); + exit(2); + } + } + if(gpu_upload_source.remaining) { + /* The source synchronizes elapsed DMA work before changing any DMA + * register. In particular, completion at this timestamp must see the + * previous DICR enables. Reads do not perform this partial service. */ + advance_source_gpu(); + if(gpu_upload_source.remaining && psx_cycle_count>gpu_upload_source.last_cycle) { + gpu_upload_source.budget+=(int32_t)(psx_cycle_count-gpu_upload_source.last_cycle); + gpu_upload_source.last_cycle=psx_cycle_count; + source_gpu_upload_words(); + } + } + if(gpu_upload_source.remaining && addr==0x1f8010f0u && ((dpcr^val)&mask&0xf00u)) { + fprintf(stderr,"[dma-model] active source GPU channel control change unsupported\n");exit(2); + } /* DPCR */ if (addr == 0x1F8010F0u) { dpcr = (dpcr & ~mask) | (val & mask); @@ -1233,6 +1777,9 @@ void dma_write_masked(uint32_t addr, uint32_t val, uint32_t mask) { return; } + if(gpu_upload_source.remaining && addr>=0x1f8010a0u && addr<=0x1f8010abu) { + fprintf(stderr,"[dma-model] source GPU upload register replacement unsupported\n");exit(2); + } /* Per-channel registers */ if (addr >= 0x1F801080u && addr <= 0x1F8010EFu) { uint32_t offset = addr - 0x1F801080u; @@ -1352,6 +1899,22 @@ static int dma_r_delay(PstR *r, DMADelayedComplete *d) { uint32_t dma_snapshot_bytes(void) { return DMA_SNAP_WIRE_BYTES; } void dma_snapshot_write(uint8_t *p) { + if(spu_source.remaining) { + fprintf(stderr,"[dma-model] active source SPU request capture unsupported\n");exit(2); + } + if(gpu_ll_source.active) { + fprintf(stderr,"[dma-model] active source linked-list capture unsupported\n");exit(2); + } + if(gpu_upload_source.remaining) { + fprintf(stderr,"[dma-model] active source GPU upload capture unsupported\n");exit(2); + } + if(cd_source_model && cdrom_async.active) { + fprintf(stderr,"[dma-model] state capture during source CD transfer is unsupported\n");exit(2); + } + if (otc_source.remaining) { + fprintf(stderr, "[dma-model] state capture during experimental OTC transfer is unsupported\n"); + exit(2); + } PstW w; pst_w_init(&w, p, DMA_SNAP_WIRE_BYTES); for (int i = 0; i < 7; i++) { @@ -1369,6 +1932,16 @@ void dma_snapshot_write(uint8_t *p) { } int dma_snapshot_read(const uint8_t *p, uint32_t len) { + if(gpu_upload_source_model) { + fprintf(stderr,"[dma-model] source GPU upload timing requires cold boot\n");return 0; + } + if(cd_source_model) { + fprintf(stderr,"[dma-model] source CD timing requires cold boot\n");return 0; + } + if (otc_source_model) { + fprintf(stderr, "[dma-model] experimental source OTC model requires cold boot\n"); + return 0; + } PstR r; if (len != DMA_SNAP_WIRE_BYTES) return 0; pst_r_init(&r, p, len); diff --git a/runtime/src/event_ring.c b/runtime/src/event_ring.c index 2fb1c3cec..86b868ed0 100644 --- a/runtime/src/event_ring.c +++ b/runtime/src/event_ring.c @@ -94,8 +94,7 @@ static int format_entry(char *out, int cap, const EventEntry *e) { (unsigned)e->detail, e->aux); } -int event_ring_dump_file(const char *path) { - FILE *f = fopen(path ? path : "event_ring.json", "w"); +int event_ring_dump_stream(FILE *f) { if (!f) return -1; uint64_t total = s_seq; uint64_t start = (total > EVENT_RING_CAP) ? (total - EVENT_RING_CAP) : 0; @@ -109,7 +108,14 @@ int event_ring_dump_file(const char *path) { first = 0; count++; } fputs("]\n", f); - fclose(f); + return ferror(f) ? -1 : count; +} + +int event_ring_dump_file(const char *path) { + FILE *f = fopen(path ? path : "event_ring.json", "w"); + if (!f) return -1; + int count = event_ring_dump_stream(f); + if (fclose(f)) return -1; return count; } diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 28026b81e..333a060e4 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -11,6 +11,8 @@ */ #include "gpu.h" +#include "source_gpu_runtime.h" +#include "interrupts.h" #include "pgxp.h" #include "mod_memory.h" #include "gpu_primitive_reject.h" @@ -33,6 +35,8 @@ #include #include +static int gpu_source_dispatch(const SourceGPUCommandDispatch *); + extern uint16_t psx_read_half(uint32_t addr); extern uint8_t psx_read_byte(uint32_t addr); extern uint32_t psx_read_word(uint32_t addr); @@ -2278,12 +2282,17 @@ static int32_t polyline_prev_x, polyline_prev_y; /* previous vertex */ static uint16_t polyline_prev_c; /* shaded polyline: previous color */ static int polyline_semi_trans; /* semi-transparency flag from command word */ static int polyline_has_prev; /* have we seen at least one vertex? */ +/* Read-only admission metadata for the bounded source DMA experiment. It + * never changes parser behavior. Source restore is excluded by dma.c. */ +static unsigned source_ll_polyline_vertices; +static int source_ll_incomplete_terminator; /* VRAM write transfer state (CPU→VRAM, command 0xA0) */ static uint16_t vram_write_x, vram_write_y; /* start coords */ static uint16_t vram_write_w, vram_write_h; /* dimensions */ static uint16_t vram_write_col, vram_write_row; /* current offset */ static uint32_t vram_write_remaining; /* words remaining */ +uint32_t gpu_dma_vram_upload_words(void) { return vram_write_remaining; } /* Stage one complete GP0(A0) transfer so renderer backends receive one bulk * rectangle instead of hundreds of thousands of single-pixel callbacks. The * CPU-visible transfer remains ordered because GP0 accepts no next command @@ -2314,6 +2323,14 @@ static void gp0_commit_cpu_to_vram(void) { /* VRAM read transfer state (VRAM→CPU, command 0xC0) */ static int vram_read_active; +int gpu_dma_source_ll_ready(void) { + int projected=source_gpu_runtime_ready(); + if(projected!=-2)return projected; + if(source_ll_incomplete_terminator || vram_read_active)return -1; + if(gp0_state==GP0_IDLE)return 1; + if(gp0_state==GP0_POLYLINE_MONO && source_ll_polyline_vertices>=2)return 0; + return -1; +} static uint16_t vram_read_x, vram_read_y; static uint16_t vram_read_w, vram_read_h; static uint16_t vram_read_col, vram_read_row; @@ -2629,6 +2646,7 @@ static void gpu_reset_state(int clear_vram) { polyline_prev_c = 0; polyline_semi_trans = 0; polyline_has_prev = 0; + source_ll_polyline_vertices=0;source_ll_incomplete_terminator=0; vram_write_x = vram_write_y = 0; vram_write_w = vram_write_h = 0; vram_write_col = vram_write_row = 0; @@ -2695,6 +2713,8 @@ static void gpu_reset_state(int clear_vram) { void gpu_init(void) { gpu_reset_state(1); + source_gpu_runtime_set_dispatch_sink(gpu_source_dispatch); + gr_source_texture_control(0,0); } /* ---- GPUSTAT read (0x1F801814) ---- */ @@ -2790,6 +2810,15 @@ uint32_t gpu_read_gpustat(void) { /* Bit 31: LCF — drawing even/odd lines in interlace mode */ stat |= (lcf & 1) << 31; + uint32_t raster_bits; + if (interrupts_raster_gpu_status(&raster_bits)) + stat=(stat&~0x80002000u)|raster_bits; + + /* Source-profile busy and feedback bits describe the same queue that + * owns command effects. A status read observes it without servicing it. */ + if(source_gpu_runtime_active()) + stat=(stat&~0x1e000000u)|source_gpu_runtime_status_bits(); + return stat; } @@ -2810,12 +2839,17 @@ uint32_t gpu_read_gpuread(void) { value |= (uint32_t)gr_vram_read((int)rx, (int)ry) << (i * 16); if (++vram_read_col == vram_read_w) { - vram_read_col = 0; - if (++vram_read_row == vram_read_h) { + if(source_gpu_runtime_active()) { + /* Original source reads both halves of the final word. For + * odd extents its extra half uses the next X on the final row. */ + if(vram_read_row+1==vram_read_h)vram_read_active=0; + else {vram_read_row++;vram_read_col=0;} + } else if (++vram_read_row == vram_read_h) { /* Transfer complete */ + vram_read_col = 0; vram_read_active = 0; break; - } + } else vram_read_col=0; } } @@ -2828,7 +2862,8 @@ uint32_t gpu_read_gpuread(void) { c0_capture_slot_fwd = -1; /* transfer complete */ } - gpuread_latch = value; + if(source_gpu_runtime_active())source_gpu_runtime_read(); + else gpuread_latch = value; return value; } @@ -3197,6 +3232,18 @@ int gpu_video_standard_is_pal(void) { return (int)(video_mode & 1u); } +int gpu_display_is_interlaced(void) { + return (int)(vertical_interlace & 1u); +} + +/* Passive private observer: no GPUSTAT read, polling side effect or time pump. */ +void gpu_observer_video_state(uint32_t *out) { + out[0]=(hres1&3u)|((vres&1u)<<2)|((video_mode&1u)<<3)| + ((display_depth&1u)<<4)|((vertical_interlace&1u)<<5)|((hres2&1u)<<6); + out[1]=v_display_y1; out[2]=v_display_y2; + out[3]=(uint32_t)interlace_field; out[4]=(uint32_t)lcf; +} + void gpu_get_display_info(GpuDisplayInfo* out) { out->display_x = display_area_x; out->display_y = display_area_y; @@ -4494,6 +4541,10 @@ static void gp0_exec_vram_to_cpu(void) { uint32_t h = (gp0_cmd_buf[2] >> 16) & 0x1FFu; vram_read_w = (w == 0) ? 0x400 : (uint16_t)w; vram_read_h = (h == 0) ? 0x200 : (uint16_t)h; + if(source_gpu_runtime_active()) { + uint32_t raw_height=(gp0_cmd_buf[2]>>16)&1023u; + vram_read_h=(uint16_t)(raw_height>512 ? raw_height&511u : raw_height); + } /* Record for debug */ if (c0_history_count < C0_HISTORY_CAP) { @@ -4512,7 +4563,7 @@ static void gp0_exec_vram_to_cpu(void) { vram_read_col = 0; vram_read_row = 0; - vram_read_active = 1; + vram_read_active = vram_read_h != 0; } /* Determine how many words a GP0 command requires (header only, not counting @@ -5292,7 +5343,6 @@ uint16_t gpu_vram_peek(int x, int y) { } static void gpu_write_gp0_body(uint32_t val) { - gp0_write_count++; /* State: consuming pixel data for CPU→VRAM transfer */ if (gp0_state == GP0_VRAM_WRITE) { @@ -5345,6 +5395,7 @@ static void gpu_write_gp0_body(uint32_t val) { /* State: mono polyline — each word is a vertex (or terminator) */ if (gp0_state == GP0_POLYLINE_MONO) { if ((val & 0xF000F000u) == 0x50005000u) { + if(source_ll_polyline_vertices<2)source_ll_incomplete_terminator=1; /* Terminator: hardware ends a polyline ONLY when the masked word * matches 0x50005000 (the 0x55555555 terminator) — Beetle * gpu.cpp:1030, psx-spx. The old `(val & 0xF000F000) != 0` test @@ -5359,6 +5410,7 @@ static void gpu_write_gp0_body(uint32_t val) { return; } int32_t x, y; + if(source_ll_polyline_vertices<2)source_ll_polyline_vertices++; parse_vertex(val, &x, &y); x += draw_offset_x; y += draw_offset_y; if (polyline_has_prev && @@ -5448,6 +5500,7 @@ static void gpu_write_gp0_body(uint32_t val) { polyline_has_prev = 0; gr_set_semi_transparency(polyline_semi_trans, (int)semi_transparency); gp0_state = shaded ? GP0_POLYLINE_SHADED : GP0_POLYLINE_MONO; + source_ll_polyline_vertices=0; gp0_draw_count++; /* Record polyline header (variable-length body not captured; * just enough so per-frame stream shows the polyline existed). */ @@ -5475,7 +5528,95 @@ static void gpu_write_gp0_body(uint32_t val) { * rasterization / batching / VRAM transfer work on the emu thread — as its * own phase so it is separable from the guest code that issued the write. * Covers both the MMIO store chokepoint and DMA channel-2 feeds. */ +static int gpu_source_draw_triangle(const uint32_t *words,int second) { + unsigned opcode=words[0]>>24,stride=source_gpu_polygon_stride(opcode); + int x[3],y[3],extra_work=0;uint32_t colors[3];SourceGPUTexture texture={0}; + SourceGPUCommandProjection state;source_gpu_runtime_copy(0,&state); + if((opcode&4) && !second) { + uint32_t page=words[4+!!(opcode&0x10)]>>16; + gr_source_texture_control(3,page); + set_tpage_from_poly((uint16_t)page); + } + texture.page=current_texpage();texture.window=texture_window_value; + texture.clut=words[2]>>16;texture.raw=!!(opcode&1);texture.load_clut=!second; + for(unsigned i=0;i<3;i++) { + unsigned v=i+(second?1:0); + x[i]=source_gpu_command_coord(words[1+stride*v],0)+draw_offset_x; + y[i]=source_gpu_command_coord(words[1+stride*v],16)+draw_offset_y; + colors[i]=words[(opcode&0x10)?stride*v:0]&0xffffffu; + if(opcode&4)texture.uv[i]=words[2+stride*v]&0xffff; + } + gr_set_semi_transparency(!!(opcode&2),(int)semi_transparency); + if(!gr_draw_source_triangle(x,y,colors,!!(opcode&0x10),(int)dither_enabled, + (state.display_mode&0x24)==0x24 && !(state.draw_mode&0x400),state.skip_field, + (opcode&4)?&texture:0,&extra_work)) + psx_fatal_halt("Source GPU triangle requires native software rendering without geometry enhancement"); + return extra_work; +} + +static int gpu_source_draw_block(const uint32_t *words) { + unsigned opcode=words[0]>>24;int extra=0; + SourceGPUCommandProjection state;source_gpu_runtime_copy(0,&state); + SourceGPUBlock block={0};block.words=words;block.draw_mode=state.draw_mode; + block.texture_window=state.texture_window; + block.clip_left=state.clip_x0;block.clip_top=state.clip_y0; + block.clip_right=state.clip_x1;block.clip_bottom=state.clip_y1; + block.x=source_gpu_sprite_origin(words[1],0,state.offset_x); + block.y=source_gpu_sprite_origin(words[1],16,state.offset_y); + block.interlace=(state.display_mode&0x24)==0x24 && !(state.draw_mode&0x400); + block.skip_field=state.skip_field; + if(opcode==0x80)gr_source_texture_control(2,0); + gr_set_semi_transparency(!!(opcode&2),(int)semi_transparency); + if(!gr_draw_source_block(&block,&extra))psx_fatal_halt("Source GPU block requires native software rendering without enhancement"); + return extra; +} + +static int gpu_source_dispatch(const SourceGPUCommandDispatch *event) { + extern int g_exec_phase; + int previous_phase=g_exec_phase,extra_work=0; + g_exec_phase=4; + memcpy(gp0_cmd_buf,event->words,event->count*sizeof(uint32_t)); + gp0_cmd_source_addr=gp0_next_source_addr; + gp0_words_collected=gp0_words_needed=(int)event->count; + if(event->kind==SOURCE_GPU_DISPATCH_UPLOAD_WORD) { + gpu_write_gp0_body(event->words[0]); + } else if(event->kind==SOURCE_GPU_DISPATCH_COMMAND) { + unsigned opcode=event->words[0]>>24; + if(source_gpu_block_supported(opcode)) { + gp0_opcode_count[opcode]++; + if(opcode==2)gp0_fill_count++;else if(opcode==0x80)gp0_copy_count++;else gp0_draw_count++; + gp0_ring_record(gp0_cmd_buf,event->count); + extra_work=gpu_source_draw_block(event->words); + } else if(source_gpu_polygon_supported(opcode)) { + gp0_opcode_count[opcode]++;gp0_draw_count++; + gp0_ring_record(gp0_cmd_buf,event->count); + extra_work=gpu_source_draw_triangle(event->words,0); + } else { + if(opcode==1)gr_source_texture_control(1,0); + if(opcode==0xa0 || opcode==0x80)gr_source_texture_control(2,0); + if(opcode==0xe1)gr_source_texture_control(3,event->words[0]); + gp0_execute_command(); + } + } else if(event->kind==SOURCE_GPU_DISPATCH_QUAD_FIRST) { + /* Dispatch only the first three vertex groups. */ + unsigned opcode=event->words[0]>>24; + gp0_opcode_count[opcode]++;gp0_draw_count++; + extra_work=gpu_source_draw_triangle(event->words,0); + } else if(event->kind==SOURCE_GPU_DISPATCH_QUAD_SECOND) { + /* The completed packet supplies source vertex groups (1,2,3). */ + gp0_ring_record(gp0_cmd_buf,event->count); + extra_work=gpu_source_draw_triangle(event->words,1); + } + g_exec_phase=previous_phase; + return extra_work; +} + void gpu_write_gp0(uint32_t val) { + gp0_write_count++; + if(source_gpu_runtime_active()) { + source_gpu_runtime_gp0(val); + return; + } extern int g_exec_phase; int prev_phase = g_exec_phase; g_exec_phase = 4; @@ -5495,6 +5636,7 @@ static void gp1_reset(void) { static void gp1_reset_command_buffer(void) { /* GP1(01h): Reset command buffer — clears FIFO, aborts current command */ gp0_state = GP0_IDLE; + source_ll_polyline_vertices=0;source_ll_incomplete_terminator=0; gp0_words_collected = 0; gp0_words_needed = 0; vram_write_remaining = 0; @@ -5620,10 +5762,14 @@ static void gp1_get_info(uint32_t val) { } void gpu_write_gp1(uint32_t val) { + source_gpu_runtime_gp1(val); + interrupts_raster_gp1(val); uint32_t cmd = (val >> 24) & 0x3F; switch (cmd) { - case 0x00: gp1_reset(); break; + case 0x00: gp1_reset(); + if(source_gpu_runtime_active())gr_source_texture_control(4,0); + break; case 0x01: gp1_reset_command_buffer(); break; case 0x02: gp1_ack_irq1(); break; case 0x03: gp1_display_enable(val); break; diff --git a/runtime/src/gpu_render.c b/runtime/src/gpu_render.c index c833f491f..c37122bc7 100644 --- a/runtime/src/gpu_render.c +++ b/runtime/src/gpu_render.c @@ -94,6 +94,17 @@ void gr_set_backend(GrBackend backend) { GrBackend gr_backend(void) { return g_effective; } /* ---- Dispatch wrappers (one line each; forward to the active backend) ---- */ +int gr_draw_source_block(const SourceGPUBlock *block,int *extra_work) { + return g_b==&SW_BACKEND && sw_draw_source_block(block,extra_work); +} +void gr_source_texture_control(unsigned action,uint32_t page) { + if(g_b==&SW_BACKEND)sw_source_texture_control(action,page); +} +int gr_draw_source_triangle(const int *x,const int *y,const uint32_t *colors, + int shaded,int dither,int interlace,unsigned skip_field, + const SourceGPUTexture *texture,int *extra_work) { + return g_b==&SW_BACKEND && sw_draw_source_triangle(x,y,colors,shaded,dither,interlace,skip_field,texture,extra_work); +} void gr_init(uint16_t *vram) { g_b->init(vram); } void gr_set_scale(int scale) { g_b->set_scale(scale); } int gr_scale(void) { return g_b->scale(); } diff --git a/runtime/src/gpu_sw_renderer.c b/runtime/src/gpu_sw_renderer.c index ff40aceeb..55cf2de13 100644 --- a/runtime/src/gpu_sw_renderer.c +++ b/runtime/src/gpu_sw_renderer.c @@ -30,6 +30,7 @@ #include "gpu_sw_renderer.h" #include "gpu_vram_dirty.h" #include "gpu_sw_edges.h" +#include "source_gpu_polygon_projection.h" #include #include #include @@ -280,7 +281,187 @@ static inline void put_opaque(const RTarget *t, int x, int y, uint16_t color) { gpu_vram_dirty_mark_row((uint32_t)y); } -/* Write a textured pixel — semi-trans only if texel bit 15 is set */ +/* Native cache state for the opt-in source-comparison profile. Cache reads + * sample only g_vram. Uploads invalidate texture tags, but deliberately leave + * the palette cached until a palette-key change or GP0 cache clear. */ +static struct { + uint32_t tag[256],palette_key,page; + uint16_t line[256][4],palette[256]; +} source_texture_cache; +void sw_source_texture_control(unsigned action,uint32_t page) { + int clear=action==0 || action==1 || action==2 || action==4; + if(action==3) { + unsigned old=source_texture_cache.page; + clear=((old^page)&31u)!=0 || (!!(old&0x180u)!=!!(page&0x180u)); + } + if(action==0)memset(&source_texture_cache,0,sizeof(source_texture_cache)); + if(action==0 || action==1 || action==4)source_texture_cache.palette_key=UINT32_MAX; + if(clear)for(unsigned i=0;i<256;i++)source_texture_cache.tag[i]=UINT32_MAX; + if(action==3 || action==4)source_texture_cache.page=page&511u; +} +typedef struct SourceTriangleColors { + RTarget target; + int core_x,core_y,dither,extra_work,mode; + uint32_t base[5],dx[5],dy[5]; + const SourceGPUTexture *texture; +} SourceTriangleColors; +static uint16_t source_texture_fetch(SourceTriangleColors *c,unsigned u,unsigned v) { + unsigned window=c->texture->window,page=c->texture->page; + unsigned mx=window&31u,my=(window>>5)&31u; + unsigned ux=(u&~(mx<<3))+(((window>>10)&mx)<<3)+((page&15u)<<(8-c->mode)); + unsigned vy=(v&~(my<<3))+(((window>>15)&my)<<3)+((page&16u)<<4); + unsigned address=vy*1024u+((ux>>(2-c->mode))&1023u); + unsigned entry=c->mode==0?((address>>2)&3u)|((address>>8)&252u): + ((address>>2)&7u)|((address>>7)&248u); + unsigned tag=address&~3u; + if(source_texture_cache.tag[entry]!=tag) { + c->extra_work+=4; + for(unsigned i=0;i<4;i++)source_texture_cache.line[entry][i]=g_vram[tag+i]; + source_texture_cache.tag[entry]=tag; + } + uint16_t value=source_texture_cache.line[entry][address&3u]; + if(c->mode==0)return source_texture_cache.palette[(value>>((ux&3u)*4))&15u]; + if(c->mode==1)return source_texture_cache.palette[(value>>((ux&1u)*8))&255u]; + return value; +} +static void source_texture_palette(SourceTriangleColors *c) { + unsigned clut=c->texture->clut; + uint32_t key=(clut&32767u)|((unsigned)c->mode<<16); + if(c->mode>=2 || !c->texture->load_clut || source_texture_cache.palette_key==key)return; + unsigned count=c->mode?256:16; + c->extra_work+=(int)count; + for(unsigned i=0;i>6)&511u)*1024u+(((clut&63u)*16u+i)&1023u)]; + source_texture_cache.palette_key=key; +} +static unsigned source_triangle_component(const SourceTriangleColors *c,unsigned channel,int x,int y) { + uint32_t fraction=c->base[channel]+c->dx[channel]*(uint32_t)(x-c->core_x)+c->dy[channel]*(uint32_t)(y-c->core_y); + return (fraction>>12)&255u; +} +static void source_triangle_span(void *context,int y,int x,int width) { + SourceTriangleColors *c=context; + static const int matrix[4][4]={{-4,0,-3,1},{2,-2,3,-1},{-3,1,-4,0},{3,-1,2,-2}}; + for(int end=x+width;xtexture) { + texel=source_texture_fetch(c,source_triangle_component(c,3,x,y),source_triangle_component(c,4,x,y)); + if(!texel)continue; + pixel=texel&0x8000u; + } + if(c->texture && c->texture->raw)pixel=texel; + else for(unsigned channel=0;channel<3;channel++) { + int component=(int)source_triangle_component(c,channel,x,y); + if(c->texture)component=(((texel>>(channel*5))&31)*component)>>4; + if(c->dither)component+=matrix[y&3][x&3]; + if(component<0)component=0; + if(component>255)component=255; + pixel|=(uint16_t)((component>>3)<<(5*channel)); + } + /* Coverage was clipped in logical draw-area coordinates. Installed + * VRAM has nine Y bits; wrapping happens only at the pixel store. */ + unsigned physical_y=(unsigned)y&511u,address=physical_y*1024u+(unsigned)x; + if(g_mask_check_bit && (g_vram[address]&0x8000u))continue; + if(g_semi_trans_enabled && (!c->texture || (texel&0x8000u))) { + pixel=blend_pixels(g_vram[address],pixel,g_semi_trans_mode); + if(c->texture)pixel|=0x8000u; + } + if(g_mask_set_bit)pixel|=0x8000u; + g_vram[address]=pixel;gpu_vram_dirty_mark_row(physical_y); + } +} +int sw_draw_source_triangle(const int *x,const int *y,const uint32_t *colors, + int shaded,int dither,int interlace,unsigned skip_field, + const SourceGPUTexture *texture,int *extra_work) { + *extra_work=0; + if(g_hr || g_wide_cur || g_precise_valid || g_perspective_valid)return 0; + int area=(x[1]-x[0])*(y[2]-y[0])-(x[2]-x[0])*(y[1]-y[0]); + int core=x[1]<=x[0]?(x[2]<=x[1]?2:1):x[2]page>>7)&3;if(c.mode>2)c.mode=2; + source_texture_palette(&c); /* CLUT fill precedes even degenerate draws. */ + } + if(area)for(unsigned channel=0;channel<(texture?5u:3u);channel++) { + int values[3]; + for(unsigned i=0;i<3;i++)values[i]=channel<3?(colors[i]>>(channel*8))&255u: + (texture->uv[i]>>((channel-3)*8))&255u; + c.base[channel]=((uint32_t)values[core]<<12)+2048u; + if(shaded || channel>=3) { + int64_t ax=(int64_t)(values[1]-values[0])*(y[2]-y[0])-(int64_t)(values[2]-values[0])*(y[1]-y[0]); + int64_t ay=(int64_t)(x[1]-x[0])*(values[2]-values[0])-(int64_t)(x[2]-x[0])*(values[1]-values[0]); + c.dx[channel]=(uint32_t)(ax*4096/area);c.dy[channel]=(uint32_t)(ay*4096/area); + } + } + int result=source_poly_walk(x,y,g_clip_x1,g_clip_y1,g_clip_x2,g_clip_y2,shaded||texture, + g_mask_check_bit||g_semi_trans_enabled,interlace,skip_field,source_triangle_span,&c); + *extra_work=c.extra_work; + return result>=0; +} + +int sw_draw_source_block(const SourceGPUBlock *block,int *extra_work) { + *extra_work=0; + if(g_hr || g_wide_cur || g_precise_valid || g_perspective_valid)return 0; + const uint32_t *words=block->words;unsigned opcode=words[0]>>24; + if(opcode==2) { + unsigned x0=words[1]&1008u,y0=(words[1]>>16)&511u; + unsigned width=((words[2]&1023u)+15u)&~15u,height=(words[2]>>16)&511u; + uint16_t color=((words[0]>>3)&31u)|((words[0]>>6)&992u)|((words[0]>>9)&31744u); + for(unsigned row=0;rowinterlace && (y&1u)==block->skip_field)continue; + for(unsigned col=0;col>16)&511u; + unsigned dx=words[2]&1023u,dy=(words[2]>>16)&511u; + unsigned width=words[3]&1023u,height=(words[3]>>16)&511u; + if(!width)width=1024;if(!height)height=512; + uint16_t row_chunk[128]; + for(unsigned row=0;row128)count=128; + for(unsigned i=0;ix,top=block->y,right=left+(int)(dimensions&1023u),bottom=top+(int)((dimensions>>16)&511u); + SourceGPUTexture texture={0};SourceTriangleColors c={0}; + c.target=rt_native();c.core_x=left;c.core_y=top; + for(unsigned channel=0;channel<3;channel++)c.base[channel]=((words[0]>>(channel*8))&255u)*4096u; + if(textured) { + texture.page=block->draw_mode&511u;texture.window=block->texture_window; + texture.clut=words[2]>>16;texture.raw=opcode&1;texture.load_clut=1; + c.texture=&texture;c.mode=(texture.page>>7)&3;if(c.mode>2)c.mode=2; + source_texture_palette(&c); + unsigned u=words[2]&255u,v=(words[2]>>8)&255u; + if(block->draw_mode&0x1000u)u|=1; + c.base[3]=u*4096u;c.base[4]=v*4096u; + c.dx[3]=(block->draw_mode&0x1000u)?(uint32_t)-4096:4096u; + c.dy[4]=(block->draw_mode&0x2000u)?(uint32_t)-4096:4096u; + } + if(leftclip_left)left=block->clip_left;if(topclip_top)top=block->clip_top; + if(right>block->clip_right+1)right=block->clip_right+1;if(bottom>block->clip_bottom+1)bottom=block->clip_bottom+1; + if(right>left)for(int row=top;rowinterlace && ((unsigned)row&1u)==block->skip_field)continue; + source_triangle_span(&c,row,left,right-left); + } + *extra_work=c.extra_work;return 1; +} + static inline void put_textured(const RTarget *t, int x, int y, uint16_t texel, int mod_r, int mod_g, int mod_b, int raw_texture) { diff --git a/runtime/src/input_route_observer.c b/runtime/src/input_route_observer.c new file mode 100644 index 000000000..fd09b22ef --- /dev/null +++ b/runtime/src/input_route_observer.c @@ -0,0 +1,376 @@ +/* Optional source-neutral observation of a preloaded digital route. + * Boundary N is before input N+1, after N records were supplied. This does + * not claim equivalence with another engine's frame counter. */ +#include "input_route_observer.h" +#include "psx_sha256.h" +#include "psx_memory.h" +#include "gpu.h" +#include "sio.h" +#include "cdrom.h" +#include "dma.h" +#include "timers.h" +#include "event_ring.h" +#include "debug_server.h" +#include "png_write.h" +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +extern uint8_t *g_psx_ram; +extern void gl_renderer_sync_cpu(void); +extern void vk_renderer_sync_cpu(void); +static FILE *log_file; +static FILE *sio_trace_file; +static FILE *watch_file; +static FILE *cpu_file; +static FILE *video_file; +extern void gpu_observer_video_state(uint32_t *out); +extern void interrupts_observer_field_state(uint32_t *out); +extern uint64_t psx_cycle_count; +extern uint32_t i_stat, i_mask; +static uint32_t watch_u16[32]; +static unsigned watch_count; +static uint32_t trace_seen; +static const char *output_dir; +static uint32_t total_inputs; +static uint32_t tail_ticks; +static uint32_t capture_every = 300; +static psx_sha256_ctx input_hash; +static psx_sha256_ctx delivered_hash; +static uint32_t delivered_inputs; +static uint16_t pending_word; +static int pending; +static int first_non_neutral_seen; + +static void fail(const char *message) { + fprintf(stderr, "input route observation failed: %s\n", message); + exit(3); +} +static FILE *open_output(const char *name) { + char path[4096]; + int n = snprintf(path, sizeof(path), "%s/%s", output_dir, name); + if (n < 0 || (size_t)n >= sizeof(path)) fail("output path too long"); + /* New output directories belong to the launcher; never overwrite evidence. */ + /* MSVCRT does not implement C11 fopen's x modifier. O_EXCL retains + * atomic no-overwrite semantics on both Windows and POSIX. */ +#ifdef _WIN32 + int fd = _open(path, _O_WRONLY | _O_CREAT | _O_EXCL | _O_BINARY, + _S_IREAD | _S_IWRITE); + FILE *f = fd < 0 ? NULL : _fdopen(fd, "wb"); + if (!f && fd >= 0) _close(fd); +#else + int fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0600); + FILE *f = fd < 0 ? NULL : fdopen(fd, "wb"); + if (!f && fd >= 0) close(fd); +#endif + if (!f) { fprintf(stderr, "output %s: %s\n", path, strerror(errno)); + fail("cannot create new output"); } + return f; +} +static void hex_hash(const uint8_t bytes[32], char text[65]) { + static const char h[] = "0123456789abcdef"; + for (unsigned i=0; i<32; ++i) { + text[i*2] = h[bytes[i] >> 4]; text[i*2+1] = h[bytes[i] & 15]; + } + text[64] = 0; +} +FILE *input_route_observer_output(const char *name) { return open_output(name); } +static void capture_cd_metadata(void) { + const CDROMCommandHistoryEntry *commands; + const CDROMSectorHistoryEntry *sectors; + uint64_t total = cdrom_debug_get_command_history(&commands); + FILE *f = open_output("cd-commands.jsonl"); + uint64_t start = total > CDROM_COMMAND_HISTORY_CAP ? total - CDROM_COMMAND_HISTORY_CAP : 0; + for (uint64_t i = start; i < total; ++i) { + const CDROMCommandHistoryEntry *e = &commands[i % CDROM_COMMAND_HISTORY_CAP]; + if (e->seq != i) fail("CD command history sequence mismatch"); + fprintf(f, "{\"seq\":%llu,\"frame\":%u,\"cycle\":%llu,\"kind\":%u,\"cmd\":%u,\"mode\":%u,\"pc\":%u,\"func\":%u,\"stat\":%u,\"reading\":%u,\"read_msf\":[%u,%u,%u],\"params\":[", + (unsigned long long)i, e->frame, (unsigned long long)e->cycle, e->kind, e->cmd, e->mode, + e->pc, e->func, e->stat, e->reading, e->read_min, e->read_sec, e->read_sect); + for (unsigned j = 0; j < e->param_count; ++j) fprintf(f, "%s%u", j ? "," : "", e->params[j]); + fputs("]",f); + if(e->source_clock) fprintf(f,",\"source_clock\":true,\"random_cursor\":%u,\"random_calls\":%u", + e->source_random_cursor,e->source_random_calls); + fputs("}\n", f); + } + if (ferror(f) || fclose(f)) fail("CD command metadata write"); + total = cdrom_debug_get_sector_history(§ors); + f = open_output("cd-sectors.jsonl"); + start = total > CDROM_SECTOR_HISTORY_CAP ? total - CDROM_SECTOR_HISTORY_CAP : 0; + for (uint64_t i = start; i < total; ++i) { + const CDROMSectorHistoryEntry *e = §ors[i % CDROM_SECTOR_HISTORY_CAP]; + if (e->seq != i) fail("CD sector history sequence mismatch"); + fprintf(f, "{\"seq\":%llu,\"frame\":%u,\"lba\":%d,\"size\":%d,\"mode\":%u,\"data_delivered\":%u}\n", + (unsigned long long)i, e->frame, e->lba, e->size, e->mode, e->data_delivered); + } + if (ferror(f) || fclose(f)) fail("CD sector metadata write"); + CdBurstRecord bursts[128]; + int n = cdrom_get_bursts(bursts, 128); + f = open_output("cd-bursts.jsonl"); + for (int i = n - 1; i >= 0; --i) fprintf(f, + "{\"start_frame\":%u,\"end_frame\":%u,\"sectors\":%u,\"divisor\":%u}\n", + bursts[i].start_frame, bursts[i].end_frame, bursts[i].sectors, bursts[i].divisor); + if (ferror(f) || fclose(f)) fail("CD burst metadata write"); + const DMACDROMHistoryEntry *dma; + total = dma_debug_get_cdrom_history(&dma); + start = total > DMA_CDROM_HISTORY_CAP ? total - DMA_CDROM_HISTORY_CAP : 0; + f = open_output("cd-dma.jsonl"); + for (uint64_t i = start; i < total; ++i) { + const DMACDROMHistoryEntry *e = &dma[i % DMA_CDROM_HISTORY_CAP]; + if (e->seq != i) fail("CD DMA history sequence mismatch"); + /* Retain transfer metadata only, never the ring's retail payload words. */ + fprintf(f,"{\"seq\":%llu,\"frame_start\":%u,\"frame_end\":%u,\"address\":%u,\"requested_words\":%u,\"moved_words\":%u,\"lba\":%d,\"sector_offset\":%d,\"completed\":%u,\"pc\":%u}\n", + (unsigned long long)i, e->frame_start, e->frame_end, e->start_addr, + e->requested_words,e->moved_words,e->lba,e->sector_read_pos_start,e->completed,e->pc); + } + if (ferror(f) || fclose(f)) fail("CD DMA metadata write"); + total = cdrom_timing_total(); + start = total > 4096 ? total - 4096 : 0; + f = open_output("cd-timing.jsonl"); + for (uint64_t i = start; i < total; ++i) { + CdTimingPub e; + if (!cdrom_timing_record(i, &e) || e.seq != i) fail("CD timing history sequence mismatch"); + fprintf(f,"{\"seq\":%llu,\"frame\":%u,\"lba\":%d,\"due_cycle\":%llu,\"buffer_cycle\":%llu,\"irq_arm_cycle\":%llu,\"intc_cycle\":%llu,\"flags\":%u}\n", + (unsigned long long)i,e.frame,e.lba,(unsigned long long)e.due_cycle, + (unsigned long long)e.buffer_cycle,(unsigned long long)e.irq_arm_cycle, + (unsigned long long)e.intc_cycle,e.flags); + } + if (ferror(f) || fclose(f)) fail("CD timing metadata write"); + f = open_output("device-events.json"); + if (event_ring_dump_stream(f) < 0 || fclose(f)) fail("device event metadata write"); + const CDROMTraceEntry *registers; + total = cdrom_debug_get_trace(®isters); + start = total > CDROM_TRACE_CAP ? total - CDROM_TRACE_CAP : 0; + f = open_output("cd-registers.jsonl"); + for (uint64_t i = start; i < total; ++i) { + const CDROMTraceEntry *e = ®isters[i % CDROM_TRACE_CAP]; + if (e->seq != i) fail("CD register history sequence mismatch"); + /* Exclude data-port payloads; retain status, command and IRQ metadata. */ + if (e->kind == 'D' || (e->kind == 'R' && (e->addr & 3u) == 2u)) continue; + fprintf(f,"{\"seq\":%llu,\"cycle\":%llu,\"frame\":%u,\"kind\":%u,\"addr\":%u,\"val\":%u,\"pc\":%u,\"guest_ra\":%u,\"index\":%u,\"istat\":%u,\"irq_enable\":%u,\"irq_flag\":%u,\"response_read\":%u,\"response_count\":%u,\"sector_available\":%u,\"sector_pos\":%d}\n", + (unsigned long long)i,(unsigned long long)e->cycle,e->frame,e->kind,e->addr,e->val, + e->pc,e->guest_ra,e->index_reg,e->i_stat,e->irq_enable,e->irq_flag,e->response_read, + e->response_count,e->sector_available,e->sector_read_pos); + } + if (ferror(f) || fclose(f)) fail("CD register metadata write"); +} +int input_route_observer_init(uint32_t total) { + if (log_file || total == 0) return 0; + output_dir = getenv("PSX_INPUT_ROUTE_CAPTURE_DIR"); + if (!output_dir) return 1; + if (!output_dir[0]) return 0; + total_inputs = total; + const char *watch = getenv("PSX_INPUT_ROUTE_WATCH_U16"); + if (watch) { + const char *p = watch; + do { + char *end; + if (*p < '0' || *p > '9' || watch_count == 32) return 0; + errno = 0; + unsigned long address = strtoul(p, &end, 0); + if (errno || end == p || (*end && *end != ',') || + address > PSX_MAIN_RAM_BYTES - 2 || (address & 1)) return 0; + for (unsigned i = 0; i < watch_count; ++i) + if (watch_u16[i] == address) return 0; + watch_u16[watch_count++] = (uint32_t)address; + p = *end ? end + 1 : end; + if (*end && !*p) return 0; + } while (*p); + } + const char *every = getenv("PSX_INPUT_ROUTE_CAPTURE_EVERY"); + if (every) { + char *end; + unsigned long n = strtoul(every, &end, 10); + if (!every[0] || *end || !n || n > 10000) return 0; + capture_every = (uint32_t)n; + } + const char *tail = getenv("PSX_INPUT_ROUTE_NEUTRAL_TAIL"); + if (tail) { + char *end; + unsigned long n = strtoul(tail, &end, 10); + if (!tail[0] || *end || n > 60000) return 0; + tail_ticks = (uint32_t)n; + } + psx_sha256_init(&input_hash); + psx_sha256_init(&delivered_hash); + delivered_inputs = 0; + pending = 0; + first_non_neutral_seen = 0; + log_file = open_output("checkpoints.jsonl"); + const char *cpu_state = getenv("PSX_INPUT_ROUTE_CPU_STATE"); + if (cpu_state && strcmp(cpu_state, "1") == 0) + cpu_file = open_output("cpu-state.jsonl"); + if (getenv("PSX_INPUT_ROUTE_VIDEO_STATE") && strcmp(getenv("PSX_INPUT_ROUTE_VIDEO_STATE"),"1")==0) + video_file = open_output("video-state.jsonl"); + if (watch_count) watch_file = open_output("ram-u16.jsonl"); + const char *trace = getenv("PSX_INPUT_ROUTE_TRACE"); + if (trace && strcmp(trace, "1") == 0) sio_trace_file = open_output("sio-bytes.jsonl"); + return 1; +} +void input_route_observer_input(uint16_t buttons) { + if (!log_file) return; + if (pending) fail("previous input was not delivered to SIO"); + pending_word = buttons; + pending = 1; + const uint8_t word[2] = {(uint8_t)buttons, (uint8_t)(buttons >> 8)}; + if (delivered_inputs < total_inputs) + psx_sha256_update(&input_hash, word, sizeof(word)); + else if (buttons != 0xFFFF) fail("post-input sample was not neutral"); +} +void input_route_observer_set_end(uint32_t total) { + if (!total || total != delivered_inputs || pending) fail("invalid dynamic input end"); + total_inputs=total; +} +void input_route_observer_applied(uint16_t buttons, int connected, int analog) { + if (!log_file) return; + if (!pending || pending_word != buttons || !connected || analog) + fail("SIO delivery differs from digital route"); + const uint8_t word[2] = {(uint8_t)buttons, (uint8_t)(buttons >> 8)}; + if (delivered_inputs < total_inputs) + psx_sha256_update(&delivered_hash, word, sizeof(word)); + ++delivered_inputs; + pending = 0; + if (buttons != 0xFFFF && !first_non_neutral_seen) { + fprintf(stdout, "input_route_first_non_neutral: record=%u buttons=%04X connected=%d analog=%d\n", + (unsigned)delivered_inputs, (unsigned)buttons, connected, analog); + fflush(stdout); + first_non_neutral_seen = 1; + } +} +void input_route_observer_boundary(uint32_t completed, uint64_t runtime_frame) { + if (log_file && (pending || completed != delivered_inputs)) + fail("boundary does not follow exact SIO delivery count"); + if (video_file) { + uint32_t gpu[5],clock[3]; + gpu_observer_video_state(gpu); interrupts_observer_field_state(clock); + fprintf(video_file,"{\"frame\":%u,\"runtime_frame\":%llu,\"boundary\":\"before_next_input\",\"cycle\":%llu,\"display_mode\":%u,\"vertical_start\":%u,\"vertical_end\":%u,\"gpu_field\":%u,\"lcf\":%u,\"cycles_since_vblank\":%u,\"next_period\":%u,\"profile_field\":%u}\n", + completed,(unsigned long long)runtime_frame,(unsigned long long)psx_cycle_count, + gpu[0],gpu[1],gpu[2],gpu[3],gpu[4],clock[0],clock[1],clock[2]); + if(ferror(video_file) || fflush(video_file))fail("video state write"); + } + if (cpu_file) { + const CPUState *cpu = debug_cpu_ptr; + if (!cpu) fail("CPU state unavailable"); + uint16_t counter[3], target[3]; + uint32_t mode[3], frac[3]; + int32_t irq_line[3]; + timers_get_snapshot(counter, mode, target, irq_line, frac); + fprintf(cpu_file, "{\"frame\":%u,\"runtime_frame\":%llu,\"boundary\":\"before_next_input\",\"pc\":%u,\"ra\":%u,\"sr\":%u,\"cause\":%u,\"epc\":%u,\"i_stat\":%u,\"i_mask\":%u,\"timers\":[", + completed, (unsigned long long)runtime_frame, cpu->pc, cpu->gpr[31], + cpu->cop0[12], cpu->cop0[13], cpu->cop0[14], i_stat, i_mask); + for (unsigned i=0; i<3; ++i) + fprintf(cpu_file, "%s{\"channel\":%u,\"counter\":%u,\"mode\":%u,\"target\":%u,\"irq_line\":%d,\"fraction\":%u}", + i ? "," : "", i, counter[i], mode[i], target[i], irq_line[i], frac[i]); + fputs("]}\n", cpu_file); + if (ferror(cpu_file) || fflush(cpu_file)) fail("CPU state write"); + } + if (sio_trace_file) { + const SioTraceEntry *ring; + int next; + uint32_t total = sio_get_trace(&ring, &next); + if (total - trace_seen > SIO_TRACE_CAP) fail("SIO trace overwritten"); + while (trace_seen < total) { + const SioTraceEntry *e = &ring[trace_seen % SIO_TRACE_CAP]; + if (e->seq != trace_seen) fail("SIO trace sequence mismatch"); + if (fprintf(sio_trace_file, + "{\"boundary\":%u,\"seq\":%u,\"tx\":%u,\"rx\":%u,\"ctrl\":%u,\"dev_pre\":%u,\"dev_post\":%u}\n", + completed, e->seq, e->tx, e->rx, e->ctrl, e->dev_pre, e->dev_post) < 0) + fail("SIO trace write"); + ++trace_seen; + } + if (fflush(sio_trace_file)) fail("SIO trace flush"); + } + if (watch_file) { + fprintf(watch_file, "{\"frame\":%u,\"runtime_frame\":%llu,\"boundary\":\"before_next_input\",\"cpu_pc\":%u,\"cpu_ra\":%u,\"ram_u16\":{", + completed, (unsigned long long)runtime_frame, + debug_cpu_ptr ? debug_cpu_ptr->pc : 0, + debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0); + for (unsigned i = 0; i < watch_count; ++i) { + uint32_t a = watch_u16[i]; + unsigned value = g_psx_ram[a] | ((unsigned)g_psx_ram[a + 1] << 8); + fprintf(watch_file, "%s\"%06X\":%u", i ? "," : "", a, value); + } + if (fputs("}}\n", watch_file) < 0 || fflush(watch_file)) fail("RAM watch write"); + } + if (!log_file || (completed % (completed > total_inputs && capture_every > 60 ? 60 : capture_every) + && completed != total_inputs && completed != total_inputs + tail_ticks)) return; + uint8_t digest[32]; char ram_hash[65], applied_hash[65], supplied_hash[65], name[80]; + GpuDisplayInfo di; + gpu_get_display_info(&di); + if (!di.depth24 && !di.disabled) { + gl_renderer_sync_cpu(); vk_renderer_sync_cpu(); + } + psx_sha256_compute(g_psx_ram, PSX_MAIN_RAM_BYTES, digest); + hex_hash(digest, ram_hash); + psx_sha256_ctx supplied = input_hash; + psx_sha256_final(&supplied, digest); hex_hash(digest, supplied_hash); + psx_sha256_ctx applied = delivered_hash; + psx_sha256_final(&applied, digest); hex_hash(digest, applied_hash); + unsigned w = di.disabled ? 1u : (unsigned)di.width; + unsigned h = di.disabled ? 1u : (unsigned)di.height; + if (!w || w > 640) w = 1; + if (!h || h > 512) h = 1; + uint8_t *rgb = (uint8_t *)calloc((size_t)w*h, 3); + if (!rgb) fail("screenshot allocation"); + if (!di.disabled) for (unsigned y=0; y total_inputs ? completed - total_inputs : 0), w, h, + di.disabled ? "true" : "false") < 0 || fflush(log_file)) fail("checkpoint write"); + if (completed == total_inputs) { + FILE *input_end = open_output("input-end.json"); + if (fprintf(input_end, + "{\"frame\":%u,\"applied_words_sha256\":\"%s\",\"neutral_tail_ticks_planned\":%u}\n", + (unsigned)completed, applied_hash, (unsigned)tail_ticks) < 0 || fclose(input_end)) + fail("input end write"); + } + if (completed == total_inputs + tail_ticks) { + if (watch_count) { + FILE *writes = open_output("ram-writes.jsonl"); + debug_server_dump_watched_writes(writes, watch_u16, watch_count); + if (ferror(writes) || fclose(writes)) fail("RAM write metadata export"); + } + if (watch_file && fclose(watch_file)) fail("RAM watch close"); + if (cpu_file && fclose(cpu_file)) fail("CPU state close"); + if (video_file && fclose(video_file)) fail("video state close"); + video_file = NULL; + watch_file = NULL; + if (sio_trace_file) capture_cd_metadata(); + if (sio_trace_file && fclose(sio_trace_file)) fail("SIO trace close"); + if (fclose(log_file)) fail("checkpoint close"); + log_file = NULL; + FILE *done = open_output("complete.json"); + if (fprintf(done, + "{\"frame\":%u,\"input_frames\":%u,\"neutral_tail_ticks\":%u," + "\"stop_reason\":\"declared_observation_end_before_next_sample\"," + "\"applied_words_sha256\":\"%s\",\"game_outcome\":\"unclassified\"}\n", + (unsigned)completed, (unsigned)total_inputs, (unsigned)tail_ticks, + applied_hash) < 0 || fclose(done)) fail("completion write"); + fprintf(stdout, "input_route_complete: frames=%u words_sha256=%s\n", completed, applied_hash); + fflush(stdout); + exit(0); + } +} diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index c7797f462..dff7f2f0a 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -36,8 +36,14 @@ #include "event_ring.h" #include "lockstep.h" #include "psx_cycles.h" +#include "psx_icache.h" +#include "psx_memory.h" +#include "psx_cyc.h" #include "psx_scheduler.h" #include "spu.h" +#include "input_route_field_clock.h" +#include "input_route_raster_clock.h" +#include "source_gpu_runtime.h" #include #include #include @@ -185,8 +191,30 @@ void psx_irq_raise(uint32_t bit, uint32_t detail) * (T32, MGS PAL: measured 564,480 cycles/VBlank with GPUSTAT.20 = PAL). */ #define VBLANK_CYCLES_NTSC 564480u #define VBLANK_CYCLES_PAL 677376u +static int input_route_source_fields; +static InputRouteFieldClock input_route_field_clock; +static int input_route_source_raster; +static int input_route_source_gpu_status; +static InputRouteRasterClock input_route_raster; +static uint64_t input_route_raster_deadline; +static uint32_t input_route_raster_pending; +static void input_route_raster_recompute(void) { + uint32_t distance=input_route_raster_until_rise(&input_route_raster); + input_route_raster_deadline=distance==UINT32_MAX ? UINT64_MAX : input_route_raster.cycle+distance; +} extern int gpu_video_standard_is_pal(void); static inline uint32_t vblank_period_cycles(void) { + if (input_route_source_raster) { + if (input_route_raster_deadline==UINT64_MAX) return UINT32_MAX; + return (uint32_t)(input_route_raster_deadline-input_route_raster.last_rise); + } + if (input_route_source_fields) { + if (gpu_video_standard_is_pal()) { + fprintf(stderr, "[input-field-clock] NTSC comparison profile cannot run PAL display mode\n"); + exit(4); + } + return input_route_field_clock.current_cycles; + } return gpu_video_standard_is_pal() ? VBLANK_CYCLES_PAL : VBLANK_CYCLES_NTSC; } #define VBLANK_CYCLES vblank_period_cycles() @@ -194,6 +222,37 @@ static inline uint32_t vblank_period_cycles(void) { static uint32_t dispatch_count; static uint64_t total_checks; static uint32_t cycles_since_vblank; /* incremented by interrupts_advance_cycles */ +void interrupts_observer_field_state(uint32_t *out) { + out[0]=cycles_since_vblank; out[1]=VBLANK_CYCLES; + out[2]=input_route_source_fields ? input_route_field_clock.field : UINT32_MAX; + if (input_route_source_raster) out[2]=input_route_raster.field; +} +void interrupts_raster_gp1(uint32_t word) { + if (!input_route_source_raster) return; + if (input_route_raster.cycle!=psx_cycle_count || !input_route_raster_gp1(&input_route_raster,word)) { + fprintf(stderr,"[input-raster-clock] unsynchronized GP1 or unsupported PAL mode\n"); + exit(4); + } + uint32_t command=(word>>24)&63u; + if (command==0 || command==7 || command==8) { + extern uint64_t g_psx_cycle_fast_limit; + input_route_raster_recompute(); + g_psx_cycle_fast_limit=0; + /* Existing MMIO sync ran before this write. Publish the new deadline + * after changing the raster controls so a prior later deadline cannot + * let the CPU cross an earlier newly programmed vertical boundary. */ + psx_devices_mmio_sync(); + } +} +int interrupts_raster_gpu_status(uint32_t *bits) { + if (!input_route_source_gpu_status) return 0; + if (input_route_raster.cycle!=psx_cycle_count) { + fprintf(stderr,"[input-raster-status] unsynchronized GPUSTAT read\n"); + exit(4); + } + *bits=input_route_raster_status(&input_route_raster); + return 1; +} extern uint64_t g_vblank_raise_count; extern int g_cosim_dirty_pump_site; @@ -203,6 +262,7 @@ static int in_exception; * below this. 0 = no cooldown active. Counted in guest cycles (not calls) so both * backends make the identical delivery decision — see the cooldown constants. */ static uint64_t post_exception_cooldown_until; +static struct {uint32_t pc,target,cause;} source_irq_slot; static uint32_t last_sio_seq_seen; static uint64_t last_sio_progress_cycle; @@ -413,11 +473,11 @@ uint64_t g_spu_sample_ctrl_rejects; void psx_set_midframe_audio_pump(void (*fn)(void)) { s_midframe_audio_pump = fn; } -/* While SPU IRQ9 is enabled, expose each 44.1-kHz sample as a device deadline - * so guest code can acknowledge and re-arm an IRQ-address hit before the next - * sample. Rendering a whole VBlank's accumulated samples as one chunk collapses - * multiple hardware IRQ edges into one latch, slowing IRQ-driven audio engines - * and blocking cutscene synchronization. Keep an explicit opt-out for bisecting +/* Expose each 44.1-kHz sample as a device deadline. Status-control application + * progresses even with IRQ9 disabled; gating this on IRQ enable leaves a + * polling guest's applied control stale until a later VBlank pump. IRQ-enabled + * code also needs to acknowledge and re-arm between sample address hits. + * Keep an explicit opt-out for bisecting * old captures; faithful per-sample scheduling is the production default. */ static int spu_sample_event_mode(void) { static int enabled = -1; @@ -429,7 +489,6 @@ static int spu_sample_event_mode(void) { } uint32_t psx_spu_sample_event_cycles_to_next(void) { - SpuGlobalState state; g_spu_sample_deadline_queries++; if (!spu_sample_event_mode()) { g_spu_sample_mode_rejects++; @@ -439,11 +498,6 @@ uint32_t psx_spu_sample_event_cycles_to_next(void) { g_spu_sample_pump_null_rejects++; return UINT32_MAX; } - spu_get_global_state(&state); - if ((state.ctrl & 0x0040u) == 0) { - g_spu_sample_ctrl_rejects++; - return UINT32_MAX; - } g_spu_sample_enabled_queries++; /* The PS1 CPU/SPU ratio is exactly 768 CPU cycles per 44.1-kHz sample. @@ -471,8 +525,7 @@ void psx_spu_sample_event_service(void) { if ((psx_get_cycle_count() % 768u) != 0) g_spu_sample_deferred_mismatches++; } - if ((state.ctrl & 0x0040u) != 0 && - (psx_get_cycle_count() % 768u) == 0) { + if ((psx_get_cycle_count() % 768u) == 0) { g_spu_sample_service_pumps++; s_midframe_audio_pump(); } @@ -482,7 +535,13 @@ static void fire_vblank_edge(void) { /* Subtract one VBlank period rather than reset to 0 so cycle overshoot * carries forward. Prevents long-running blocks from rounding multiple * VBlanks together. */ - cycles_since_vblank -= VBLANK_CYCLES; + if (input_route_source_raster) { + input_route_raster_pending--; + cycles_since_vblank=(uint32_t)(input_route_raster.cycle-input_route_raster.last_rise); + } else cycles_since_vblank -= VBLANK_CYCLES; + if (input_route_source_fields) + input_route_field_clock_next(&input_route_field_clock, + gpu_display_is_interlaced()); dispatch_count = 0; /* DEQUEUE: this VBlank fired. ENQUEUE: next VBlank scheduled one period out. */ event_ring_record_aux(EV_DEQ, (uint8_t)SRC_VBLANK, @@ -503,7 +562,13 @@ static void fire_vblank_edge(void) { void interrupts_service_scheduled_events(void) { note_sio_progress_cycle(); - if (in_exception) return; + /* A source raster edge latches I_STAT even while an interrupt handler + * runs with IEc clear. CPU delivery remains gated separately below. */ + if (in_exception && !source_gpu_runtime_active()) return; + if (input_route_source_raster) { + if (input_route_raster_pending && !should_defer_vblank_for_sio()) fire_vblank_edge(); + return; + } while (cycles_since_vblank >= VBLANK_CYCLES) { if (should_defer_vblank_for_sio()) return; fire_vblank_edge(); @@ -511,6 +576,11 @@ void interrupts_service_scheduled_events(void) { } uint32_t interrupts_cycles_to_vblank(void) { + if (input_route_source_raster) { + if (input_route_raster_pending) return 0; + if (input_route_raster_deadline==UINT64_MAX) return UINT32_MAX; + return input_route_raster_deadline<=input_route_raster.cycle ? 0 : (uint32_t)(input_route_raster_deadline-input_route_raster.cycle); + } if (cycles_since_vblank >= VBLANK_CYCLES) return 0; return VBLANK_CYCLES - cycles_since_vblank; } @@ -520,11 +590,30 @@ uint32_t interrupts_get_cycles_since_vblank(void) { } void interrupts_set_cycles_since_vblank(uint32_t v) { + if (input_route_source_fields || input_route_source_raster) { + fprintf(stderr, "[input-field-clock] comparison profile requires a cold boot; state restore is unsupported\n"); + exit(4); + } cycles_since_vblank = v; } void interrupts_advance_cycles(uint32_t cycles) { cycles_since_vblank += cycles; + if (input_route_source_raster) { + uint32_t old=input_route_raster.rises; + if(timers_source_raster_enabled()) { + input_route_raster_advance_observed(&input_route_raster,cycles,timers_source_raster_event,NULL); + timers_source_raster_finish(input_route_raster.cycle); + } else input_route_raster_advance(&input_route_raster,cycles); + if (input_route_raster.rises!=old) { + input_route_raster_pending+=input_route_raster.rises-old; + if (input_route_raster_pending>1) { + fprintf(stderr,"[input-raster-clock] more than one undelivered VBlank; comparison unsupported\n"); + exit(4); + } + input_route_raster_recompute(); + } + } interrupts_service_scheduled_events(); } @@ -817,6 +906,39 @@ void psx_get_freeze_diag(uint64_t *out_total_checks, } void interrupts_init(void) { + const char *field_model = getenv("PSX_INPUT_ROUTE_FIELD_MODEL"); + input_route_source_fields = 0; + input_route_source_raster = 0; + input_route_raster_pending = 0; + input_route_source_gpu_status = 0; + if (field_model && *field_model) { + int raster=strcmp(field_model,"octoshock-2.2.2-ntsc-raster")==0; + if ((!raster && strcmp(field_model, "octoshock-2.2.2-ntsc-fields") != 0) || + !getenv("PSX_INPUT_ROUTE_FILE")) { + fprintf(stderr, "[input-field-clock] invalid model or missing input route\n"); + exit(4); + } + if (raster) { + input_route_source_raster=1; + input_route_raster_reset(&input_route_raster); + input_route_raster_recompute(); + fprintf(stderr,"[input-raster-clock] experimental NTSC raster VBlank deadlines; first=%llu; native IRQ deferral/GPUSTAT/timers retained\n",(unsigned long long)input_route_raster_deadline); + } else { + input_route_source_fields = 1; + input_route_field_clock_reset(&input_route_field_clock); + fprintf(stderr, "[input-field-clock] experimental cold-boot NTSC field durations; first=%u, ratio=103896/65536\n", + input_route_field_clock.current_cycles); + } + } + const char *status_model=getenv("PSX_GPU_STATUS_MODEL"); + if (status_model && *status_model) { + if (strcmp(status_model,"octoshock-2.2.2-raster") || !input_route_source_raster) { + fprintf(stderr,"[input-raster-status] invalid model or missing NTSC raster clock\n"); + exit(4); + } + input_route_source_gpu_status=1; + fprintf(stderr,"[input-raster-status] experimental source GPUSTAT31/13; native readiness and rendering retained\n"); + } dispatch_count = 0; in_exception = 0; exception_nest_depth = 0; @@ -1009,8 +1131,7 @@ uint32_t cycles_to_next_event(void) { * card-SIO case only pushes VBlank LATER, so this estimate stays a safe * under-estimate. */ if (i_mask & (1u << IRQ_VBLANK)) { - uint32_t d = (cycles_since_vblank >= VBLANK_CYCLES) - ? 0u : (VBLANK_CYCLES - cycles_since_vblank); + uint32_t d = interrupts_cycles_to_vblank(); if (d < best) best = d; } uint32_t t = timers_cycles_to_irq(i_mask); if (t < best) best = t; @@ -1051,6 +1172,11 @@ void psx_interrupt_check_path_diag(uint64_t *entry, uint64_t *fast_sr, if (irq_deliv) *irq_deliv = g_irq_deliver_count; } +int psx_interrupt_cooldown_active(void) { + return post_exception_cooldown_until != 0 && + psx_get_cycle_count() < post_exception_cooldown_until; +} + int psx_interrupt_delivery_needed(const CPUState* cpu) { if (s_defer_switch_pending) { s_need_defer++; return 1; } if ((i_stat & i_mask) == 0) { s_skip_none++; return 0; } @@ -1058,8 +1184,7 @@ int psx_interrupt_delivery_needed(const CPUState* cpu) { uint32_t sr = cpu->cop0[COP0_SR]; if (!(sr & 0x01u) || !(sr & (1u << 10))) { s_skip_sr++; return 0; } - if (post_exception_cooldown_until != 0 && - psx_get_cycle_count() < post_exception_cooldown_until) { + if (psx_interrupt_cooldown_active()) { s_skip_cooldown++; return 0; } @@ -1075,6 +1200,12 @@ int psx_interrupt_delivery_needed(const CPUState* cpu) { return 1; } +int psx_irq_opcode_eligible(uint32_t pc) { + uint32_t instruction; + return !source_gpu_runtime_active() || + !memory_peek_instruction_word(pc,&instruction) || (instruction>>26)!=0x12u; +} + void psx_check_interrupts(CPUState* cpu) { psx_cyc_batch_flush(); extern int g_ls_suppress_record; @@ -1463,7 +1594,7 @@ void psx_check_interrupts(CPUState* cpu) { * RFE before the next delivery. Gated on the guest-cycle deadline (not a * per-call countdown) so compiled and interp agree on the delivery cycle. */ if (post_exception_cooldown_until != 0) { - if (psx_get_cycle_count() < post_exception_cooldown_until) { + if (psx_interrupt_cooldown_active()) { irq_record_outcome(EV_IRQ_GATE, GATE_COOLDOWN, 0); #ifdef PSX_COSIM COSIM_IRQ_NOTE(3u); @@ -1496,6 +1627,7 @@ void psx_check_interrupts(CPUState* cpu) { PSX_CHECK_INTERRUPTS_RETURN(); } /* No deliverable interrupt source (hw masked and no sw pending) */ + uint32_t source_irq_cause_ce = 0; /* Architectural take-PC = the resume PC (same selection the async-RFE block * below uses): the dirty-interp commits the exact interrupted instruction in * g_dirty_safe_resume_pc; compiled code passes its block-entry PC via @@ -1505,6 +1637,42 @@ void psx_check_interrupts(CPUState* cpu) { extern uint32_t g_dirty_safe_resume_pc; uint32_t take_pc = g_dirty_safe_resume_pc ? g_dirty_safe_resume_pc : s_compiled_interrupt_resume_pc; + /* The existing cache/cycle model fetches the interrupted opcode before + * selecting the interrupt operation. Preserve that fetch's tags and + * load-absorb effects, but never execute its register/store effects. + * Authored original-core IRQ/JR/RFE cases own this source-model order; + * it is not a claim of physical pipeline timing. Unknown legacy entry + * sentinels have no guest fetch identity and retain their old behavior. + * Keep this before SR/EPC mutation and before the delivery timestamp. */ + uint32_t fetch_pc = source_irq_slot.pc?source_irq_slot.pc:take_pc; + if (fetch_pc == 0u) { + extern int psx_scheduler_top_level_resume_active(void); + if (psx_scheduler_top_level_resume_active()) fetch_pc = cpu->pc; + } + /* Source IPCache selects COP2 itself even with an IRQ pending. Do not + * fetch/charge a synthetic interrupt operation at that opcode; the + * normal executor owns its effects and the next recognition point. */ + if(fetch_pc && !psx_irq_opcode_eligible(fetch_pc)) + PSX_CHECK_INTERRUPTS_RETURN(); + uint32_t fetch_phys = fetch_pc & 0x1FFFFFFFu; + if (fetch_pc != 0u && (fetch_pc & 3u) == 0u && + (fetch_phys < 0x00200000u || + (fetch_phys >= 0x1FC00000u && fetch_phys < 0x1FC80000u && + psx_is_dispatchable(fetch_pc)))) { + extern int source_gpu_runtime_active(void); + if (source_gpu_runtime_active() && !source_irq_slot.pc) { + uint32_t instruction; + if (memory_peek_instruction_word(fetch_pc, &instruction)) + source_irq_cause_ce = (instruction << 2) & 0x30000000u; + } + psx_icache_fetch(cpu, fetch_pc); +#ifdef PSX_ENABLE_BLOCK_CYCLES + /* Interrupt dispatch has no dependencies on the preempted opcode. + * Its ordinary step still consumes base/load-absorb timing. */ + psx_cyc_step(cpu, 0u); + psx_cyc_batch_flush(); +#endif + } irq_record_outcome(EV_IRQ_DELIVER, 0, take_pc); } g_irq_deliver_count++; @@ -1553,6 +1721,17 @@ void psx_check_interrupts(CPUState* cpu) { * software-interrupt delivery gets IP2 reflecting the true line state * instead of whatever bit 10 happened to be left as. */ cpu->cop0[COP0_CAUSE] = (cpu->cop0[COP0_CAUSE] & ~0x7C) | (0 << 2); + if(source_irq_slot.pc) { + cpu->cop0[COP0_CAUSE]=(cpu->cop0[COP0_CAUSE]&0x0000ff00u)|source_irq_slot.cause; + cpu->cop0[6]=source_irq_slot.target; /* source TAR / branch destination */ + source_irq_slot.pc=0; /* nested handler entries have their own context */ + } else { + extern int source_gpu_runtime_active(void); + /* Original Exception derives CE from the fetched opcode for IRQ too. + * This is source-model compatibility, not a physical-CPU CE claim. */ + if(source_gpu_runtime_active()) + cpu->cop0[COP0_CAUSE]=(cpu->cop0[COP0_CAUSE]&0x0000ff00u)|source_irq_cause_ce; + } psx_irq_refresh_cause_ip2(); /* Push SR exception stack: shift bits [5:0] left by 2. */ @@ -1698,25 +1877,11 @@ void psx_check_interrupts(CPUState* cpu) { * (e.g. VSync callback loop at 0xBFC421D8), still in exception * context. The redirected code eventually calls ReturnFromException. * - Code 1: ReturnFromException — exit the loop entirely. */ - uint32_t target_pc; - if (sr & 0x00400000u) { - target_pc = 0xBFC00180u; - } else { - uint32_t w0 = cpu->read_word(0x80000080u); - uint32_t w1 = cpu->read_word(0x80000084u); - uint32_t hi_val = (w0 & 0xFFFF) << 16; - int16_t lo_val = (int16_t)(w1 & 0xFFFF); - target_pc = hi_val + (uint32_t)(int32_t)lo_val; - /* The RAM exception vector is a LUI/ADDIU pair that materializes the - * installed handler address in $k0 before transferring to it. The - * host-side fast path decodes that pair and dispatches straight to the - * target, so it must also commit the pair's architectural register - * result. Vigilante 8's handler uses $k0 as its table base on its very - * first instruction; leaving the interrupted value in $k0 made it load - * a BIOS instruction word (0xAD400000) as a jump target. */ - uint32_t vector_reg = (w1 >> 16) & 31u; - if (vector_reg != 0u) cpu->gpr[vector_reg] = target_pc; - } + uint32_t target_pc = (sr & 0x00400000u) ? 0xBFC00180u : 0x80000080u; + /* Execute the installed vector through the ordinary guest dispatcher. + * Decoding LUI/ADDIU here skipped its instruction fetches and effects, + * charged data reads instead, and assumed every vector used that encoding. + * The real vector sets $k0 when required by its installed handler. */ /* Record which fiber owns this setjmp. Any subsequent longjmp must * happen on this same fiber; if a non-owner fiber needs to longjmp @@ -2017,7 +2182,13 @@ void psx_check_interrupts(CPUState* cpu) { * executes at least one instruction between exceptions; in our model * each "block" is many instructions, but the handler also consumes * hundreds of sub-dispatches per invocation. */ - if ((i_stat & i_mask) != 0 && i_stat == pre_handler_istat) { + extern int source_gpu_runtime_active(void); + if (source_gpu_runtime_active()) { + /* The original instruction loop recognizes an eligible IRQ again after + * guest RFE, including a newly armed or still-pending line. Legacy + * dispatcher breathing room must not add guest clocks to this profile. */ + post_exception_cooldown_until = 0; + } else if ((i_stat & i_mask) != 0 && i_stat == pre_handler_istat) { /* unclaimed: give main code guest-time to install handlers */ post_exception_cooldown_until = psx_get_cycle_count() + POST_EXC_UNCLAIMED_COOLDOWN_CYCLES; } else { @@ -2188,6 +2359,21 @@ void psx_check_interrupts_at(CPUState* cpu, uint32_t resume_pc) { s_compiled_interrupt_resume_pc = prev; } +int psx_check_interrupts_delay_slot(CPUState *cpu,uint32_t slot_pc, + uint32_t target,int taken,uint32_t instruction) { + if(source_gpu_runtime_active() && (instruction>>26)==0x12u)return 0; + extern uint32_t g_dirty_safe_resume_pc; + uint32_t previous=g_dirty_safe_resume_pc; + uint64_t before=g_irq_deliver_count; + source_irq_slot.pc=slot_pc;source_irq_slot.target=target; + source_irq_slot.cause=0x80000000u|(taken?0x40000000u:0u)|((instruction<<2)&0x30000000u); + g_dirty_safe_resume_pc=slot_pc-4u;cpu->pc=slot_pc-4u; + psx_check_interrupts_at(cpu,slot_pc-4u); + g_dirty_safe_resume_pc=previous;source_irq_slot.pc=0; + if(g_irq_deliver_count!=before && !cpu->pc)cpu->pc=slot_pc-4u; + return g_irq_deliver_count!=before; +} + int psx_interrupts_checked_at_current_cycle(uint32_t resume_pc) { return s_last_interrupt_check_cycle == psx_get_cycle_count() && same_guest_pc(s_last_interrupt_check_pc, resume_pc); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 75bb8121c..83ee54eb5 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -18,6 +18,7 @@ #include "bios_hle_plan.h" #include "psx_bios_backend.h" #include "psx_cycles.h" +#include "source_gpu_runtime.h" #include "starvation_ring.h" #include "load_accel.h" #include "savestate.h" @@ -7041,6 +7042,9 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { sample_headless_pad_into_sio(override); else sample_pad_into_sio(override); +#ifndef PSX_NO_DEBUG_TOOLS + debug_server_note_input_applied(); +#endif } /* Offline vblank boundary: record/replay/compare (PSX_RB_SELFCHECK). * Defer opening a window while multitap arming is still pending — @@ -13513,6 +13517,15 @@ int main(int argc, char** argv) { interrupts_init(); sio_init(); psx_event_step_conservative_env_init(); + const char *gpu_work_model=std::getenv("PSX_GPU_DMA_MODEL"); + if(gpu_work_model && !std::strcmp(gpu_work_model,"octoshock-2.2.2-bounded-quad")) { + const char *field=std::getenv("PSX_INPUT_ROUTE_FIELD_MODEL"); + if(!std::getenv("PSX_INPUT_ROUTE_FILE") || !field || std::strcmp(field,"octoshock-2.2.2-ntsc-raster")) { + std::fprintf(stderr,"[source-gpu-service] bounded quad mode requires a route and source NTSC raster clock\n"); + return 2; + } + source_gpu_runtime_init(); + } /* Seed per-player device routing from the resolved [controller] config. * SDL controller handles are opened later (after SDL_Init); here we only * set the PSX-visible connection + pad type so the BIOS sees the right @@ -13642,8 +13655,22 @@ int main(int argc, char** argv) { std::atexit(game_options_save_now); #ifndef PSX_NO_DEBUG_TOOLS debug_server_init(debug_port); + /* Private deterministic route gate: no TCP-upload timing in guest + * history. An explicit malformed route aborts before guest execution. */ + if (const char *route = std::getenv("PSX_INPUT_ROUTE_FILE")) { + if (!route[0] || net_cfg.enabled || + !debug_server_preload_input_route(route)) { + std::fprintf(stderr, "psxrecomp: prestart input route rejected\n"); + debug_server_shutdown(); + return 2; + } + } #else (void)debug_port; + if (std::getenv("PSX_INPUT_ROUTE_FILE")) { + std::fprintf(stderr, "psxrecomp: input routes require debug tools\n"); + return 2; + } #endif #ifdef PSX_COSIM cosim_init(); /* first-divergence oracle server */ diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 124a6c37c..c24ea9151 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -14,6 +14,7 @@ #include "dma.h" #include "fntrace.h" #include "gpu.h" +#include "source_gpu_runtime.h" #include "mdec.h" #include "mod_memory.h" #include "sio.h" @@ -24,6 +25,7 @@ #include "dirty_ram_interp.h" #include "guest_tty.h" #include "psx_cycles.h" +#include "psx_icache.h" #include "starvation_ring.h" #include #include @@ -114,9 +116,24 @@ static inline uint32_t psx_phys_addr(uint32_t addr) { /* Expose RAM pointer for oracle comparison (find_first_divergence). */ uint8_t *memory_get_ram_ptr(void) { return ram; } +int memory_peek_instruction_word(uint32_t address, uint32_t *value) { + if (!value || (address & 3u) || address >= 0xC0000000u) return 0; + uint32_t phys = address & 0x1FFFFFFFu; + const uint8_t *bytes; + if (phys < RAM_SIZE) bytes = ram + phys; + else if (phys >= 0x1FC00000u && phys < 0x1FC00000u + BIOS_ROM_SIZE) + bytes = bios_rom + (phys - 0x1FC00000u); + else return 0; + *value = (uint32_t)bytes[0] | ((uint32_t)bytes[1] << 8) | + ((uint32_t)bytes[2] << 16) | ((uint32_t)bytes[3] << 24); + return 1; +} uint8_t *memory_get_scratchpad_ptr(void) { return scratchpad; } void memory_clear_low_boot_scratch(void) { + /* Game entry is a host bookkeeping event. The source profile preserves + * guest RAM across it, including BIOS scratch and trampoline words. */ + if (source_gpu_runtime_active()) return; memset(ram, 0, 0x10u); } @@ -1103,6 +1120,14 @@ static void unmapped_fatal(uint32_t vaddr, uint32_t phys, const char* op) { /* --- MMIO read/write helpers --- */ +static uint32_t irq_read_image(uint32_t value) { + /* Retained Octoshock 2.2.2 IRQ_Read supplies these fixed upper bits + * before selecting the byte lane. This is source compatibility, not + * an emulation of a measured physical open bus. Keep stored IRQ state + * and the default runtime's read image unchanged. */ + return value | (source_gpu_runtime_active() ? 0x1F800000u : 0u); +} + static uint32_t mmio_read32_impl(uint32_t addr) { SHADOW_NOTE_MMIO(); /* Memory control: 0x1F801000..0x1F801020 */ @@ -1118,8 +1143,8 @@ static uint32_t mmio_read32_impl(uint32_t addr) { return ram_size_reg; } /* Interrupts: 0x1F801070, 0x1F801074 */ - if (addr == 0x1F801070u) { sio_tick(0); return i_stat; } - if (addr == 0x1F801074u) return i_mask; + if (addr == 0x1F801070u) { sio_tick(0); return irq_read_image(i_stat); } + if (addr == 0x1F801074u) return irq_read_image(i_mask); /* DMA: 0x1F801080..0x1F8010FF */ if (addr >= 0x1F801080u && addr <= 0x1F8010FFu) { return dma_read(addr); @@ -1241,14 +1266,17 @@ static uint16_t mmio_read16_impl(uint32_t addr) { if (addr >= 0x1F801070u && addr <= 0x1F801072u) { sio_tick(0); uint32_t shift = (addr & 2u) ? 16u : 0u; - return (uint16_t)(i_stat >> shift); + return (uint16_t)(irq_read_image(i_stat) >> shift); } if (addr >= 0x1F801074u && addr <= 0x1F801076u) { uint32_t shift = (addr & 2u) ? 16u : 0u; - return (uint16_t)(i_mask >> shift); + return (uint16_t)(irq_read_image(i_mask) >> shift); } /* Timers: 0x1F801100..0x1F80112F */ if (addr >= 0x1F801100u && addr <= 0x1F80112Fu) { + if(source_gpu_runtime_active() && timers_source_raster_enabled() && + addr>=0x1f801110u && addr<=0x1f80111fu) + return (uint16_t)(timers_read(addr&~3u) >> (8u*(addr&3u))); return (uint16_t)timers_read(addr); } /* DMA: 0x1F801080..0x1F8010FF */ @@ -1345,6 +1373,11 @@ static void mmio_write16(uint32_t addr, uint16_t val) { static uint8_t mmio_read8_impl(uint32_t addr) { SHADOW_NOTE_MMIO(); + /* The source Timer1 read decodes the register before selecting its lane. + * Reading an upper mode lane still performs the mode read-to-clear. */ + if(source_gpu_runtime_active() && timers_source_raster_enabled() && + addr>=0x1f801110u && addr<=0x1f80111fu) + return (uint8_t)(timers_read(addr&~3u) >> (8u*(addr&3u))); /* Memory control: 0x1F801000..0x1F80103C — byte lane of the 32-bit reg. */ if (addr >= 0x1F801000u && addr <= 0x1F80103Fu) { uint32_t v = mem_ctrl[(addr - 0x1F801000u) >> 2]; @@ -1357,7 +1390,7 @@ static uint8_t mmio_read8_impl(uint32_t addr) { if (addr >= 0x1F801070u && addr <= 0x1F801077u) { if (addr < 0x1F801074u) sio_tick(0); uint32_t val = (addr < 0x1F801074u) ? i_stat : i_mask; - return (uint8_t)(val >> (8 * (addr & 3))); + return (uint8_t)(irq_read_image(val) >> (8 * (addr & 3))); } /* SIO: 0x1F801040..0x1F80104F */ if (addr >= 0x1F801040u && addr <= 0x1F80104Fu) { @@ -1628,6 +1661,11 @@ void psx_write_word(uint32_t addr, uint32_t val) { } static void psx_write_word_raw(uint32_t addr, uint32_t val) { g_guest_store_count++; + /* Isolated stores target the cache before memory-address routing. */ + if (sr_ptr && (*sr_ptr & 0x10000u)) { + psx_icache_isolated_store(addr, cache_ctrl); + return; + } /* (pgxp) plain-store shadow invalidation retired: the PGXP engine * validates tracked words against the actual packet word on read, so an * overwritten word can never be believed (ENHANCEMENTS.md G1). */ @@ -1636,25 +1674,16 @@ static void psx_write_word_raw(uint32_t addr, uint32_t val) { /* KSEG2 guard — see psx_read_word_raw. */ if (addr >= 0xC0000000u) { g_kseg2_ignored_writes++; return; } - /* IsC (Isolate Cache): when set, writes go to D-cache only. - * We have no cache model, so silently discard RAM/scratchpad writes. */ - if (sr_ptr && (*sr_ptr & 0x10000u)) return; - uint32_t phys = psx_phys_addr(addr); - /* The generated BIOS mirrors its exception trampoline to 0x80000000 during - * boot. On hardware this mirror copy is not visible in RAM; only the real - * exception vector at 0x80000080 is. Tomba 2 later passes buffer=0 to the - * BIOS card write routine, so stale mirror bytes at 0 corrupt the sector 63 - * management write and leave the load menu stuck checking the card. */ - if (fntrace_is_game_started() && + /* Legacy Tomba 2 card-buffer workaround. Source execution writes ordinary + * RAM here, so the source profile must not suppress the guest store. */ + if (!source_gpu_runtime_active() && fntrace_is_game_started() && phys < 0x10u && g_debug_last_store_pc == 0xBFC10A00u) return; - /* BIOS helpers use RAM address zero as a tiny delay-loop scratch between - * device-register polls. Treat these two dummy stores as non-visible; real - * hardware / Beetle preserve Tomba 2's sector-63 card-management payload - * when it passes buffer=0 to _card_write. */ - if (fntrace_is_game_started() && phys == 0u) { + /* Retain the legacy card-buffer store filter outside the source profile. + * BIOS delay-loop scratch stores are observable guest RAM writes. */ + if (!source_gpu_runtime_active() && fntrace_is_game_started() && phys == 0u) { switch (g_debug_last_store_pc) { case 0xBFC04E90u: case 0xBFC04EF0u: @@ -1783,7 +1812,9 @@ static uint16_t psx_read_half_raw(uint32_t addr) { uint32_t phys = psx_phys_addr(addr); if (phys < RAM_SIZE) { - return (uint16_t)ram[phys] | ((uint16_t)ram[phys + 1] << 8); + uint16_t v = (uint16_t)ram[phys] | ((uint16_t)ram[phys + 1] << 8); + if (g_ram_read_watch_active) debug_server_trace_ram_read_watch(phys, v); + return v; } { uint32_t off; @@ -1830,7 +1861,10 @@ void psx_write_half(uint32_t addr, uint16_t val) { } static void psx_write_half_raw(uint32_t addr, uint16_t val) { g_guest_store_count++; - if (sr_ptr && (*sr_ptr & 0x10000u)) return; + if (sr_ptr && (*sr_ptr & 0x10000u)) { + psx_icache_isolated_store(addr, cache_ctrl); + return; + } /* KSEG2 guard — see psx_read_word_raw. */ if (addr >= 0xC0000000u) { g_kseg2_ignored_writes++; return; } @@ -1902,7 +1936,9 @@ static uint8_t psx_read_byte_raw(uint32_t addr) { uint32_t phys = psx_phys_addr(addr); if (phys < RAM_SIZE) { - return ram[phys]; + uint8_t v = ram[phys]; + if (g_ram_read_watch_active) debug_server_trace_ram_read_watch(phys, v); + return v; } { uint32_t off; @@ -1947,7 +1983,9 @@ static uint8_t psx_read_byte_raw(uint32_t addr) { * (libretro.cpp:868-869). That is non-zero only while a DMA channel is actively * stealing the bus; modeling it needs the live steal count threaded out of the DMA * controller, and it can't be isolated by a static ruler. It remains an unmodeled - * dynamic axis; the per-region device waits below are the static, validatable piece. */ + * dynamic axis by default. The optional source VRAM-upload DMA profile now supplies + * that live count for its bounded request-mode context. It is sampled before the + * read charge dispatches device events, and joins the absorbed region cost. */ /* Runtime-only production cycle charge for data-load timing. Overlay DLLs * flush their local pending-cycle accumulator before entering these host @@ -2010,8 +2048,8 @@ static inline uint32_t psx_mmio_read_wait(uint32_t phys, uint32_t size) { /* Beetle ReadMemory data-access timing (cpu.cpp:369-448), after §1/deps/DO_LDS. * compl_cost = 2 (CPU load) / 1 (LWC2); arm_rt = GPR to arm as pending load, or * 0x20 = none (LWC2, dest is a GTE reg). size = access width in bytes (1/2/4). */ -static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys, uint32_t size, - uint32_t compl_cost, uint32_t arm_rt) { +static inline uint32_t psx_cyc_readmem_prepare(CPUState* cpu, uint32_t phys, uint32_t size, + uint32_t compl_cost, uint32_t arm_rt, int defer_gpu_wait) { /* ReadMemory start (369-370): clear the current give-back slot. */ cpu->read_absorb[cpu->read_absorb_which] = 0u; cpu->read_absorb_which = 0u; @@ -2019,16 +2057,29 @@ static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys, uint32_t size, if (phys >= 0x1F800000u && phys <= 0x1F8003FFu) { cpu->ld_absorb = 0u; cpu->ld_which_t = (uint8_t)arm_rt; - return; + return 0u; } - /* fudge (424): +2 iff the predecessor committed no load (read_fudge==0x20). - * Combined with region+completion into one advance — deadline catch-up - * replays exact event boundaries, so splitting the charge is only host cost. */ - uint32_t region = psx_mmio_read_wait(phys, size); /* device-region wait */ + /* The device supplies its value after the bus wait, before the CPU load + * completion cycles. Advancing through completion before an MMIO read can + * sample the next timer tick or consume a newly arrived device response. + * Preserve the existing RAM/ROM path; its DMA/read-order coverage is separate. */ + uint32_t region = psx_mmio_read_wait(phys, size) + dma_cpu_read_penalty(); uint32_t cost = region + compl_cost; /* LDAbsorb = region + completion */ uint32_t fudge = (uint32_t)((cpu->read_fudge >> 4) & 2u); cpu->ld_absorb = cost; - psx_advance_cycles(fudge + cost); + uint32_t completion = (phys >= 0x1F801000u && phys <= 0x1F802FFFu) + ? compl_cost : 0u; + /* Source MemRW services events after ReadFudge and DMA steal, before + * the device's one-cycle region wait. That final wait does not service again. + * Returning it with completion keeps the value/GPUREAD side effect ahead + * of an event crossed only by that wait, while earlier events stay visible. + * HBlank-driven Timer1 counters likewise have no CPU tick in that wait; + * TIMER_Read updates timers, but does not service a newly due GPU event. + * DMA_Read and IRQ_Read likewise return stored registers without another + * event update. IRQ status must not include an edge crossed by that wait. */ + if(defer_gpu_wait)completion+=psx_mmio_read_wait(phys,size); + psx_advance_cycles(fudge + cost - completion); + if(defer_gpu_wait)psx_devices_service_to_now(); cpu->ld_which_t = (uint8_t)arm_rt; /* PROOF GATE (PSX_POLL_PROOF=N, default 0/off): a FLAT, non-absorbed extra N * cycles per main-RAM data read — replicates the historical "+6 cyc/main-RAM @@ -2043,6 +2094,14 @@ static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys, uint32_t size, if (s_pp < 0) { const char* e = getenv("PSX_POLL_PROOF"); s_pp = (e && e[0]) ? atoi(e) : 0; } if (s_pp > 0) psx_load_charge_cycles((uint32_t)s_pp); } + return completion; +} + +/* Timing-only callers still consume the complete load duration. */ +static inline void psx_cyc_readmem(CPUState* cpu, uint32_t phys, uint32_t size, + uint32_t compl_cost, uint32_t arm_rt) { + uint32_t completion = psx_cyc_readmem_prepare(cpu, phys, size, compl_cost, arm_rt, 0); + if (completion) psx_advance_cycles(completion); } /* Resolve PSX_LOAD_DELAY once (shared with inlined psx_cyc.h helpers). */ @@ -2056,23 +2115,24 @@ int psx_load_delay_enabled(void) { /* The interlock half of a load (§1+deps+(cancel)+DO_LDS+ReadMemory). Gated on * PSX_ENABLE_BLOCK_CYCLES so the Beetle-oracle build (cycles off) does a plain read. */ -static inline void psx_cyc_load_timing(CPUState* cpu, uint32_t addr, uint32_t size, - uint32_t rt, uint32_t reg_mask) { +static inline uint32_t psx_cyc_load_timing(CPUState* cpu, uint32_t addr, uint32_t size, + uint32_t rt, uint32_t reg_mask, int defer_gpu_wait) { #ifdef PSX_ENABLE_BLOCK_CYCLES /* Bisect gate (PSX_LOAD_DELAY=0): disable the R3000A load-delay interlock * timing (the d8c4a8e/fade560/d597797 feature) to test whether it moves the * MMX6 cutscene ordering. Read once; default on. */ if (!psx_load_delay_enabled()) { (void)addr; (void)size; (void)rt; (void)reg_mask; - return; + return 0u; } psx_cyc_base(cpu); psx_cyc_deps(cpu, reg_mask); if (cpu->ld_which_t == rt) cpu->ld_which_t = 0u; /* cancel pending load to same dest */ psx_cyc_lds(cpu); - psx_cyc_readmem(cpu, addr & 0x1FFFFFFFu, size, 2u, rt); + return psx_cyc_readmem_prepare(cpu, addr & 0x1FFFFFFFu, size, 2u, rt, defer_gpu_wait); #else (void)cpu; (void)addr; (void)size; (void)rt; (void)reg_mask; + return 0u; #endif } @@ -2104,33 +2164,64 @@ static inline int psx_cyc_main_ram_fast_addr(uint32_t addr, uint32_t width, #endif /* Slow paths for the inlined helpers in psx_cyc.h (MMIO / lockstep / shards). */ +static int source_hblank_counter_sample(uint32_t addr) { + return source_gpu_runtime_active() && addr<0xc0000000u && + timers_source_hblank_counter_read(addr&0x1fffffffu); +} +static int source_dma_register_sample(uint32_t addr) { + uint32_t physical=addr&0x1fffffffu; + return source_gpu_runtime_active() && addr<0xc0000000u && + physical>=0x1f801080u && physical<=0x1f8010ffu; +} +static int source_irq_register_sample(uint32_t addr) { + uint32_t physical=addr&0x1fffffffu; + return source_gpu_runtime_active() && addr<0xc0000000u && + physical>=0x1f801070u && physical<=0x1f801077u; +} uint32_t psx_cyc_load_word_slow(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) { - psx_cyc_load_timing(cpu, addr, 4u, rt, reg_mask); - return psx_read_word(addr); + uint32_t physical=addr&0x1fffffffu; + int source_gpu_sample=source_gpu_runtime_active() && addr<0xc0000000u && + (physical==0x1f801810u || physical==0x1f801814u); + uint32_t completion=psx_cyc_load_timing(cpu,addr,4u,rt,reg_mask, + source_gpu_sample || source_hblank_counter_sample(addr) || + source_dma_register_sample(addr) || source_irq_register_sample(addr)); + uint32_t value = psx_read_word(addr); + if (completion) psx_advance_cycles(completion); + return value; } uint16_t psx_cyc_load_half_slow(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) { - psx_cyc_load_timing(cpu, addr, 2u, rt, reg_mask); - return psx_read_half(addr); + uint32_t completion = psx_cyc_load_timing(cpu, addr, 2u, rt, reg_mask, + source_hblank_counter_sample(addr) || source_dma_register_sample(addr) || + source_irq_register_sample(addr)); + uint16_t value = psx_read_half(addr); + if (completion) psx_advance_cycles(completion); + return value; } void psx_cyc_load_word_timing_only(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) { - psx_cyc_load_timing(cpu, addr, 4u, rt, reg_mask); + uint32_t completion = psx_cyc_load_timing(cpu, addr, 4u, rt, reg_mask, 0); + if (completion) psx_advance_cycles(completion); } uint8_t psx_cyc_load_byte(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) { - psx_cyc_load_timing(cpu, addr, 1u, rt, reg_mask); + uint32_t completion = psx_cyc_load_timing(cpu, addr, 1u, rt, reg_mask, + source_hblank_counter_sample(addr) || source_dma_register_sample(addr) || + source_irq_register_sample(addr)); #if defined(PSX_NO_DEBUG_TOOLS) && !defined(PSX_COSIM) uint32_t phys; if (psx_cyc_main_ram_fast_addr(addr, 1u, &phys)) return ram[phys]; #endif - return psx_read_byte(addr); + uint8_t value = psx_read_byte(addr); + if (completion) psx_advance_cycles(completion); + return value; } /* LWC2 (GTE load): §1/DO_LDS done by psx_cyc_step(cpu,0); the GTE deadline stall by * psx_gte_stall — both emitted before this call. 32-bit access, completion +1, no * LDWhich arm. */ uint32_t psx_cyc_lwc2_read(CPUState* cpu, uint32_t addr) { + uint32_t completion = 0u; #ifdef PSX_ENABLE_BLOCK_CYCLES - psx_cyc_readmem(cpu, addr & 0x1FFFFFFFu, 4u, 1u, 0x20u); + completion = psx_cyc_readmem_prepare(cpu, addr & 0x1FFFFFFFu, 4u, 1u, 0x20u, 0); #else (void)cpu; #endif @@ -2143,7 +2234,9 @@ uint32_t psx_cyc_lwc2_read(CPUState* cpu, uint32_t addr) { | ((uint32_t)ram[phys + 3] << 24); } #endif - return psx_read_word(addr); + uint32_t value = psx_read_word(addr); + if (completion) psx_advance_cycles(completion); + return value; } /* Deprecated uncharged passthroughs (the +4 flat wait-state model is gone; load @@ -2165,7 +2258,10 @@ void psx_write_byte(uint32_t addr, uint8_t val) { } static void psx_write_byte_raw(uint32_t addr, uint8_t val) { g_guest_store_count++; - if (sr_ptr && (*sr_ptr & 0x10000u)) return; + if (sr_ptr && (*sr_ptr & 0x10000u)) { + psx_icache_isolated_store(addr, cache_ctrl); + return; + } /* KSEG2 guard — see psx_read_word_raw. */ if (addr >= 0xC0000000u) { g_kseg2_ignored_writes++; return; } diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index 6a0ca6864..97f9fdf9e 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -2273,6 +2273,8 @@ static void init_callbacks(void) { extern uint8_t psx_cyc_load_byte(CPUState*, uint32_t, uint32_t, uint32_t); extern uint32_t psx_cyc_lwc2_read(CPUState*, uint32_t); extern void psx_icache_fetch_fn(CPUState*, uint32_t); + extern int psx_cpu_step_boundary_enabled(int); + extern void psx_cpu_step_boundary_fn(CPUState*, uint32_t); extern void psx_muldiv_set(CPUState*, uint32_t); extern void psx_muldiv_stall(CPUState*); extern uint32_t psx_mult_latency_s(uint32_t); @@ -2285,6 +2287,8 @@ static void init_callbacks(void) { s_callbacks.cyc_load_byte = psx_cyc_load_byte; s_callbacks.cyc_lwc2_read = psx_cyc_lwc2_read; s_callbacks.icache_fetch = psx_icache_fetch_fn; + s_callbacks.cpu_step_boundary_enabled = psx_cpu_step_boundary_enabled; + s_callbacks.cpu_step_boundary = psx_cpu_step_boundary_fn; s_callbacks.muldiv_set = psx_muldiv_set; s_callbacks.muldiv_stall = psx_muldiv_stall; s_callbacks.mult_latency_s = psx_mult_latency_s; diff --git a/runtime/src/psx_cycles.c b/runtime/src/psx_cycles.c index 1057093f4..47f8e3ebf 100644 --- a/runtime/src/psx_cycles.c +++ b/runtime/src/psx_cycles.c @@ -13,6 +13,7 @@ #include "sio.h" #include "starvation_ring.h" #include "timers.h" +#include "source_gpu_runtime.h" #if defined(PSX_HAS_RECOMP_NET) #include "psx_netplay.h" #endif @@ -77,6 +78,7 @@ static void advance_devices(uint32_t c) { psx_cycle_count += (uint64_t)c; sio_advance(c); cdrom_advance(c); + source_gpu_runtime_advance(); dma_advance(c); timers_advance(c); interrupts_advance_cycles(c); @@ -139,6 +141,7 @@ static uint32_t devices_cycles_to_next_internal_event(void) { uint32_t t = timers_cycles_to_irq(0xFFFFFFFFu); if (t < best) best = t; uint32_t c = cdrom_cycles_to_irq(0xFFFFFFFFu); if (c < best) best = c; uint32_t d = dma_cycles_to_internal_event(); if (d < best) best = d; + uint32_t g = source_gpu_runtime_cycles_to_event(); if (g < best) best = g; uint32_t s = sio_cycles_to_irq(0xFFFFFFFFu); if (s < best) best = s; uint32_t a = psx_spu_sample_event_cycles_to_next(); if (a < best) best = a; if (best == 0) best = 1; /* due/overdue: process within one cycle */ @@ -674,10 +677,16 @@ uint32_t psx_mult_latency_u(uint32_t rs) { /* MULTU (unsigned) */ /* DIV/DIVU latency is the fixed constant 37 — emitted directly at the op site. */ void psx_muldiv_set(CPUState* cpu, uint32_t latency) { + /* The deadline belongs to this instruction, including unpublished CPU + * work from generated blocks and local charge accumulators. */ + psx_cyc_batch_flush(); cpu->muldiv_ts_done = psx_cycle_count + (uint64_t)latency; } void psx_muldiv_stall(CPUState* cpu) { + /* Publish once before comparing; otherwise advance would add the pending + * work a second time on top of a stall computed from a stale clock. */ + psx_cyc_batch_flush(); /* MFLO/MFHI stall to the mult/div completion deadline (Beetle cpu.cpp:1723-1736). * While stalling it CONSUMES a pending load-delay give-back (read_absorb) — each * stalled cycle decrements read_absorb[read_absorb_which] — so cycles that would diff --git a/runtime/src/psx_icache.c b/runtime/src/psx_icache.c index 5ba898a34..d84952e91 100644 --- a/runtime/src/psx_icache.c +++ b/runtime/src/psx_icache.c @@ -11,6 +11,19 @@ #include #include #include +int g_input_instruction_histogram_active; +void (*g_psx_cpu_step_boundary_callback)(CPUState *,uint32_t,uint64_t); +int psx_cpu_step_boundary_enabled(int include_replay) { + return g_psx_cpu_step_boundary_callback != NULL && + (include_replay || !g_ls_replay_active); +} +void psx_cpu_step_boundary_fn(CPUState *cpu,uint32_t address) { + psx_cpu_step_boundary(cpu,address); +} +void (*g_input_instruction_histogram_callback)(uint32_t pc); +void input_instruction_histogram_sample(uint32_t pc) { + if(g_input_instruction_histogram_callback)g_input_instruction_histogram_callback(pc); +} int psx_icache_enabled(void) { static int s = -1; @@ -67,6 +80,18 @@ void psx_icache_reset(void) { for (int i = 0; i < 1024; i++) g_psx_icache_tv[i] = 0x1u; } +void psx_icache_isolated_store(uint32_t addr, uint32_t cache_control) { + /* PSX-SPX Memory Control: IsC + I-cache enable + tag-test mode flushes + * the indexed 16-byte line. The original Octoshock 2.2.2 WriteMemory + * comparison invalidates all four words, independent of store width/value. + * Keep the existing tag representation (bit 1 means invalid). This is a + * timing-tag correction; it does not add cache data execution semantics. */ + if ((cache_control & 0x804u) != 0x804u) return; + if (g_ls_replay_active && s_icache_shadow_state != 2) return; + uint32_t first = (addr & 0xFF0u) >> 2; + for (unsigned i = 0; i < 4; i++) g_psx_icache_tv[first + i] = 0x2u; +} + void psx_icache_fetch_miss(CPUState* cpu, uint32_t addr) { { extern int g_ls_replay_active; /* Ordinary lockstep replay must not perturb the shared cache. The whole- @@ -108,6 +133,8 @@ void psx_icache_fetch_miss(CPUState* cpu, uint32_t addr) { } void psx_icache_fetch(CPUState* cpu, uint32_t addr) { + psx_cpu_step_boundary(cpu,addr); + if (g_input_instruction_histogram_active) input_instruction_histogram_sample(addr); psx_icache_fetch_miss(cpu, addr); } diff --git a/runtime/src/sio.c b/runtime/src/sio.c index 08dcadd09..7788d18a8 100644 --- a/runtime/src/sio.c +++ b/runtime/src/sio.c @@ -488,7 +488,10 @@ static void card_handoff_push(uint8_t kind, uint8_t byte) { * (bit31 / 0xFFFFFFFF) after pop. Merged IRQ7 edges leave depth≥1 so one * pop lands on 0 and skips B4E38 — guest never re-arms bit7 / directory. * Repair = re-edge IRQ7 only (no host B4E* / A6C10 stores). - * Default ON; PSX_APE_CARD_UNSTICK=0 disables. */ + * PSX-COMPAT-001: fixed Ape RAM addresses are not a device invariant. + * Unrelated data in Tekken triggered IRQ7 and changed I_MASK after an + * absent-card probe. Keep this historical repair explicit opt-in only: + * PSX_APE_CARD_UNSTICK=1. It is not enabled by ordinary controller/card use. */ static int s_ape_unstick_env = -1; static int s_ape_unstick_pending = 0; static uint64_t s_ape_unstick_cool_cyc = 0; @@ -497,10 +500,7 @@ static int s_ape_torn_pulses = 0; static int ape_unstick_enabled(void) { if (s_ape_unstick_env < 0) { const char *e = getenv("PSX_APE_CARD_UNSTICK"); - if (e && e[0] == '0') - s_ape_unstick_env = 0; - else - s_ape_unstick_env = 1; + s_ape_unstick_env = (e && e[0] == '1') ? 1 : 0; } return s_ape_unstick_env; } @@ -637,6 +637,11 @@ volatile int g_sio_timing_active = 0; #if SIO_MODEL_CYCLE_PACED #define SIO_BAUD_CYCLES_DEFAULT 1088 #define SIO_ACK_CYCLES_DEFAULT 170 +/* Explicit digital-pad source compatibility, not a hardware/default profile. + * Original 2.2.2 gamepad.cpp requests delay 64; frontio.cpp exposes 32 clocks. */ +static int sio_source_pad_ack; +static int sio_ack_pulse_remaining; +static int sio_pending_ack_timed; static int sio_tick_quantum_cycles = 64; static int sio_shift_active = 0; static uint8_t sio_shift_byte = 0; @@ -934,6 +939,21 @@ static int sio_ack_visible_reads = 0; #define SIO_CTRL_SLOT (1 << 13) void sio_init(void) { +#if SIO_MODEL_CYCLE_PACED + const char *ack_model = getenv("PSX_INPUT_ROUTE_PAD_ACK_MODEL"); + sio_source_pad_ack = ack_model && strcmp(ack_model, "octoshock-2.2.2-digital") == 0; + if (ack_model && *ack_model && !sio_source_pad_ack) { + fprintf(stderr, "Unsupported input route pad ACK model: %s\n", ack_model); + abort(); + } + if (sio_source_pad_ack && getenv("PSX_SIO_PAD_SYNC_RX") && + getenv("PSX_SIO_PAD_SYNC_RX")[0] == '1') { + fprintf(stderr, "Source pad ACK timing requires cycle-paced RX\n"); + abort(); + } + sio_ack_pulse_remaining = 0; + sio_pending_ack_timed = 0; +#endif sio_tx_data = 0; sio_rx_data = 0xFF; sio_stat = SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY; @@ -1415,7 +1435,7 @@ static void pad_process_byte(uint8_t tx_byte) { if (tx_byte == 0x42) { /* Read poll. Analog (or in-config) uses the 8-byte format with the * four stick axes; a plain digital pad uses the 4-byte format. */ - const uint16_t btn = pad_buttons[lp]; + const uint16_t btn = debug_server_update_poll(lp, pad_buttons[lp], pad_analog[lp]); pad_response[0] = cur_id; pad_response[1] = 0x5A; pad_response[2] = (uint8_t)(btn & 0xFF); @@ -2308,7 +2328,12 @@ void sio_write(uint32_t addr, uint32_t value) { sr_record(SR_EVT_CTRL_WRITE, (uint8_t)(value & 0xFF), (uint8_t)((value >> 8) & 0xFF)); if (value & SIO_CTRL_ACK) { sio_stat &= ~SIO_STAT_IRQ; - sio_stat &= ~SIO_STAT_ACK; + /* Original source clears the IRQ latch, not the DSR pulse. Its + * optional level-sensitive reassert-on-clear is disabled. */ +#if SIO_MODEL_CYCLE_PACED + if (!sio_ack_pulse_remaining) +#endif + sio_stat &= ~SIO_STAT_ACK; sio_ack_visible_reads = 0; } if (value & SIO_CTRL_RESET) { @@ -2337,6 +2362,8 @@ void sio_write(uint32_t addr, uint32_t value) { sio_irq_countdown = 0; sio_ack_visible_reads = 0; #if SIO_MODEL_CYCLE_PACED + sio_ack_pulse_remaining = 0; sio_pending_ack_timed = 0; + if (sio_source_pad_ack) sio_stat &= ~SIO_STAT_ACK; sio_shift_active = 0; sio_shift_remaining = 0; sio_tx_buffered = 0; sio_shift_ack_irq_en = 0; sio_tx_buffer_ack_irq_en = 0; @@ -2368,7 +2395,7 @@ void sio_write(uint32_t addr, uint32_t value) { sio_bus_owner = SIO_OWNER_NONE; sio_bus_byte_index = 0; } - g_sio_timing_active = 0; + g_sio_timing_active = sio_ack_pulse_remaining > 0; sio_stat |= SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY; } } @@ -2451,6 +2478,8 @@ void sio_write(uint32_t addr, uint32_t value) { pad_current_cmd = 0; active_device = DEV_NONE; #if SIO_MODEL_CYCLE_PACED + sio_ack_pulse_remaining = 0; sio_pending_ack_timed = 0; + if (sio_source_pad_ack) sio_stat &= ~SIO_STAT_ACK; sio_shift_active = 0; sio_shift_remaining = 0; sio_tx_buffered = 0; sio_shift_ack_irq_en = 0; sio_tx_buffer_ack_irq_en = 0; @@ -2636,7 +2665,11 @@ static void sio_fire_ack_irq(void) { active_device == DEV_MEMCARD); sio_stat |= SIO_STAT_ACK; - sio_ack_visible_reads = 2; + sio_ack_visible_reads = sio_pending_ack_timed ? 0 : 2; + if (sio_pending_ack_timed) { + sio_ack_pulse_remaining = 32; + g_sio_timing_active = 1; + } sr_record(SR_EVT_ACK_FIRE, 0, 0); int irq_enabled = sio_pending_ack_irq_en || ((sio_ctrl & SIO_CTRL_ACK_IRQ_EN) ? 1 : 0); @@ -2689,7 +2722,8 @@ static void sio_fire_ack_irq(void) { sio_irq_idx = (sio_irq_idx + 1) % SIO_IRQ_RING_CAP; sio_irq_seq++; s_pace_ack_fires++; - if (!sio_shift_active && !sio_tx_buffered && !sio_pending_ack) { + if (!sio_shift_active && !sio_tx_buffered && !sio_pending_ack && + !sio_ack_pulse_remaining) { g_sio_timing_active = 0; } } @@ -2709,13 +2743,17 @@ static void sio_handle_shift_complete(void) { sio_irq_pending_source = (active_device == DEV_MEMCARD) ? SIO_IRQ_SRC_CARD_ACK : SIO_IRQ_SRC_PAD_ACK; sio_irq_pending_slot = (uint8_t)selected_slot; - sio_irq_pending_delay = (uint8_t)SIO_ACK_CYCLES_DEFAULT; + int timed_pad = sio_source_pad_ack && active_device == DEV_PAD && + !pad_analog[pad_active_logical] && !sio_multitap_active(); + int ack_delay = timed_pad ? 64 : SIO_ACK_CYCLES_DEFAULT; + sio_irq_pending_delay = (uint8_t)ack_delay; sio_irq_pending_mc_state = (uint8_t)mc_state; sio_irq_pending_byte_seq = sio_trace_seq; if (acked) { sio_pending_ack = 1; - sio_ack_remaining = SIO_ACK_CYCLES_DEFAULT; + sio_ack_remaining = ack_delay; + sio_pending_ack_timed = timed_pad; sio_pending_ack_irq_en = sio_shift_ack_irq_en; } @@ -2734,7 +2772,8 @@ static void sio_handle_shift_complete(void) { } else { sio_stat |= SIO_STAT_TX_RDY | SIO_STAT_TX_EMPTY; } - if (!sio_shift_active && !sio_tx_buffered && !sio_pending_ack) { + if (!sio_shift_active && !sio_tx_buffered && !sio_pending_ack && + !sio_ack_pulse_remaining) { g_sio_timing_active = 0; } } @@ -2765,7 +2804,10 @@ uint64_t sio_get_advance_with_work(void) { return s_sio_advance_with_work; } static void sio_pace_walk(int cycles) { int remaining = cycles; int transitions = 0; - const int MAX_TRANSITIONS = 1; + /* Preserve the legacy card walker. The source-pad experiment consumes + * the whole supplied interval across shift, ACK-on and ACK-off edges. */ + const int MAX_TRANSITIONS = sio_source_pad_ack && + (sio_bus_owner == SIO_OWNER_PAD || sio_ack_pulse_remaining) ? 32 : 1; while (transitions < MAX_TRANSITIONS && (remaining > 0 || (sio_shift_active && sio_shift_remaining <= 0) || @@ -2783,6 +2825,12 @@ static void sio_pace_walk(int cycles) { dt = sio_ack_remaining; next_event = 1; } + if (sio_ack_pulse_remaining > 0 && + (sio_ack_pulse_remaining < dt || + (sio_ack_pulse_remaining == dt && next_event < 0))) { + dt = sio_ack_pulse_remaining; + next_event = 2; + } if (sio_shift_active && sio_shift_remaining <= 0) { dt = 0; next_event = 0; } else if (sio_pending_ack && sio_ack_remaining <= 0) { @@ -2790,6 +2838,10 @@ static void sio_pace_walk(int cycles) { } if (sio_shift_active) sio_shift_remaining -= dt; if (sio_pending_ack) sio_ack_remaining -= dt; + if (sio_ack_pulse_remaining > 0) { + sio_ack_pulse_remaining -= dt; + if (!sio_ack_pulse_remaining) sio_stat &= ~SIO_STAT_ACK; + } remaining -= dt; if (next_event == 0) { sio_handle_shift_complete(); @@ -2798,10 +2850,15 @@ static void sio_pace_walk(int cycles) { sio_pending_ack = 0; sio_fire_ack_irq(); transitions++; + } else if (next_event == 2) { + sio_stat &= ~SIO_STAT_ACK; + transitions++; } else { break; } } + if (sio_source_pad_ack && !sio_shift_active && !sio_tx_buffered && + !sio_pending_ack && !sio_ack_pulse_remaining) g_sio_timing_active = 0; } void sio_advance(uint32_t cycles) { @@ -3137,6 +3194,9 @@ static int sio_snap_parse(PstR *r) { } uint32_t sio_snapshot_bytes(void) { +#if SIO_MODEL_CYCLE_PACED + if (sio_source_pad_ack) return 0; /* explicit cold-boot experiment only */ +#endif PstW w; pst_w_init(&w, NULL, 0); (void)sio_snap_emit(&w); @@ -3146,6 +3206,7 @@ uint32_t sio_snapshot_bytes(void) { void sio_snapshot_write(uint8_t *p) { PstW w; uint32_t n = sio_snapshot_bytes(); + if (!n) return; pst_w_init(&w, p, n); (void)sio_snap_emit(&w); } @@ -3153,6 +3214,7 @@ void sio_snapshot_write(uint8_t *p) { int sio_snapshot_read(const uint8_t *p, uint32_t len) { PstR r; const uint32_t current = sio_snapshot_bytes(); + if (!current) return 0; const uint32_t rumble_bytes = (uint32_t)(sizeof(pad_rumble_map) + sizeof(pad_rumble_small) + sizeof(pad_rumble_large)); diff --git a/runtime/src/source_gpu_runtime.c b/runtime/src/source_gpu_runtime.c new file mode 100644 index 000000000..5c3ecdec3 --- /dev/null +++ b/runtime/src/source_gpu_runtime.c @@ -0,0 +1,128 @@ +#include "source_gpu_runtime.h" +#include "psx_icache.h" +#include "psx_cycles.h" +#include "dma.h" +#include "source_cpu_boundary_probe.h" +#include "source_ram_page_probe.h" +#include +#include +extern uint64_t g_psx_cycle_fast_limit; +static int enabled; +static SourceGPUServiceClock clock_state; +static SourceGPUCommandProjection command_state; +/* GPU Update consumes queued work before it advances raster state. The + * event clock also predicts intermediate deadlines; draw parity must instead + * follow only actual GPU service calls, plus ordinary GP1 register writes. */ +static InputRouteRasterClock draw_raster; +static SourceGPUServiceClock return_clock; +static SourceGPUCommandProjection return_command; +static SourceGPUDispatchSink dispatch_sink; +void source_gpu_runtime_set_dispatch_sink(SourceGPUDispatchSink sink) {dispatch_sink=sink;} +static void dispatch(void) { + if(command_state.dispatch.kind && dispatch_sink) + command_state.budget-=dispatch_sink(&command_state.dispatch); + command_state.dispatch.kind=SOURCE_GPU_DISPATCH_NONE; +} +static void fail(const char *reason) { + fprintf(stderr,"[source-gpu-service] rejected draw mode=%04X (draw-to-display=%u)\n",command_state.draw_mode,(command_state.draw_mode>>10)&1u); + if(command_state.count>=4 && command_state.queue[0]>>24==0x28) { + fprintf(stderr,"[source-gpu-service] rejected first triangle vertices"); + for(unsigned i=1;i<4;i++)fprintf(stderr," %d,%d",source_gpu_command_coord(command_state.queue[i],0)+command_state.offset_x,source_gpu_command_coord(command_state.queue[i],16)+command_state.offset_y); + fputc('\n',stderr); + } + fprintf(stderr,"[source-gpu-service] %s at %llu; budget=%d phase=%u queued=%u front=%08X clip=%d,%d..%d,%d offset=%d,%d mode=%02X mask=%u\n",reason,(unsigned long long)psx_get_cycle_count(),command_state.budget,command_state.phase,command_state.count,command_state.count?command_state.queue[0]:0,command_state.clip_x0,command_state.clip_y0,command_state.clip_x1,command_state.clip_y1,command_state.offset_x,command_state.offset_y,command_state.display_mode,command_state.mask_bits);exit(2); +} +static void service(void *context,uint64_t cycle,unsigned kind) { + (void)context; + if(!source_gpu_command_update(&command_state,cycle))fail("unsupported command service"); + dispatch(); + if(cycleUINT32_MAX)fail("invalid draw raster time"); + input_route_raster_advance(&draw_raster,(uint32_t)(cycle-draw_raster.cycle)); + command_state.skip_field=(draw_raster.y_start+draw_raster.readout_field)&1u; + if(kind==SOURCE_GPU_EVENT_DMA || kind==SOURCE_GPU_EVENT_WRITE) + dma_source_gpu_service_at(cycle); +} +static void cpu_boundary(CPUState *cpu,uint32_t pc,uint64_t cycle) { + for(;;) { + if(clock_state.frame_pending) { + psx_devices_service_to_now(); + if(!source_gpu_service_cpu_boundary(&clock_state,cycle,service,0))fail("invalid CPU boundary"); + return_clock=clock_state;return_command=command_state; + source_cpu_return_probe(cpu,pc,cycle,clock_state.frame_returns); + source_ram_page_probe(clock_state.frame_returns,cycle); + psx_next_service_cycle=0;g_psx_cycle_fast_limit=0; + } + dma_cpu_read_wait_boundary(); + source_cpu_boundary_probe(cpu,pc,cycle); + if(!dma_cpu_source_halted())return; + /* Original RunReal fetches before selecting its halt operation. + * Cache fill therefore overlaps DMA, while only the base/absorb step + * runs. No opcode effects or deferred-load writeback occur here. + * COP2 bypasses the original interrupt/halt dispatch table; allow the + * existing decoder to execute it, with the halt retained for its next + * instruction. This is source compatibility, not PS1 bus arbitration. */ + uint32_t instruction=cpu->read_word(pc); + if((instruction>>26)==0x12u)return; + psx_icache_fetch_miss(cpu,pc); + if(cpu->read_absorb[cpu->read_absorb_which]) + --cpu->read_absorb[cpu->read_absorb_which]; + else psx_advance_cycles(1u); + psx_cyc_batch_flush(); + psx_devices_service_to_now(); + cycle=psx_get_cycle_count(); + } +} + +void source_gpu_runtime_init(void) { + if(enabled || psx_get_cycle_count()!=0 || g_psx_cpu_step_boundary_callback)fail("cold initialization/CPU owner required"); + source_gpu_service_cold(&clock_state);source_gpu_command_cold(&command_state); + input_route_raster_reset(&draw_raster);command_state.field_valid=1; + enabled=1;g_psx_cpu_step_boundary_callback=cpu_boundary; + psx_next_service_cycle=0;g_psx_cycle_fast_limit=0; +} +int source_gpu_runtime_active(void) {return enabled;} +int source_gpu_runtime_ready(void) {return enabled?source_gpu_command_ready(&command_state):-2;} +uint32_t source_gpu_runtime_status_bits(void) { + uint32_t bits=(command_state.dma_direction&2u)?1u<<25:0; + if(!command_state.phase && !command_state.count && command_state.budget>=0)bits|=1u<<26; + if(source_gpu_command_ready(&command_state)>0)bits|=1u<<28; + if(command_state.phase==8)bits|=1u<<27; + return bits; +} +void source_gpu_runtime_advance(void) { + if(enabled && !source_gpu_service_to(&clock_state,psx_cycle_count,service,0))fail("reversed device time"); +} +uint32_t source_gpu_runtime_cycles_to_event(void) { + if(!enabled)return UINT32_MAX; + uint64_t next=source_gpu_service_next(&clock_state); + return next>psx_cycle_count?(uint32_t)(next-psx_cycle_count):1u; +} +void source_gpu_runtime_dma_write(void) { + if(enabled && !source_gpu_service_dma_write(&clock_state,psx_cycle_count,service,0))fail("invalid DMA write time"); +} +void source_gpu_runtime_gp0(uint32_t word) { + if(!enabled)return; + unsigned previous=command_state.first_triangles; + if(!source_gpu_command_write(&command_state,word))fail("unsupported GP0 word"); + dispatch(); + if(!previous && command_state.first_triangles) + fprintf(stderr,"[source-gpu-service] first quad at %llu; budget=%d phase=%u queued=%u clip=%d,%d..%d,%d offset=%d,%d mode=%02X mask=%u\n",(unsigned long long)psx_get_cycle_count(),command_state.budget,command_state.phase,command_state.count,command_state.clip_x0,command_state.clip_y0,command_state.clip_x1,command_state.clip_y1,command_state.offset_x,command_state.offset_y,command_state.display_mode,command_state.mask_bits); +} +void source_gpu_runtime_read(void) { + if(enabled)source_gpu_command_read(&command_state); +} +void source_gpu_runtime_gp1(uint32_t word) { + if(!enabled)return; + source_gpu_runtime_advance(); + if(!source_gpu_command_gp1(&command_state,word) || !input_route_raster_gp1(&clock_state.raster,word) || !input_route_raster_gp1(&draw_raster,word))fail("unsupported GP1 control"); + command_state.skip_field=(draw_raster.y_start+draw_raster.readout_field)&1u; + psx_next_service_cycle=0;g_psx_cycle_fast_limit=0; +} +void source_gpu_runtime_copy(SourceGPUServiceClock *clock,SourceGPUCommandProjection *command) { + if(clock)*clock=clock_state; + if(command)*command=command_state; +} +void source_gpu_runtime_copy_return(SourceGPUServiceClock *clock,SourceGPUCommandProjection *command) { + if(clock)*clock=return_clock; + if(command)*command=return_command; +} diff --git a/runtime/src/spu.c b/runtime/src/spu.c index dc3589550..2d7ef1c92 100644 --- a/runtime/src/spu.c +++ b/runtime/src/spu.c @@ -36,6 +36,9 @@ static uint8_t spu_ram[SPU_RAM_SIZE]; static uint16_t spu_regs[SPU_REG_COUNT]; static uint32_t transfer_addr; static uint32_t key_on_count; +static int source_key_timing; +static uint32_t source_key_on_pending, source_key_off_pending; +static uint8_t source_play_delay[24]; static uint64_t render_frames; static uint64_t nonzero_frames; static int32_t last_peak; @@ -153,6 +156,16 @@ typedef struct { static SpuVoice voices[SPU_VOICE_COUNT]; +/* The retained source profile decodes one four-sample word when fewer than + * eleven samples remain. Its END/loop/envelope readbacks belong to that + * decoder boundary, not to the mixer's whole-block consumption boundary. + * Keep this queue separate from the default renderer's 28-sample blocks. */ +typedef struct { + int16_t samples[32]; + uint8_t read_pos, write_pos, available, shift, filter, ignore_loop; +} SourceSpuDecode; +static SourceSpuDecode source_decode[SPU_VOICE_COUNT]; + static void spu_event_record(uint8_t kind, int voice, uint32_t addr) { SpuEvent *e = &s_events[s_event_idx & (SPU_EVENT_CAP - 1u)]; e->seq = s_event_seq++; @@ -804,9 +817,115 @@ typedef char spu_shadow_voice_count_check[ const void* spu_shadow_tap_buffer(void) { return s_shadow_tap; } int spu_shadow_tap_count(void) { return s_shadow_tap_frame; } +static void source_decode_irq(uint32_t address) { + uint32_t irq_address = (uint32_t)spu_regs[reg_index(0x1F801DA4u)] << 3; + address &= SPU_RAM_SIZE - 1u; + if (irq_address == address || irq_address == (address & ~15u)) + spu_irq_check(irq_address, 1u); +} + +static void source_decode_word(int idx, uint32_t noise_mask) { + SpuVoice *v = &voices[idx]; + SourceSpuDecode *d = &source_decode[idx]; + if (d->available >= 11) { + source_decode_irq(v->cur_addr - 2u); + return; + } + if (!(v->cur_addr & 15u)) { + if (v->flags & 1u) { + v->cur_addr = v->repeat_addr & ~15u; + endx_latch |= 1u << idx; + if (!(v->flags & 2u) && !(noise_mask & (1u << idx))) { + v->env_level = 0; + v->adsr_phase = ADSR_RELEASE; + /* Source END keeps the envelope divider; KEYOFF resets it. */ + } + spu_event_record((v->flags & 2u) ? SPU_EV_END_LOOP : SPU_EV_END_STOP, + idx, v->cur_addr); + } + source_decode_irq(v->cur_addr); + uint8_t header = spu_ram[v->cur_addr]; + v->flags = spu_ram[v->cur_addr + 1u]; + d->shift = header & 15u; + d->filter = header >> 4; + if ((v->flags & 4u) && !d->ignore_loop) { + v->repeat_addr = v->cur_addr; + spu_regs[(uint32_t)idx * 8u + 7u] = (uint16_t)(v->cur_addr >> 3); + } + v->cur_addr = (v->cur_addr + 2u) & (SPU_RAM_SIZE - 1u); + } else { + source_decode_irq(v->cur_addr); + } + uint16_t word = (uint16_t)(spu_ram[v->cur_addr] | + (uint16_t)spu_ram[v->cur_addr + 1u] << 8); + unsigned shift = d->shift; + if (shift > 12u) { shift = 8u; word &= 0x8888u; } + static const int16_t weights[5][2] = {{0,0},{60,0},{115,-52},{98,-55},{122,-60}}; + int w1 = d->filter < 5u ? weights[d->filter][0] : 0; + int w2 = d->filter < 5u ? weights[d->filter][1] : 0; + for (unsigned n = 0; n < 4; ++n) { + int32_t value = (int16_t)((word & 15u) << 12); + value = (value >> shift) + (((int32_t)v->hist1 * w1) >> 6) + + (((int32_t)v->hist2 * w2) >> 6); + int16_t sample = clamp16(value); + d->samples[(d->write_pos + n) & 31u] = sample; + v->hist2 = v->hist1; v->hist1 = sample; + word >>= 4; + } + d->write_pos = (d->write_pos + 4u) & 31u; + d->available += 4; + v->cur_addr = (v->cur_addr + 2u) & (SPU_RAM_SIZE - 1u); +} + +static int16_t source_voice_sample(int idx) { + SpuVoice *v = &voices[idx]; + SourceSpuDecode *d = &source_decode[idx]; + uint32_t noise_mask = (uint32_t)spu_regs[reg_index(0x1F801D94u)] | + (uint32_t)spu_regs[reg_index(0x1F801D96u)] << 16; + source_decode_word(idx, noise_mask); + unsigned gi = (v->phase >> 4) & 255u; + int32_t raw = noise_mask & (1u << idx) ? (int16_t)noise_lfsr : + ((int32_t)d->samples[d->read_pos] * spu_gauss_table[255u - gi] + + (int32_t)d->samples[(d->read_pos + 1u) & 31u] * spu_gauss_table[511u - gi] + + (int32_t)d->samples[(d->read_pos + 2u) & 31u] * spu_gauss_table[256u + gi] + + (int32_t)d->samples[(d->read_pos + 3u) & 31u] * spu_gauss_table[gi]) >> 15; + int32_t shaped = (raw * (int16_t)v->env_level) >> 15; + /* Every voice is visited in ascending order within one sample, including + * silent/disabled voices. This value is only used by the following voice. */ + static int32_t previous_voice; + if (!source_play_delay[idx]) { + adsr_run(idx, v); + uint32_t pitch = voice_reg(idx, 2); + uint32_t pmon = (uint32_t)spu_regs[reg_index(0x1F801D90u)] | + (uint32_t)spu_regs[reg_index(0x1F801D92u)] << 16; + if (idx && (pmon & (1u << idx))) + pitch += ((int16_t)pitch * previous_voice) >> 15; + if (pitch > 0x3FFFu) pitch = 0x3FFFu; + uint32_t phase = v->phase + pitch; + unsigned consumed = phase >> 12; + v->phase = phase & 4095u; + d->available -= consumed; + d->read_pos = (d->read_pos + consumed) & 31u; + } else source_play_delay[idx]--; + previous_voice = shaped; + v->sample_idx = d->read_pos; /* diagnostic position in the source ring */ + return (int16_t)shaped; +} + static int16_t voice_next_sample(int idx) { + if (source_key_timing) return source_voice_sample(idx); SpuVoice *v = &voices[idx]; - if (!v->active) return 0; + if (!v->active) { + /* The retained source has no inactive-envelope shortcut. Cold + * voices start in Attack; a cancelled key-on still leaves their + * envelope clock running. Keep this readback behavior independent + * of our silent-voice decoder optimization. */ + if (source_key_timing) { + if (source_play_delay[idx]) source_play_delay[idx]--; + else adsr_run(idx, v); + } + return 0; + } if (v->sample_idx >= SPU_BLOCK_SAMPLES) { if (v->flags & 0x01u) { @@ -841,6 +960,13 @@ static int16_t voice_next_sample(int idx) { decode_block(v); } + /* Original source key-on is sample-applied, followed by four samples + * without envelope or pitch advancement. Keep ordinary decoder work. */ + if (source_key_timing && source_play_delay[idx]) { + source_play_delay[idx]--; + return 0; + } + /* Noise mode (NON bit set): the voice outputs the live noise LFSR value * INSTEAD of its interpolated ADPCM sample. Everything else — block * decoding/address advance, ENDX latching, pitch stepping, the ADSR @@ -926,6 +1052,13 @@ static void key_on(uint32_t mask) { v->env_level = 0; v->adsr_divider = 0; v->adsr_phase = ADSR_ATTACK; + if (source_key_timing) { + SourceSpuDecode *d = &source_decode[i]; + d->read_pos = d->write_pos = d->available = 0; + d->ignore_loop = 0; + v->repeat_addr = (uint32_t)voice_reg(i, 7) << 3; + /* The source retains decoded sample contents across KEYON. */ + } key_on_count++; endx_latch &= ~(1u << i); /* KEYON clears ENDX bit on real hw */ spu_event_record(SPU_EV_KEYON, i, v->cur_addr); @@ -939,7 +1072,8 @@ static void key_on(uint32_t mask) { static void key_off(uint32_t mask) { for (int i = 0; i < SPU_VOICE_COUNT; i++) { if (!(mask & (1u << i))) continue; - if (!voices[i].active) continue; + if (!voices[i].active && !source_key_timing) continue; + if (source_key_timing && voices[i].adsr_phase == ADSR_RELEASE) continue; spu_event_record(SPU_EV_KEYOFF, i, voices[i].cur_addr); voices[i].adsr_phase = ADSR_RELEASE; voices[i].adsr_divider = 0; @@ -947,7 +1081,27 @@ static void key_off(uint32_t mask) { } } +static void source_apply_keys(int enabled) { + key_off(source_key_off_pending); + key_on(source_key_on_pending); + for (int i = 0; i < SPU_VOICE_COUNT; ++i) { + if (source_key_on_pending & (1u << i)) source_play_delay[i] = 4; + if (!enabled) { + voices[i].adsr_phase = ADSR_RELEASE; + voices[i].env_level = 0; + } + } + source_key_on_pending = source_key_off_pending = 0; +} + void spu_init(void) { + const char *model = getenv("PSX_GPU_DMA_MODEL"); + source_key_timing = model && + (!strcmp(model, "octoshock-2.2.2-bounded-linked-list") || + !strcmp(model, "octoshock-2.2.2-bounded-quad")); + source_key_on_pending = source_key_off_pending = 0; + memset(source_play_delay, 0, sizeof(source_play_delay)); + memset(source_decode, 0, sizeof(source_decode)); memset(spu_ram, 0, sizeof(spu_ram)); memset(spu_regs, 0, sizeof(spu_regs)); memset(voices, 0, sizeof(voices)); @@ -987,6 +1141,11 @@ void spu_render(int16_t* out_stereo, int frames) { if (!out_stereo || frames <= 0) return; uint16_t ctrl = spu_regs[reg_index(0x1F801DAAu)]; + /* SPUSTAT reports the applied low control bits, not the pending write. + * Apply at the existing output-sample boundary. This is the SPU model's + * sample granularity; it does not assert a measured hardware subphase. + * The register image already participates in snapshot save/restore. */ + spu_regs[reg_index(0x1F801DAEu)] = ctrl & 0x3Fu; int enabled = (ctrl & 0x8000u) != 0; int cd_on = (ctrl & 0x0001u) != 0; int cd_rev = cd_on && (ctrl & 0x0004u) != 0; /* CD reverb send needs CD enable */ @@ -1003,6 +1162,9 @@ void spu_render(int16_t* out_stereo, int frames) { } } + if (source_key_timing) + any_voice = 1; /* source envelopes clock even without an active voice */ + /* Shadow tap: arm recording for this block if the float SPU shadow is on. * Off by default => s_shadow_tap_on stays 0 and the mix loop is unchanged * and byte-identical to upstream. */ @@ -1131,7 +1293,7 @@ void spu_render(int16_t* out_stereo, int frames) { int16_t main_l = chan_volume(spu_regs[reg_index(0x1F801D80u)], &sweep_main_env[0]); int16_t main_r = chan_volume(spu_regs[reg_index(0x1F801D82u)], &sweep_main_env[1]); - if (enabled) { + if (enabled || source_key_timing) { int32_t voice_l = 0; int32_t voice_r = 0; int32_t rev_send_l = 0; @@ -1177,6 +1339,11 @@ void spu_render(int16_t* out_stereo, int frames) { } } } + /* The retained source clocks capture/decoder state even with + * SPU enable clear. Its separate mute bit gates the voice mix + * and reverb sends, after voice capture and before CD mixing. */ + if (source_key_timing && !(ctrl & 0x4000u)) + voice_l = voice_r = rev_send_l = rev_send_r = 0; mix_l = voice_l; mix_r = voice_r; if (voice_sum_pos < voice_sum_cap) { @@ -1267,6 +1434,11 @@ void spu_render(int16_t* out_stereo, int frames) { mix_r = ((int32_t)mix_r * main_r) >> 15; } + if (source_key_timing) { + /* Disable forces Release/zero after each source sample; its + * divider and startup delay still clock before that reset. */ + source_apply_keys(enabled); + } out_stereo[f * 2 + 0] = clamp16(mix_l); out_stereo[f * 2 + 1] = clamp16(mix_r); int32_t frame_peak = abs32(out_stereo[f * 2 + 0]); @@ -1343,8 +1515,8 @@ uint32_t spu_read(uint32_t addr) { uint32_t idx = reg_index(addr); if (idx < SPU_REG_COUNT) { if (addr == 0x1F801DAEu) { - /* SPUSTAT (psx-spx): bits 5-0 mirror SPUCNT bits 5-0 (the - * current SPU mode), bit 6 is the IRQ flag (cleared by + /* SPUSTAT (psx-spx): bits 5-0 mirror the applied SPUCNT + * mode at the sample boundary, bit 6 is the IRQ flag (cleared by * writing SPUCNT with bit 6 clear), bit 7 follows SPUCNT.5 * (DMA r/w request), bit 10 is the data-transfer busy flag — * 0 here because this runtime completes FIFO/DMA transfers @@ -1355,7 +1527,7 @@ uint32_t spu_read(uint32_t addr) { * "currently writing the SECOND half of the capture * buffers" (capture offset >= 0x200). */ uint16_t cnt = spu_regs[reg_index(0x1F801DAAu)]; - uint32_t st = (uint32_t)((cnt & 0x3Fu) | (((cnt >> 5) & 1u) << 7)); + uint32_t st = (uint32_t)((spu_regs[idx] & 0x3Fu) | (((cnt >> 5) & 1u) << 7)); if (irq_flag) st |= 0x40u; if (capture_pos & 0x200u) st |= 0x800u; return st; @@ -1439,6 +1611,12 @@ void spu_write(uint32_t addr, uint32_t value) { audio_trace_event(AUDIO_EV_REG_WRITE, addr, value & 0xFFFFu); spu_regs[idx] = (uint16_t)value; + /* Source voice register 6 writes the live envelope, without + * resetting its phase or divider (PS_SPU::Write, case 0x0C). */ + if (source_key_timing && idx < (uint32_t)SPU_VOICE_COUNT * 8u && + (idx & 7u) == 6u) + voices[idx >> 3].env_level = (uint16_t)value; + /* Voice repeat/loop address (voice reg 7) is LIVE state on real * hardware: writing it after KEYON retargets where the next * END+REPEAT block jumps (Beetle spu.cpp:1150/333). X5's driver @@ -1452,6 +1630,10 @@ void spu_write(uint32_t addr, uint32_t value) { /* bit0 ignored (16-byte alignment) — same masking as KEYON. */ voices[v].repeat_addr = ((uint32_t)((uint16_t)value & ~1u) << 3) & (SPU_RAM_SIZE - 1u); + if (source_key_timing) { + voices[v].repeat_addr = ((uint32_t)(uint16_t)value << 3) & (SPU_RAM_SIZE - 1u); + source_decode[v].ignore_loop = 1; + } } /* Volume registers feed the sweep envelopes: a direct write @@ -1484,19 +1666,27 @@ void spu_write(uint32_t addr, uint32_t value) { if (addr == 0x1F801D88u) { kon_latch = (kon_latch & 0xFFFF0000u) | (uint32_t)(uint16_t)value; - key_on((uint32_t)(uint16_t)value); + if (source_key_timing) + source_key_on_pending = (source_key_on_pending & 0xFFFF0000u) | (uint16_t)value; + else key_on((uint32_t)(uint16_t)value); } if (addr == 0x1F801D8Au) { kon_latch = (kon_latch & 0x0000FFFFu) | ((uint32_t)(uint16_t)value << 16); - key_on((uint32_t)(uint16_t)value << 16); + if (source_key_timing) + source_key_on_pending = (source_key_on_pending & 0x0000FFFFu) | ((uint32_t)(value & 0xFFu) << 16); + else key_on((uint32_t)(uint16_t)value << 16); } if (addr == 0x1F801D8Cu) { koff_latch = (koff_latch & 0xFFFF0000u) | (uint32_t)(uint16_t)value; - key_off((uint32_t)(uint16_t)value); + if (source_key_timing) + source_key_off_pending = (source_key_off_pending & 0xFFFF0000u) | (uint16_t)value; + else key_off((uint32_t)(uint16_t)value); } if (addr == 0x1F801D8Eu) { koff_latch = (koff_latch & 0x0000FFFFu) | ((uint32_t)(uint16_t)value << 16); - key_off((uint32_t)(uint16_t)value << 16); + if (source_key_timing) + source_key_off_pending = (source_key_off_pending & 0x0000FFFFu) | ((uint32_t)(value & 0xFFu) << 16); + else key_off((uint32_t)(uint16_t)value << 16); } if (addr == 0x1F801DA6u) { @@ -1722,9 +1912,13 @@ static int spu_r_voice(PstR *r, int idx) { #define SPU_SNAPSHOT_TAIL_BYTES \ (20u + 1u + 2u + 4u + 4u + 1u + 4u + 4u + 4u + 4u + 4u + 2u * (2u + 4u)) +/* SPK2 extends the source-only footer with each decoder's complete queue. */ +#define SOURCE_SPU_TAIL_BYTES (36u + SPU_VOICE_COUNT * (64u + 6u)) + uint32_t spu_snapshot_bytes(void) { return (uint32_t)(SPU_REG_COUNT * 2u) + - (SPU_VOICE_COUNT * SPU_VOICE_WIRE_BYTES) + SPU_SNAPSHOT_TAIL_BYTES; + (SPU_VOICE_COUNT * SPU_VOICE_WIRE_BYTES) + SPU_SNAPSHOT_TAIL_BYTES + + (source_key_timing ? SOURCE_SPU_TAIL_BYTES : 0u); } void spu_snapshot_write(uint8_t *p) { @@ -1754,6 +1948,19 @@ void spu_snapshot_write(uint8_t *p) { pst_w_i16(&w, sweep_main_env[ch].level); pst_w_u32(&w, sweep_main_env[ch].divider); } + if (source_key_timing) { + pst_w_u32(&w, 0x324B5053u); /* SPK2: source keys and decoder queue */ + pst_w_u32(&w, source_key_on_pending); + pst_w_u32(&w, source_key_off_pending); + for (int i = 0; i < SPU_VOICE_COUNT; ++i) pst_w_u8(&w, source_play_delay[i]); + for (int i = 0; i < SPU_VOICE_COUNT; ++i) { + const SourceSpuDecode *d = &source_decode[i]; + for (int j = 0; j < 32; ++j) pst_w_i16(&w, d->samples[j]); + pst_w_u8(&w, d->read_pos); pst_w_u8(&w, d->write_pos); + pst_w_u8(&w, d->available); pst_w_u8(&w, d->shift); + pst_w_u8(&w, d->filter); pst_w_u8(&w, d->ignore_loop); + } + } } int spu_snapshot_read(const uint8_t *p, uint32_t len) { @@ -1780,6 +1987,25 @@ int spu_snapshot_read(const uint8_t *p, uint32_t len) { !pst_r_u32(&r, &sweep_main_env[ch].divider)) return 0; } + if (source_key_timing) { + uint32_t magic; + if (!pst_r_u32(&r, &magic) || magic != 0x324B5053u || + !pst_r_u32(&r, &source_key_on_pending) || + !pst_r_u32(&r, &source_key_off_pending)) return 0; + for (int i = 0; i < SPU_VOICE_COUNT; ++i) + if (!pst_r_u8(&r, &source_play_delay[i]) || source_play_delay[i] > 4) return 0; + for (int i = 0; i < SPU_VOICE_COUNT; ++i) { + SourceSpuDecode *d = &source_decode[i]; + for (int j = 0; j < 32; ++j) + if (!pst_r_i16(&r, &d->samples[j])) return 0; + if (!pst_r_u8(&r, &d->read_pos) || d->read_pos > 31 || + !pst_r_u8(&r, &d->write_pos) || d->write_pos > 31 || + !pst_r_u8(&r, &d->available) || d->available > 14 || + !pst_r_u8(&r, &d->shift) || d->shift > 15 || + !pst_r_u8(&r, &d->filter) || d->filter > 15 || + !pst_r_u8(&r, &d->ignore_loop) || d->ignore_loop > 1) return 0; + } + } return 1; } uint8_t* spu_get_ram_ptr(void){ return spu_ram; } @@ -1810,7 +2036,8 @@ void spu_snapshot_part_digests(SpuSnapPartDigests *out) spu_snapshot_write(buf); regs_n = (uint32_t)(SPU_REG_COUNT * 2u); voices_n = (uint32_t)(SPU_VOICE_COUNT * SPU_VOICE_WIRE_BYTES); - if (regs_n + voices_n + SPU_SNAPSHOT_TAIL_BYTES != n) + uint32_t tail_n = SPU_SNAPSHOT_TAIL_BYTES + (source_key_timing ? SOURCE_SPU_TAIL_BYTES : 0u); + if (regs_n + voices_n + tail_n != n) return; crc = 0xFFFFFFFFu; crc = crc32_update(crc, buf, regs_n); @@ -1819,6 +2046,6 @@ void spu_snapshot_part_digests(SpuSnapPartDigests *out) crc = crc32_update(crc, buf + regs_n, voices_n); out->voices = crc ^ 0xFFFFFFFFu; crc = 0xFFFFFFFFu; - crc = crc32_update(crc, buf + regs_n + voices_n, SPU_SNAPSHOT_TAIL_BYTES); + crc = crc32_update(crc, buf + regs_n + voices_n, tail_n); out->tail = crc ^ 0xFFFFFFFFu; } diff --git a/runtime/src/timers.c b/runtime/src/timers.c index ef1599589..0d2ac01e7 100644 --- a/runtime/src/timers.c +++ b/runtime/src/timers.c @@ -15,6 +15,10 @@ */ #include "timers.h" +#include "timer1_source_clock.h" +#include "timer2_source_clock.h" +#include +#include #include "event_ring.h" #include @@ -47,6 +51,34 @@ typedef struct { } Timer; static Timer timers[3]; +static PsxTimer1Source source_timer1; +static int source_timer1_enabled; +static uint64_t source_timer1_cycle; +static PsxTimer2Source source_timer2; +static int source_timer2_enabled; +static uint32_t source_timer2_elapsed,source_timer2_deadline; +static void source_timer2_flush(void); +int timers_source_raster_enabled(void) { return source_timer1_enabled; } +int timers_source_hblank_counter_read(uint32_t addr) { + return source_timer1_enabled && (addr&~3u)==0x1f801110u && + (source_timer1.mode&0x100u); +} +void timers_source_raster_finish(uint64_t cycle) { + if(!source_timer1_enabled) return; + if(cycleUINT32_MAX) { + fprintf(stderr,"[timer1-source-clock] invalid ordered clock interval\n");exit(4); + } + timer1_source_cpu(&source_timer1,(uint32_t)(cycle-source_timer1_cycle)); + source_timer1_cycle=cycle; +} +void timers_source_raster_event(void *context,uint64_t cycle,unsigned event,int blank) { + (void)context; + if(!source_timer1_enabled) return; + timers_source_raster_finish(cycle); + if(event==1) timer1_source_hblank(&source_timer1,1); + else if(event==2) timer1_source_blank(&source_timer1,blank); +} + static uint32_t timer_frac[3]; /* Always-on RootCounter/Timer IRQ-fire telemetry (Tomba 2 RCnt-wait diag): @@ -71,6 +103,28 @@ extern uint32_t i_stat; extern void psx_irq_raise(uint32_t bit, uint32_t detail); void timers_init(void) { + const char *model=getenv("PSX_TIMER1_MODEL"); + source_timer1_enabled=model && *model; + if(source_timer1_enabled) { + const char *field=getenv("PSX_INPUT_ROUTE_FIELD_MODEL"); + if(strcmp(model,"octoshock-2.2.2") || !getenv("PSX_INPUT_ROUTE_FILE") || + !field || strcmp(field,"octoshock-2.2.2-ntsc-raster")) { + fprintf(stderr,"[timer1-source-clock] requires named model, file route and NTSC raster clock\n");exit(4); + } + fprintf(stderr,"[timer1-source-clock] experimental original source timer1; IRQ-enabled modes and restore unsupported; timer2 has a separate option\n"); + } + timer1_source_reset(&source_timer1);source_timer1_cycle=0; + model=getenv("PSX_TIMER2_MODEL");source_timer2_enabled=model && *model; + if(source_timer2_enabled) { +#ifndef PSX_ENABLE_BLOCK_CYCLES + fprintf(stderr,"[timer2-source-clock] block-cycle execution required\n");exit(4); +#endif + if(strcmp(model,"octoshock-2.2.2") || !source_timer1_enabled) { + fprintf(stderr,"[timer2-source-clock] requires named model and source timer1 profile\n");exit(4); + } + fprintf(stderr,"[timer2-source-clock] experimental original source timer2; timer0 IRQ modes and restore unsupported\n"); + } + timer2_source_reset(&source_timer2);source_timer2_elapsed=0;source_timer2_deadline=1024; memset(timers, 0, sizeof(timers)); memset(timer_frac, 0, sizeof(timer_frac)); } @@ -85,11 +139,24 @@ void timers_get_snapshot(uint16_t counter[3], uint32_t mode[3], irq_line[i] = timers[i].irq_line; frac[i] = timer_frac[i]; } + if(source_timer1_enabled) { + counter[1]=(uint16_t)source_timer1.counter; mode[1]=source_timer1.mode; + target[1]=(uint16_t)source_timer1.target; irq_line[1]=0; frac[1]=0; + } + if(source_timer2_enabled) { + PsxTimer2Source view=source_timer2; + (void)timer2_source_cpu(&view,source_timer2_elapsed); + counter[2]=(uint16_t)view.counter;mode[2]=view.mode;target[2]=(uint16_t)view.target; + irq_line[2]=0;frac[2]=view.divider; + } } void timers_set_snapshot(const uint16_t counter[3], const uint32_t mode[3], const uint16_t target[3], const int32_t irq_line[3], const uint32_t frac[3]) { + if(source_timer1_enabled || source_timer2_enabled) { + fprintf(stderr,"[timer1-source-clock] cold boot only; restore unsupported\n");exit(4); + } for (int i = 0; i < 3; i++) { timers[i].counter = counter[i]; timers[i].mode = mode[i]; @@ -99,6 +166,18 @@ void timers_set_snapshot(const uint16_t counter[3], const uint32_t mode[3], } } +static void source_timer2_pulses(unsigned pulses) { + while(pulses--) { + g_timer_irq_fired[2]++;psx_irq_raise(IRQ_TIMER2,2); + event_ring_record_aux(EV_DEQ,(uint8_t)(SRC_TIMER0+2),source_timer2.counter); + } +} +static void source_timer2_flush(void) { + if(!source_timer2_enabled)return; + source_timer2_pulses(timer2_source_cpu(&source_timer2,source_timer2_elapsed)); + source_timer2_elapsed=0;source_timer2_deadline=timer2_source_next(&source_timer2); +} + /* Determine whether this timer uses system clock ticks */ static int timer_uses_sysclk(int t) { int src = (timers[t].mode >> 8) & 3; @@ -203,6 +282,17 @@ void timers_advance(uint32_t cycles) { if (cycles == 0) return; for (int t = 0; t < 3; t++) { + if(t==1 && source_timer1_enabled) continue; /* supplied by raster events */ + if(t==2 && source_timer2_enabled) { + /* Only source timer deadlines and timer MMIO update this model. + * Unrelated device service boundaries must not reset its cadence. */ + if(cycles>source_timer2_deadline-source_timer2_elapsed) { + fprintf(stderr,"[timer2-source-clock] scheduler crossed timer deadline\n");exit(4); + } + source_timer2_elapsed+=cycles; + if(source_timer2_elapsed==source_timer2_deadline)source_timer2_flush(); + continue; + } int src = (timers[t].mode >> 8) & 3; if (t == 2 && (src == 2 || src == 3)) { timer_advance_divided(t, cycles, 8); @@ -240,6 +330,12 @@ static uint32_t timer_divisor(int t) { uint32_t timers_cycles_to_irq(uint32_t i_mask) { uint32_t best = 0xFFFFFFFFu; for (int t = 0; t < 3; t++) { + if(t==1 && source_timer1_enabled) continue; /* IRQ modes rejected on write */ + if(t==2 && source_timer2_enabled) { + uint32_t next=source_timer2_deadline-source_timer2_elapsed; + if(nextmode & MODE_IRQ_TARGET) != 0; @@ -275,6 +371,9 @@ uint32_t timers_read(uint32_t addr) { int reg = (addr - TIMER_BASE) & 0x0F; if (timer < 0 || timer > 2) return 0; + source_timer2_flush(); /* Original TIMER_Read updates every timer first. */ + if(timer==2 && source_timer2_enabled)return timer2_source_read(&source_timer2,(unsigned)reg); + if(timer==1 && source_timer1_enabled) return timer1_source_read(&source_timer1,(unsigned)reg); switch (reg) { case 0x00: { @@ -313,6 +412,28 @@ void timers_write(uint32_t addr, uint32_t value) { int reg = (addr - TIMER_BASE) & 0x0F; if (timer < 0 || timer > 2) return; + source_timer2_flush(); /* Original TIMER_Write updates every timer first. */ + if(source_timer2_enabled) { + /* The MMIO wrapper synchronized against the OLD timer configuration. + * Its cached absolute deadline cannot survive a guest reprogramming + * the timer to an earlier event. The next CPU charge must recompute + * both scheduler paths; no guest clocks or counter values are added. */ + extern uint64_t psx_next_service_cycle,g_psx_cycle_fast_limit; + psx_next_service_cycle=0;g_psx_cycle_fast_limit=0; + } + if(source_timer2_enabled && timer==0 && reg==4 && (value&0x30)) { + fprintf(stderr,"[timer2-source-clock] timer0 IRQ modes unsupported in combined source profile\n");exit(4); + } + if(timer==2 && source_timer2_enabled) { + source_timer2_pulses(timer2_source_write(&source_timer2,(unsigned)reg,(uint16_t)value)); + source_timer2_deadline=timer2_source_next(&source_timer2);return; + } + if(timer==1 && source_timer1_enabled) { + if(!timer1_source_write(&source_timer1,(unsigned)reg,(uint16_t)value)) { + fprintf(stderr,"[timer1-source-clock] IRQ-enabled mode 0x%04X unsupported\n",(unsigned)value);exit(4); + } + return; + } switch (reg) { case 0x00: diff --git a/runtime/src/traps.c b/runtime/src/traps.c index d6ea41819..9af780e69 100644 --- a/runtime/src/traps.c +++ b/runtime/src/traps.c @@ -18,6 +18,7 @@ #include "savestate.h" /* scheduler-top coherent disk save/load boundary */ #include "parity_trace.h" /* general two-process control-flow parity ring */ #include "pst_wire.h" +#include "source_gpu_runtime.h" /* RAM reader adapter for the parity trace (cpu->read_word takes only addr). */ static uint32_t traps_parity_rw(void* ctx, uint32_t addr) { @@ -1028,6 +1029,21 @@ void psx_scheduler_run(CPUState* cpu) } } +/* Explicit research control: execute the installed guest SYS01/02 handler. + * This does not select a BIOS image, scheduler, delay, or title-specific PC. + * The old direct mode remains the default until the exception route has wider + * validation. Non-delay-slot entry is the existing psx_syscall API contract. + * Nested synchronous IRQ windows need a separate RFE owner stack; fail closed + * rather than let an inner RFE unwind the outer handler's native stack. */ +static int critical_exception_enabled(void) { + static int mode = -1; + if (mode < 0) { + const char* value = getenv("PSX_CRITICAL_SECTION_MODEL"); + mode = value && strcmp(value, "exception") == 0; + } + return mode; +} + int psx_syscall(CPUState* cpu, uint32_t code) { /* * PS1 BIOS SYSCALL convention: @@ -1046,6 +1062,24 @@ int psx_syscall(CPUState* cpu, uint32_t code) { uint32_t func = cpu->gpr[4]; /* $a0 = syscall function number */ uint32_t sr = cpu->cop0[12]; + if ((func == 1 || func == 2) && critical_exception_enabled()) { + if (psx_get_in_exception()) { + fprintf(stderr, "SYS01/02 exception model: nested synchronous IRQ entry unsupported at %08X\n", cpu->pc); + exit(1); + } + /* Return the vector to the flat dispatcher. Calling psx_dispatch here + * would create a host continuation that could outlive the guest RFE. + * Guest code owns result registers, TCB stores, and EPC advancement. */ + cpu->cop0[14] = cpu->pc; + /* Source exception entry retains only pending interrupt bits. A + * non-delay SYSCALL has no CE, BD, or BT bits of its own. */ + cpu->cop0[13] = (cpu->cop0[13] & (source_gpu_runtime_active() + ? 0x0000FF00u : ~(0x80000000u | 0x7Cu))) | (8u << 2); + cpu->cop0[12] = (sr & ~0x3Fu) | ((sr & 0x0Fu) << 2); + cpu->pc = (sr & 0x00400000u) ? 0xBFC00180u : 0x80000080u; + return 1; + } + switch (func) { case 1: /* EnterCriticalSection: disable interrupts */ cpu->cop0[12] = sr & ~1u; /* clear IEc (bit 0) */ diff --git a/runtime/tests/test_bios_alias_admission.c b/runtime/tests/test_bios_alias_admission.c new file mode 100644 index 000000000..a58c92134 --- /dev/null +++ b/runtime/tests/test_bios_alias_admission.c @@ -0,0 +1,40 @@ +#include +#include +#include +#include "cpu_state.h" +#include "psx_bios_backend.h" +extern const PsxBiosBackend Test_psx_bios_backend; +#include "psx_icache.h" +#include "psx_cycles.h" +extern unsigned char* memory_get_ram_ptr(void); +extern void dirty_ram_mark_executable_range(uint32_t,uint32_t); +extern void memory_set_sr_ptr(const uint32_t*); +extern uint32_t psx_read_word(uint32_t); +extern uint16_t psx_read_half(uint32_t); +extern uint8_t psx_read_byte(uint32_t); +extern void psx_write_word(uint32_t,uint32_t); +extern void psx_write_half(uint32_t,uint16_t); +extern void psx_write_byte(uint32_t,uint8_t); +extern void Test_psx_dispatch_call(CPUState*,uint32_t,uint32_t); +extern uint64_t g_dispatch_static_hits; +extern CPUState *debug_cpu_ptr; +extern int g_input_instruction_histogram_active; +static uint32_t fetches[256]; static unsigned nf; +void __wrap_input_instruction_histogram_sample(uint32_t pc) { if(nf<256)fetches[nf++]=pc; } +int main(int argc,char**argv) { + if(argc!=5)return 2; + uint32_t alias=(uint32_t)strtoul(argv[1],0,16), entry=(uint32_t)strtoul(argv[2],0,16), stop=0x1000; + FILE*f=fopen(argv[3],"rb"); if(!f)return 3; + fread(memory_get_ram_ptr()+0x500,1,0x130,f);fclose(f);if(atoi(argv[4]))dirty_ram_mark_executable_range(0x500,0x130); + CPUState cpu={0};cpu.read_word=psx_read_word;cpu.read_half=psx_read_half;cpu.read_byte=psx_read_byte; + cpu.write_word=psx_write_word;cpu.write_half=psx_write_half;cpu.write_byte=psx_write_byte; + cpu.read_fudge=32;cpu.ld_which_t=32;cpu.gpr[29]=0x1ff000;cpu.gpr[31]=0x1000;cpu.gpr[17]=0x1000;cpu.gpr[8]=99;cpu.gpr[4]=1; + if(entry==0xa0){memcpy(memory_get_ram_ptr()+0xa0,memory_get_ram_ptr()+0x500,16);if(atoi(argv[4]))dirty_ram_mark_executable_range(0xa0,16);} + if(entry==0x500 || entry==0xa0)stop=0x598; + if(entry==0x5c0)stop=0x80000080; + psx_bios_activate(&Test_psx_bios_backend);debug_cpu_ptr=&cpu;memory_set_sr_ptr(&cpu.cop0[12]); + psx_icache_reset();g_psx_icache_active=1;g_input_instruction_histogram_active=1;g_input_instruction_histogram_callback=__wrap_input_instruction_histogram_sample;psx_next_service_cycle=1000000; + Test_psx_dispatch_call(&cpu,alias|entry,stop);psx_cyc_batch_flush(); + printf("{\"alias\":%u,\"entry\":%u,\"cycles\":%llu,\"static_hits\":%llu,\"s0\":%u,\"v0\":%u,\"t0\":%u,\"epc\":%u,\"cause\":%u,\"pc\":%u,\"fetches\":[",alias,entry,(unsigned long long)psx_cycle_count,(unsigned long long)g_dispatch_static_hits,cpu.gpr[16],cpu.gpr[2],cpu.gpr[8],cpu.cop0[14],cpu.cop0[13],cpu.pc); + for(unsigned i=0;i +#include +#include "cpu_state.h" +#include "psx_bios_backend.h" +#include "psx_icache.h" +#include "psx_cycles.h" +extern const PsxBiosBackend Test_psx_bios_backend; +extern void Test_psx_dispatch_call(CPUState*,uint32_t,uint32_t); +extern uint32_t psx_read_word(uint32_t); +extern uint16_t psx_read_half(uint32_t); +extern uint8_t psx_read_byte(uint32_t); +extern void psx_write_word(uint32_t,uint32_t); +extern void psx_write_half(uint32_t,uint16_t); +extern void psx_write_byte(uint32_t,uint8_t); +extern void memory_set_sr_ptr(const uint32_t*); +extern CPUState *debug_cpu_ptr; +extern uint64_t g_dispatch_static_hits; +extern int g_input_instruction_histogram_active; +static unsigned char code[0x48]; +static uint32_t fetches[32]; static unsigned nf; +static void sample(uint32_t pc) { if(nf<32) fetches[nf++]=pc; } +void __wrap_input_instruction_histogram_sample(uint32_t pc) { sample(pc); } +static uint32_t read_word(uint32_t addr) { + uint32_t phys=addr&0x1fffffff,value; + if(phys>=0x1fc00000 && phys+4<=0x1fc00000+sizeof(code)) { + memcpy(&value,code+(phys-0x1fc00000),4);return value; + } + return psx_read_word(addr); +} +int main(int argc,char**argv) { + if(argc!=3)return 2; + FILE*f=fopen(argv[1],"rb");if(!f)return 3; + if(fread(code,1,sizeof(code),f)!=sizeof(code)){fclose(f);return 4;}fclose(f); + CPUState cpu={0};cpu.read_word=read_word;cpu.read_half=psx_read_half;cpu.read_byte=psx_read_byte; + cpu.write_word=psx_write_word;cpu.write_half=psx_write_half;cpu.write_byte=psx_write_byte; + cpu.read_fudge=32;cpu.ld_which_t=32;cpu.gpr[29]=0x1ff000;cpu.gpr[31]=0x1000;cpu.gpr[16]=0x1000;cpu.gpr[4]=1; + psx_bios_activate(&Test_psx_bios_backend);debug_cpu_ptr=&cpu;memory_set_sr_ptr(&cpu.cop0[12]); + psx_icache_reset();g_psx_icache_active=1;g_input_instruction_histogram_active=1;g_input_instruction_histogram_callback=sample;psx_next_service_cycle=1000000; + uint64_t entry_cycles=0;uint32_t resumed_pc=0; + if(!strcmp(argv[2],"sys")) { + Test_psx_dispatch_call(&cpu,0xbfc00000,0x80000080);psx_cyc_batch_flush();entry_cycles=psx_cycle_count; + /* Supply the ordinary non-delay-slot EPC+4 return to isolate admission. + This authored fixture does not simulate the guest handler or RFE. */ + resumed_pc=cpu.cop0[14]+4; + Test_psx_dispatch_call(&cpu,resumed_pc,0x1000); + } else Test_psx_dispatch_call(&cpu,0xbfc00020,0x1000); + psx_cyc_batch_flush(); + printf("{\"entry_cycles\":%llu,\"cycles\":%llu,\"static_hits\":%llu,\"s1\":%u,\"epc\":%u,\"cause\":%u,\"resumed_pc\":%u,\"pc\":%u,\"fetches\":[",(unsigned long long)entry_cycles,(unsigned long long)psx_cycle_count,(unsigned long long)g_dispatch_static_hits,cpu.gpr[17],cpu.cop0[14],cpu.cop0[13],resumed_pc,cpu.pc); + for(unsigned i=0;inear_delay+10000000,"far implicit seek includes distance latency"); + CHECK(reverse==far_delay,"ReadS uses the same distance rule in reverse direction"); + /* Same target while already reading must preserve the buffered stream. */ + setup(100); reading=1; stat_reg=CDSTAT_MOTOR|CDSTAT_READ; + read_delay=12345; RB_.size=2048; RB_.pos=4; + target(100); command(6); + CHECK(read_delay==12345&&RB_.size==2048&&RB_.pos==4,"same-stream read is not restarted"); + /* Existing command latency is independent of the long sector deadline. */ + setup(103948); target(251926); command(6); + CHECK(cdrom_irq_present_due==CDROM_IRQ_PRESENT_DELAY,"ACK presentation latency unchanged"); + ack(); command(9); ack(); advance(100000000); + CHECK(reads==0,"Pause cancels an implicit seek before any sector is delivered"); + /* A data-ready result is asynchronous, unlike a response created inside + * the guest's command store. Its readable flag and INTC line must agree. + * Otherwise the SDK can clear a newly visible INT1 before INTC ever sees + * it when preparing its next GetStat, losing the first sector header. */ + setup(100); target(200); command(6); ack(); i_stat=0; irq_raises=0; + advance(read_delay); + CHECK(irq_flag==CDIRQ_DATA_READY,"first target owns the visible response"); + CHECK((i_stat&(1u<<2))!=0,"async INT1 reaches INTC when the ready flag becomes visible"); + CdTimingPub timing; + CHECK(cdrom_timing_record(cdrom_timing_total()-1,&timing)&& + timing.lba==200&&timing.intc_cycle==psx_cycle_count&& + timing.irq_arm_cycle==timing.intc_cycle, + "immediate INTC timestamp belongs to the sector that raised it"); + command(1); + CHECK(queued_cmd.pending&&irq_flag==CDIRQ_DATA_READY, + "GetStat cannot replace an outstanding sector response"); + CHECK(last_sector_lba==200&&s_ring_read==s_ring_write, + "queued GetStat preserves the first target ring owner"); + advance(2472); + CHECK(irq_raises==1,"unacked asynchronous INT1 raises INTC once"); + ack(); i_stat=0; irq_raises=0; + CHECK(irq_flag==CDIRQ_ACK,"acknowledging data-ready releases queued GetStat"); + advance(CDROM_IRQ_PRESENT_DELAY-1); + CHECK(i_stat==0,"queued command retains command-response delay"); + advance(1); + CHECK((i_stat&(1u<<2))&&irq_raises==1,"queued command ACK presents once at its own deadline"); + /* An interrupt mask suppresses INTC, without hiding the controller flag. */ + setup(100); target(200); command(6); ack(); irq_enable=0; i_stat=0; + advance(read_delay); + CHECK(irq_flag==CDIRQ_DATA_READY&&i_stat==0,"masked async result retains its pending flag"); + irq_enable=7; advance(0); + CHECK((i_stat&(1u<<2))!=0,"enabling a pending async result presents INTC"); + /* Source-profile deadline control: a two-slot source pipeline does not + * present the first sector until its third fetch. Default stays two. */ + for (int double_speed=0; double_speed<=1; ++double_speed) { + SET_MODEL(""); setup(100); mode_reg=double_speed?0x80:0; + target(200); command(6); int baseline=read_delay; + SET_MODEL("octoshock-2.2.2-pipeline"); + setup(100); mode_reg=double_speed?0x80:0; target(200); command(6); + int period=double_speed?225792:451584; + CHECK(read_delay==baseline+period,"profile adds exactly one sector period to first delivery"); + ack(); advance(baseline); CHECK(reads==0,"profile target not presented at old deadline"); + advance(period-1); CHECK(reads==0,"profile target not presented before third-fetch deadline"); + advance(1); CHECK(reads==1&&last_sector_lba==200,"profile presents requested target at third-fetch deadline"); + ack(); advance(period); CHECK(reads==2&&last_sector_lba==201,"profile preserves subsequent sector cadence"); + } + SET_MODEL(""); + if(failures) { fprintf(stderr,"FAILED (%d)\n",failures); return 1; } + puts("CD implicit seek: ALL PASS"); return 0; +} diff --git a/runtime/tests/test_cdrom_random_tape.c b/runtime/tests/test_cdrom_random_tape.c new file mode 100644 index 000000000..24a5f5b0d --- /dev/null +++ b/runtime/tests/test_cdrom_random_tape.c @@ -0,0 +1,45 @@ +#include "cdrom_random_tape.h" +static int failures; +#define CHECK(x) do { if (!(x)) { fprintf(stderr,"FAIL line %d: %s\n",__LINE__,#x); failures++; } } while(0) +int main(int argc,char **argv) { + CdRandomTape tape={0}; uint32_t value=99; + static uint8_t authored[]={3,0,0,0,2,0,0,0,0,0,0,0}; + tape.bytes=malloc(sizeof(authored));memcpy(tape.bytes,authored,sizeof(authored));tape.count=3; + CHECK(cd_tape_bounded(&tape,2,&value) && value==2 && tape.cursor==2); + CHECK(cd_tape_bounded(&tape,0,&value) && value==0 && tape.cursor==3); + value=99;CHECK(!cd_tape_bounded(&tape,5,&value) && value==99 && tape.cursor==3); + uint8_t changed[32]={1}; + CHECK(!cd_tape_restore(&tape,changed,3,1) && tape.cursor==3); + CHECK(!cd_tape_restore(&tape,tape.sha256,2,1) && tape.cursor==3); + CHECK(!cd_tape_restore(&tape,tape.sha256,3,4) && tape.cursor==3); + CHECK(cd_tape_restore(&tape,tape.sha256,3,1) && tape.cursor==1); + CHECK(cd_tape_bounded(&tape,2,&value) && value==2 && tape.cursor==2); + cd_tape_reset(&tape);CHECK(tape.cursor==0); + CHECK(cd_tape_bounded(&tape,UINT32_MAX,&value) && value==3 && tape.cursor==1); + cd_tape_free(&tape); + if (argc==3 && !strcmp(argv[1],"--reject")) { + CHECK(!cd_tape_load(&tape,argv[2])); + } + if (argc==2) { + CHECK(cd_tape_load(&tape,argv[1])); + printf("tape SHA256 ");for(unsigned i=0;i<32;i++)printf("%02x",tape.sha256[i]);puts(""); + static const uint32_t vectors[][3]={ + {3000,1137,3},{3000,103,1},{3000,900,1},{3000,952,2},{3000,2893,1}, + {25000,24420,1},{3000,2573,1},{3000,637,1},{3000,1936,2},{3000,2431,3}, + {25000,5306,5},{3000,2586,1},{3000,2704,1},{25000,22008,1},{3000,1775,1}, + {3000,2376,1},{3000,2138,2},{25000,19069,1},{3000,1518,2},{3000,1518,1}, + {25000,9362,3},{3000,1698,1}}; + for (unsigned i=0;is_source_ready_due?s_source_command_due:s_source_ready_due; + CHECK(due>=psx_cycle_count,"command stage deadline monotonic"); + advance((int)(due-psx_cycle_count)); + } + CHECK(!queued_cmd.pending,"command admitted within phase bound"); +} +int main(int argc,char **argv) { + if(argc<2)return 2; + tape_path=argv[1]; + if(argc==3) { + clock_boot(); + if(!strcmp(argv[2],"late-argument")) {command(1);cdrom_write(0x1f801802,0);} + else if(!strcmp(argv[2],"cdda"))command(3); + else if(!strcmp(argv[2],"exhausted")) {s_source_clock_tape.cursor=s_source_clock_tape.count;command(1);} + return 1; /* The invalid operation must have stopped the process. */ + } + set_model("PSX_CD_SOURCE_CLOCK_TAPE",""); + CHECK(baseline_explicit_timing_main()==0,"old explicit timing controls remain passing"); + for(int size=0;size<=4;size++) { + clock_boot();request_reg=CDROM_REQUEST_BFRD;RB_.pos=0;RB_.size=size; + uint32_t expected=0; + for(int i=0;i