Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/italian_pal_port.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

10 changes: 6 additions & 4 deletions game_ita.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
174 changes: 159 additions & 15 deletions packaging/release/game_ita.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,55 +14,159 @@ 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
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",
]
Expand All @@ -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"
Expand All @@ -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]]
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion psxrecomp-v4