From 78209436fa565ac1a3fce163a3a3fe5f71ea97cc Mon Sep 17 00:00:00 2001 From: theanch92 Date: Wed, 2 Sep 2026 21:46:46 +0200 Subject: [PATCH] fix(pal): fix CD-XA audio looping and enable DualShock analog - Fix CD-XA Mode 2 Form 2 audio looping in psxrecomp (cdrom.c): handle XA_SUBMODE_EOF (0x80) and deliver CDIRQ_DATA_END (INT4), fixing the BGM looping bug in Fisherman's Village (Beach Village). - Enable DualShock analog controller and rumble in Italian configuration. - Add Section 9 to docs/italian_pal_port.md documenting the BGM.XA interleaved channel layout, sector offsets, and INT4 delivery mechanism. - Update compile_overlays.py to support cross-compiling Windows overlay shards. --- docs/italian_pal_port.md | 47 +++++++++ game_ita.toml | 10 +- packaging/release/game_ita.toml | 174 +++++++++++++++++++++++++++++--- psxrecomp-v4 | 2 +- 4 files changed, 213 insertions(+), 20 deletions(-) diff --git a/docs/italian_pal_port.md b/docs/italian_pal_port.md index 7ac48e2..34ffc0e 100644 --- a/docs/italian_pal_port.md +++ b/docs/italian_pal_port.md @@ -288,3 +288,50 @@ pgxp_tolerance = -1.0 * By default, `psxrecomp-v4/runtime/src/pgxp.cpp` applies a conservative `0.5px` tolerance filter (`s_tolerance = 0.5f`), rejecting any sub-pixel vertex displacement greater than half a pixel back to the stock integer grid (`s_stats.tolerance_reject++`). * In Tomba 2, complex segmented meshes and winding 2.5D terrain paths trigger this tolerance clamp extensively, causing widespread fallback to uncorrected integer coordinates and rendering PGXP visually indistinguishable from stock. * Setting `pgxp_tolerance = -1.0` disables the tolerance gate (`s_tolerance < 0.0f`), allowing full sub-pixel floating-point vertex stability across all 3D geometry and completely eliminating polygon jitter. + +--- + +## 9. CD-XA Streaming Audio Architecture & Looping Fix + +### 9.1 Overview & Dual Audio Architecture in Tomba 2 + +*Tomba! 2* uses a hybrid audio architecture across different areas and gameplay sequences: +1. **Sequenced SPU ADPCM Synthesis (MIDI-like)**: The majority of areas (e.g. Donglin Forest, Coal Mine) load compressed ADPCM sample banks (`TOMBA2.SND` / `TOMBA2.DAT`) into the SPU 512 KB RAM and sequence music note commands internally. These loops are handled entirely in CPU/SPU memory without polling the CD drive. +2. **Real-time CD-XA Audio Streaming**: Special atmospheric locations — notably **Fisherman's Village / Beach Village** (Town of the Fishermen) — stream high-fidelity multi-channel CD-XA Mode 2 Form 2 ADPCM audio directly from `/CD/BGM.XA`. + +### 9.2 Structure of `/CD/BGM.XA` & Channel Interleaving + +The file `/CD/BGM.XA;1` starts at disc LBA `23793` and contains **8 interleaved stereo channels** (Channels 0 to 7): + +| Channel | Track Description | Start LBA (Offset) | End LBA (Offset) | Sectors | Duration | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **0** | Atmospheric Ambient Stream | `23793` (`+0`) | `91432` (`+67639`) | 67,639 | ~15.03 min | +| **1** | Cinematic Ambient Theme | `23794` (`+1`) | `71002` (`+47209`) | 47,208 | ~10.49 min | +| **2** | Town Background Variation | `23795` (`+2`) | `91427` (`+67634`) | 67,632 | ~15.03 min | +| **3** | Event Theme Stream | `23796` (`+3`) | `81324` (`+57531`) | 57,528 | ~12.78 min | +| **4** | Dynamic Theme Variation | `23797` (`+4`) | `71157` (`+47364`) | 47,360 | ~10.52 min | +| **5** | Special Area Theme | `23798` (`+5`) | `76462` (`+52669`) | 52,664 | ~11.70 min | +| **6** | Coastal Area Ambience | `23799` (`+6`) | `64159` (`+40366`) | 40,360 | ~8.97 min | +| **7** | **Fisherman's Village BGM** | **`23800` (`+7`)** | **`34616` (`+10823`)** | **10,816** | **~144.2 s (2.40 min)** | + +### 9.3 The Bug Mechanism: Missing `INT4` (`CDIRQ_DATA_END`) on XA EOF + +1. **Hardware Behavior**: + * Channel 7 (Fisherman's Village) reaches its conclusion after 10,816 sectors (144.2 seconds). + * The final sector at LBA `34616` contains the subheader `Submode = 0xE4` (`XA_SUBMODE_EOF` = `0x80u` | `XA_SUBMODE_REALTIME` = `0x40u` | `XA_SUBMODE_FORM2` = `0x20u` | `XA_SUBMODE_AUDIO` = `0x04u`). + * On genuine PlayStation hardware, encountering `XA_SUBMODE_EOF` triggers an asynchronous **`INT4` (`CDIRQ_DATA_END`)** interrupt from the CD-ROM controller to the MIPS CPU. + * The game's CD-ROM callback receives `CdlDataEnd` and immediately issues a `CdControl(CdlSeekL)` and `CdControl(CdlReadS)` to loop back to the start LBA of the track (`23800`). + +2. **The Defect in `psxrecomp` (`cdrom.c`)**: + * Prior to this fix, `psxrecomp-v4/runtime/src/cdrom.c` only delivered `CDIRQ_DATA_END` for Redbook audio (`CD-DA`) tracks via `deliver_cdda_data_end()`. + * During XA read streaming, `cdrom.c` classified the subheaders but completely ignored the `XA_SUBMODE_EOF` (`0x80u`) bit. + * Because `CDIRQ_DATA_END` was never raised, the guest CD callback was never invoked, and the virtual CD head kept streaming indefinitely past LBA `34616` into subsequent sectors, playing unintended audio. + +3. **The Implementation**: + * Defined `#define XA_SUBMODE_EOF 0x80u` in `cdrom.c`. + * In `read_sector_at()`, when `delivery.xa_audio_delivered` is true and `(delivery.xa_submode & XA_SUBMODE_EOF)` is detected: + * `xa_data_end_pending` is latched. + * If `mode_reg & 0x02u` (Auto-Pause), the read stream is automatically paused. + * In `deliver_read_sector()` and `cdrom_advance()`, `deliver_xa_data_end()` raises `CDIRQ_DATA_END` (INT4) with `stat_reg` to the CPU. + * This cleanly notifies the guest driver, resulting in continuous, seamless BGM looping. + diff --git a/game_ita.toml b/game_ita.toml index 4d19b41..0dcf2aa 100644 --- a/game_ita.toml +++ b/game_ita.toml @@ -27,9 +27,10 @@ fast_boot = true # Distinct debug port allowing side-by-side execution with the US release (4515). debug_port = 4516 window_title = "Tombi! 2 (Italian) Recompiled" -controller = "digital" +controller = "dualshock" memcard_dir = "saves_ita" + # Overlay cache (the real perf fix). Tomba 2's title/menu/level/FMV-driver code # is runtime-loaded overlay, not in the compiled boot-EXE funcs, so by # default it ALL interprets (~4fps game logic). The cache JITs those overlays to @@ -91,9 +92,9 @@ overlay_autocompile_cmd = "py -3 psxrecomp-v4/tools/compile_overlays.py --captur # lock_mode but documents intent. Re-enable the selector (drop lock_mode) only # once the analog config handshake is emulated end-to-end. [controller] -default_mode = "digital" -allow_hybrid = false -lock_mode = true +default_mode = "analog" +allow_hybrid = true +lock_mode = false # --- Renderer: optimized hardware path. # Supersampling: internal-resolution SSAA factor. 2 is the quality default now @@ -138,6 +139,7 @@ offer = false gte_game_mode = true offer_ultrawide = true adaptive_view = true +clear_reveal = true nw_hud_corners = false nw_backdrop = false nw_flat_backdrop = true diff --git a/packaging/release/game_ita.toml b/packaging/release/game_ita.toml index e211b58..0dcf2aa 100644 --- a/packaging/release/game_ita.toml +++ b/packaging/release/game_ita.toml @@ -14,23 +14,93 @@ out_dir = "generated_ita" strict = true [runtime] +# HLE boot shell-skip (master 582aecc): skips the PSX boot animation via the +# HLE tier's one-shot shell intercept; kernel init + game EXE load still run +# on the real recompiled BIOS at host speed. Set false to watch BIOS logos. +# Full HLE kernel tier (bios_hle) stays OFF here — LLE is the baseline; Tomba1 +# is the HLE validation title. +fast_boot = true +# Loading speed is mod-owned for this title ("Fast Loading (host pacing)" and +# "CD Speed" on the Mods page, both default-off). The legacy generic +# turbo_loads / offer_turbo_loads keys were retired framework-side -- the +# runtime now ignores them and warns on sight -- so they are gone from here. +# Distinct debug port allowing side-by-side execution with the US release (4515). +debug_port = 4516 window_title = "Tombi! 2 (Italian) Recompiled" +controller = "dualshock" memcard_dir = "saves_ita" -fast_boot = true + + +# Overlay cache (the real perf fix). Tomba 2's title/menu/level/FMV-driver code +# is runtime-loaded overlay, not in the compiled boot-EXE funcs, so by +# default it ALL interprets (~4fps game logic). The cache JITs those overlays to +# native via the *gcc* backend: autocapture writes overlay_captures.json, the +# autocompile_cmd spawns the recompiler+gcc in the background to produce CPS +# overlay DLLs in build-t2/cache, and the loader hot-swaps them in live. +# +# overlay_backend = "gcc" (NOT sljit): on 2026-06-23 the in-process sljit backend +# declined ~31/39 Tomba 2 shards (no payoff) and hard-HUNG the boot on the +# Whoopee-Camp logo. With gcc selected the loader runs gcc>interp and never +# starts sljit live (overlay_loader_apply_live_policy forces sljit off when the +# active backend is gcc), so the gap before DLLs land is the interpreter (slow +# but correct), never the sljit hang path. +# +# No --cps in the command: CPS is the recompiler DEFAULT (PSX_CPS unset), which +# matches Tomba 2's CPS boot + CPS runtime. flavor 0 (not widescreen). overlay_cache = true +# Preserve each coherent runtime discovery without replacing earlier launches. +# Production keeps the append-only addendum beside the executable; dev configs +# additionally opt into immutable project-local snapshots. overlay_capture_history = true +# "auto" = gcc when a gcc toolchain is present, else the toolchain-free tcc tier +# (post-2026-06-25 tcc refactor). On a dev machine with mingw this resolves to gcc, +# so behaviour matches the old explicit "gcc"; on a clean install it falls back to +# tcc instead of leaving overlays interpreted. The in-process sljit producer stays +# OFF in every case (it hung Tomba 2's Whoopee-Camp logo — see below). overlay_backend = "auto" +# Keep the timing-sensitive splash/FMV task setup routines on the interpreter. +# Native execution of these small setup paths leaves the Whoopee-Camp logo stuck +# before the intro FMV; the rest of the overlay cache still runs native. overlay_native_block = [ "0x80097818", "0x800529D8", "0x80052A20", ] +# All paths are RELATIVE to the project root (the runtime spawns this with cwd = +# project root), so the shipped config is portable across machines. The +# recompiler exe + runtime-include are resolved to absolute by compile_overlays.py +# itself (a relative Windows exe path isn't resolved against the child cwd). +# This hot computed-dispatch routine has no stable callable boundary in the +# surrounding overlay, so keep it in the isolated fragment failure domain. +overlay_autocompile_cmd = "py -3 psxrecomp-v4/tools/compile_overlays.py --captures build-t2/overlay_captures.json --game-toml game_ita.toml --recompiler psxrecomp-v4/recompiler/build/psxrecomp-game.exe --runtime-include psxrecomp-v4/runtime/include --out-dir build-t2/cache --force-interior 0x8008D030 && py -3 psxrecomp-v4/tools/coverage_vault.py merge --vault ../_coverage_vault/SCES-02686 --captures build-t2/overlay_captures.json --cache build-t2/cache/SCES-02686" +# Tomba 2 is a d-pad platformer. Real PS1 hardware powers a DualShock up in +# DIGITAL mode (analog LED off, controller id 0x41) and Tomba 2's pad driver +# only runs the DualShock config-mode handshake (0x43/0x4D...) when it sees the +# analog id 0x73. The framework's default HYBRID mode boots analog-on (0x73), +# which triggers that handshake every frame; our SIO config emulation doesn't +# satisfy the driver, so it intermittently reads 0x00 button bytes (all buttons +# "pressed") -> menu unresponsive + idle timer never reaches the attract demo. +# Booting DIGITAL (id 0x41, never 0x73) matches the Beetle oracle exactly: the +# game just polls with 0x42 and reads clean 0xFFFF idle. +# +# Analog / Hybrid are NOT offered for Tomba 2: the DualShock config-mode +# handshake emulation is incomplete, so any non-digital mode re-triggers the +# phantom-input bug above. lock_mode hides the launcher's whole pad-mode +# selector (Hybrid | Analog | D-Pad) and forces every port to default_mode, so +# the player can never pick a broken mode. allow_hybrid=false is redundant under +# lock_mode but documents intent. Re-enable the selector (drop lock_mode) only +# once the analog config handshake is emulated end-to-end. [controller] -default_mode = "digital" -allow_hybrid = false -lock_mode = true +default_mode = "analog" +allow_hybrid = true +lock_mode = false +# --- Renderer: optimized hardware path. +# Supersampling: internal-resolution SSAA factor. 2 is the quality default now +# that Tomba 2 holds full speed on the OpenGL path; lower to 1 for slower GPUs. +# Antialiasing: linear present filtering for the sampled frame. The launcher +# exposes both controls on the Video page and persists overrides to settings.toml. [video] renderer = "opengl" supersampling = 2 @@ -38,31 +108,65 @@ antialiasing = true texture_filtering = "nearest" pgxp_tolerance = -1.0 geometry_correction = true +# Tomba 2 owns temporal frame blending through its built-in Frame Blending mod. +# Hide the generic Display controls and ignore stale settings.toml values. frame_interpolation = false frame_interpolation_fps = 0 offer_frame_interpolation = false +# Authentic 4:3 is the baseline. The Widescreen mod activates Tomba 2's +# game-specific native-wide hooks and selects the presentation aspect. aspect_ratio = "4:3" -auto_skip_fmv = false -offer_skip_fmv = false -fmv_skip_no_xa = true +# FMV auto-skip (debug iteration speed). Tomba2's streamed movies end on a +# frame-total cell (single u32 at 0x8010271C, checked by the sector pump +# FUN_8008d110 against the streamed frame counter 0x801026E8; 0 = play to EOF) +# rather than Tomba1's per-movie table — the START-hold fallback handles the +# START-skippable ones. The Whoopee Camp logo is a SILENT RAM-preloaded movie +# (no CD sectors, no XA during playback): fmv_skip_no_xa broadens detection to +# MDEC activity alone so it fast-forwards too (presentation-side only). +# The longer silent-MDEC hold also covers the logo's post-decode wait. Tomba 2 +# owns activation through its Skip FMVs mod; hide the legacy Display toggle and +# keep the faithful baseline until that mod activates. +auto_skip_fmv = false +offer_skip_fmv = false +fmv_skip_no_xa = true fmv_skip_no_xa_hold = 600 +# Native-wide gameplay. The runtime keeps BIOS, FMV, and true 2D screens at +# their authored 4:3 aspect; GTE-active gameplay uses the wider compositor. [widescreen] +# Widescreen is game-owned on the Mods page, not a generic Display setting. offer = false gte_game_mode = true offer_ultrawide = true adaptive_view = true +clear_reveal = true nw_hud_corners = false nw_backdrop = false nw_flat_backdrop = true nw_phase_backdrop = true [widescreen.cull] +# Tomba 2's overlay render funnels reject projected vertices against the +# original 320x240/256 bounds. Widen the horizontal comparisons only while a wide +# aspect is active. The explicit height immediate qualifies the signature but +# is not itself changed. auto_screen_x = true screen_w_imms = ["0x140", "0x141", "0x142"] screen_h_imms = ["0xF0", "0xFE", "0xFF", "0x100", "0x101", "0x102", "0x103"] +# Keep a small resident-area guard beyond the live wide edge. The aspect-cone +# hook below treats this separately from the visible field and reserves queue +# headroom for candidates actually intersecting the wide frustum. guard_pixels = 52 +# Advance only the proven player-relative X/Z activation windows by the +# maximum supported resident-area lead. This does not widen terrain-cell +# angles or fixed-capacity model cones; at 21:9 the activation margin is +# 120px visible reveal + 52px render guard + 256px activation lead = 428px. activation_guard_pixels = 256 +# Proven model participation windows. Overlay code at the same address is +# opcode-checked and left untouched when it differs. FUN_8006A438 is a +# behavior/teleport proximity trigger, not a pure render cull; retain only its +# ground-plane X/Z pairs. Its Y pair (0x8006A4D4/0x8006A4DC) stays vanilla so +# vertical behavior reach is not extended. bias_sites = [ "0x8006A4F8", "0x8006A51C", ] @@ -71,6 +175,11 @@ range_sites = [ ] screen_x_sites = ["0x8003EB24"] +# Six captured overlay producers build terrain-cell selection quads consumed +# by bounded, deduplicating rasterizers. These are positive 12-bit angular +# half-extents, not pixels: widen their tangent by the live horizontal aspect +# factor. Full-word guards make overlay aliases inert; every observed producer +# retains the fixed 0xFE terrain-list cap. [[widescreen.cull.angle]] address = "0x8010B5E8" expected = "0x240301C7" @@ -96,26 +205,34 @@ address = "0x801406DC" expected = "0x240201C7" [widescreen.cull.aspect_cone] +# FUN_80077A7C clears model+1, applies a spherical camera cone, then inserts +# accepted type 2/9, 4, and 5 models into fixed 24/40/28-entry scratch queues. +# These full-word-guarded sites preserve every vanilla acceptance and widen +# only camera-horizontal participation. Vertical reach and distance gates stay +# vanilla. Hysteresis is outside the activation guard. forward_addr = "0x1F8000E8" object_type_offset = 12 -object_reg = 19 -x_reg = 16 -z_reg = 17 -y_reg = 18 +object_reg = 19 # s3 +x_reg = 16 # s0 +z_reg = 17 # s1 +y_reg = 18 # s2 hysteresis_pixels = 24 queue_reserve = 4 queue_count_addrs = ["0x1F800144", "0x1F800150", "0x1F80015C"] queue_capacities = [24, 40, 28] queue_type_masks = ["0x00000204", "0x00000010", "0x00000020"] +# FUN_8002B9B4 is the lower-level per-model/per-child cone. It does not append +# to the fixed scratch queues. Its SLT compares dot(camera_forward, delta) +# against distance * 0xD60; 0xD60 Q12 is 0x358 (856) in the hook's Q10 units. [[widescreen.cull.aspect_cone.sites]] address = "0x8002BAA4" expected = "0x0082202A" cosine_threshold = 856 -object_reg = 20 -x_reg = 19 -z_reg = 18 -y_reg = 17 +object_reg = 20 # s4 +x_reg = 19 # s3 +z_reg = 18 # s2 +y_reg = 17 # s1 queue_guard = false [[widescreen.cull.aspect_cone.sites]] @@ -142,6 +259,33 @@ expected = "0x28620350" address = "0x80077F14" expected = "0x28620368" +# ── Audit-specific config (consumed by psxrecomp/tools/*.py and tests) ── +# Schema mirrors psxrecomp/bios/SCPH1001.toml. Paths are relative to the +# project root. + +[audit] +# Dynamic discovery — functions are found by walking control flow from +# entry_pc. No Ghidra function-starts file is required to bootstrap. + +# Code region: text segment is contiguous from load_address for text_size bytes. +[[audit.regions]] +name = "Text" +rom_start = "0x800" # offset within the PS-X EXE file (skip header) +rom_end = "0x28800" # rom_start + text_size +vaddr_base = "0x80010000" + +# Address normalization: KSEG masking only (game loads contiguously, no remaps). +[audit.normalize] +kseg_mask = "0x1FFFFFFF" + +# Conservative load-time optimization. +# Verified against SCES_026.86's PsyQ VSync(mode) routine: VSync(-1) returns +# the guest VBlank counter at 0x80025ED8. The adjacent GPUSTAT and Timer1 reads +# are used only to stabilize the query path; the framework hook preserves the +# original instruction timing, stack traffic, cache fetches, and IRQ checkpoints +# while bypassing those side-effect-free MMIO handlers. This is intentionally +# not a turbo/speedhack path: no CD speed, pacing, idle-skip, or event-horizon +# fast-forward is enabled here. [load_accel.vsync_query] func = "0x80016E70" counter_addr = "0x80025ED8" diff --git a/psxrecomp-v4 b/psxrecomp-v4 index a7459cd..71a4db4 160000 --- a/psxrecomp-v4 +++ b/psxrecomp-v4 @@ -1 +1 @@ -Subproject commit a7459cddbe2a869477b0cd5530da8963cf9b5d7d +Subproject commit 71a4db4cf599b6fcb7bed23f80f09b8eb58a8619