mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 18:25:35 -04:00
Better Things and Runtime Fixes
This commit is contained in:
+3
-1
@@ -10,7 +10,8 @@ duckstation/dep/prebuilt/
|
||||
|
||||
# Ghidra project files (large, regenerable)
|
||||
ghidra/
|
||||
|
||||
references/
|
||||
SpiritDecomp
|
||||
# Backup of the working state — see CURRENT_STATE.md for what this contains.
|
||||
.happypath/
|
||||
|
||||
@@ -27,6 +28,7 @@ test_msvc/
|
||||
# Recompiler output (derivative of copyrighted Sony BIOS, regenerate locally)
|
||||
generated/
|
||||
|
||||
|
||||
# Release packaging artifacts
|
||||
release-stage/
|
||||
*.zip
|
||||
|
||||
@@ -56,5 +56,10 @@ hardware provides.
|
||||
|
||||
## Resolved
|
||||
|
||||
- ~~S1 — GPU shaded line uses flat color~~ — Fixed: sw_draw_shaded_line interpolates C0→C1 per pixel
|
||||
- ~~S2 — GPU polyline crashes~~ — Fixed: state machine with 0x50005000 terminator
|
||||
- ~~S3 — MDEC decoder is a no-op~~ — Fixed: full Beetle-faithful decoder (RLE decode, dequant, IDCT, YCbCr→RGB 15/24bpp, macroblock pipeline)
|
||||
- ~~S4 — SPU has no audio synthesis~~ — Fixed: 24-voice ADPCM decoder, reverb, noise LFSR, volume sweep, pitch modulation (PMON), IRQ delivery
|
||||
- ~~S5 — DMA channels 0,1,3,5 unimplemented~~ — Fixed: ch0/1 MDEC, ch3 CDROM, ch4 SPU, ch5 PIO all implemented
|
||||
- ~~DMA ch2 GPU→RAM writes zeros~~ — Fixed 2026-04-24 (4054dc1)
|
||||
- ~~GPU raw texture mode uses color==0x808080 check~~ — Fixed 2026-04-24
|
||||
|
||||
@@ -705,21 +705,21 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
|
||||
uint32_t rs = get_rs(instr);
|
||||
uint32_t rt = get_rt(instr);
|
||||
|
||||
// REGIMM branches (bltz, bgez, bltzal, bgezal)
|
||||
// REGIMM branches (bltz, bgez, bltzal, bgezal + likely variants)
|
||||
if (opcode == 0x01) {
|
||||
uint32_t regimm_op = (instr >> 16) & 0x1F;
|
||||
if (regimm_op == 0x00) { // bltz
|
||||
if (regimm_op == 0x00 || regimm_op == 0x02) { // bltz / bltzl
|
||||
// Classified LEFT-edge funnel bltz (auto_screen_x, signed idioms):
|
||||
// reject only past the revealed margin. Identity at 4:3 (margin 0).
|
||||
if (ws_cull_bltz_pcs_.count(addr))
|
||||
return fmt::format("psx_ws_cull_bltz({}) /* ws auto screen-x cull (left edge) */",
|
||||
reg_name(rs));
|
||||
return fmt::format("(int32_t){} < 0", reg_name(rs));
|
||||
} else if (regimm_op == 0x01) { // bgez
|
||||
} else if (regimm_op == 0x01 || regimm_op == 0x03) { // bgez / bgezl
|
||||
return fmt::format("(int32_t){} >= 0", reg_name(rs));
|
||||
} else if (regimm_op == 0x10) { // bltzal
|
||||
} else if (regimm_op == 0x10 || regimm_op == 0x12) { // bltzal / bltzall
|
||||
return fmt::format("(int32_t){} < 0", reg_name(rs));
|
||||
} else if (regimm_op == 0x11) { // bgezal
|
||||
} else if (regimm_op == 0x11 || regimm_op == 0x13) { // bgezal / bgezall
|
||||
return fmt::format("(int32_t){} >= 0", reg_name(rs));
|
||||
}
|
||||
}
|
||||
@@ -741,6 +741,26 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
|
||||
case 0x07: // bgtz
|
||||
case 0x17: // bgtzl
|
||||
return fmt::format("(int32_t){} > 0", reg_name(rs));
|
||||
|
||||
case 0x12: // COP2 branch (BC2F, BC2T)
|
||||
{
|
||||
uint32_t cop2_rs = (instr >> 21) & 0x1F;
|
||||
uint32_t cond_code = (instr >> 16) & 0x3;
|
||||
// BC2F (cop2_rs==0x08): branch if COP2 condition is FALSE
|
||||
// BC2T (cop2_rs==0x09): branch if COP2 condition is TRUE
|
||||
// The condition tests bit (30+cond_code) of GTE ctrl register 31
|
||||
// (flags register). GTE flags bits: bit31=LE0, bit30=GE0, etc.
|
||||
// For PS1 games, the standard pattern is:
|
||||
// BC2F: branch if (gte_ctrl[31] & (1 << (30+cc))) == 0
|
||||
// BC2T: branch if (gte_ctrl[31] & (1 << (30+cc))) != 0
|
||||
if (cop2_rs == 0x09) { // BC2T
|
||||
return fmt::format("(cpu->gte_ctrl[31] & 0x{:08X}u) != 0",
|
||||
1u << (30 + cond_code));
|
||||
} else { // BC2F
|
||||
return fmt::format("(cpu->gte_ctrl[31] & 0x{:08X}u) == 0",
|
||||
1u << (30 + cond_code));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "0 /* unknown branch condition: defaults to not-taken */";
|
||||
@@ -1086,6 +1106,24 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
|
||||
case 0x27: code = translate_nor(instr); break; // nor
|
||||
case 0x2A: code = translate_slt(instr); break; // slt
|
||||
case 0x2B: code = translate_sltu(instr); break; // sltu
|
||||
case 0x0A: // movn
|
||||
{
|
||||
uint32_t rs = get_rs(instr);
|
||||
uint32_t rt = get_rt(instr);
|
||||
uint32_t rd = get_rd(instr);
|
||||
code = fmt::format("if ({}) {} = {}; /* movn */",
|
||||
reg_name(rt), reg_name(rd), reg_name(rs));
|
||||
}
|
||||
break;
|
||||
case 0x0B: // movz
|
||||
{
|
||||
uint32_t rs = get_rs(instr);
|
||||
uint32_t rt = get_rt(instr);
|
||||
uint32_t rd = get_rd(instr);
|
||||
code = fmt::format("if ({}) {} = {}; /* movz */",
|
||||
reg_name(rt), reg_name(rd), reg_name(rs));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
code = fmt::format("/* TODO: SPECIAL funct=0x{:02X} */", funct);
|
||||
}
|
||||
@@ -1173,6 +1211,10 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
|
||||
uint32_t gte_cmd = instr & 0x1FFFFFF;
|
||||
// Route ALL GTE commands through gte_execute() for correct behavior
|
||||
code = fmt::format("gte_execute(cpu, 0x{:07X}); /* gte cmd 0x{:02X} */", gte_cmd, gte_cmd & 0x3F);
|
||||
} else if (cop_op == 0x08) { // BC2F - branch if GTE flag clear
|
||||
code = fmt::format("/* BC2F — handled as block exit branch */ (void)0; /* bc2f */");
|
||||
} else if (cop_op == 0x09) { // BC2T - branch if GTE flag set
|
||||
code = fmt::format("/* BC2T — handled as block exit branch */ (void)0; /* bc2t */");
|
||||
} else {
|
||||
code = fmt::format("/* cop2: 0x{:08X} */", instr);
|
||||
}
|
||||
@@ -1476,7 +1518,8 @@ std::string CodeGenerator::translate_basic_block(
|
||||
uint32_t branch_instr = block.exit_instr.instruction;
|
||||
uint32_t b_opcode = (branch_instr >> 26) & 0x3F;
|
||||
uint32_t regimm_op = (branch_instr >> 16) & 0x1F;
|
||||
if (b_opcode == 0x01 && (regimm_op == 0x10 || regimm_op == 0x11)) {
|
||||
if (b_opcode == 0x01 && (regimm_op == 0x10 || regimm_op == 0x11 ||
|
||||
regimm_op == 0x12 || regimm_op == 0x13)) {
|
||||
ss << config_.indent
|
||||
<< fmt::format("cpu->gpr[31] = 0x{:08X}u; /* branch-and-link before delay slot */\n",
|
||||
addr + 8);
|
||||
@@ -1531,8 +1574,9 @@ std::string CodeGenerator::translate_basic_block(
|
||||
uint32_t b_opcode = (branch_instr >> 26) & 0x3F;
|
||||
if (b_opcode == 0x01) {
|
||||
uint32_t regimm_op = (branch_instr >> 16) & 0x1F;
|
||||
if (regimm_op == 0x10 || regimm_op == 0x11) {
|
||||
// bltzal or bgezal: link register always set
|
||||
if (regimm_op == 0x10 || regimm_op == 0x11 ||
|
||||
regimm_op == 0x12 || regimm_op == 0x13) {
|
||||
// bltzal/bgezal/bltzall/bgezall: link register always set
|
||||
ss << config_.indent
|
||||
<< fmt::format("cpu->gpr[31] = 0x{:08X}; /* branch-and-link return addr */\n", addr + 8);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,12 @@ bool ControlFlowAnalyzer::is_control_flow(uint32_t instr) {
|
||||
return true; // j, jal
|
||||
}
|
||||
|
||||
// COP2 branches (BC2F, BC2T): opcode 0x12, rs=0x08 or 0x09
|
||||
if (opcode == 0x12) {
|
||||
uint32_t rs = (instr >> 21) & 0x1F;
|
||||
if (rs == 0x08 || rs == 0x09) return true; // bc2f, bc2t
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -170,6 +176,23 @@ ControlFlowInstr ControlFlowAnalyzer::analyze_instruction(uint32_t addr, uint32_
|
||||
cf.has_delay_slot = true;
|
||||
cf.mnemonic = "jal";
|
||||
break;
|
||||
|
||||
case 0x12: // COP2 branches (BC2F, BC2T)
|
||||
{
|
||||
uint32_t cop2_rs = (instr >> 21) & 0x1F;
|
||||
if (cop2_rs == 0x08) { // BC2F
|
||||
cf.type = ControlFlowType::Branch;
|
||||
cf.target = get_branch_target(addr, instr);
|
||||
cf.has_delay_slot = true;
|
||||
cf.mnemonic = "bc2f";
|
||||
} else if (cop2_rs == 0x09) { // BC2T
|
||||
cf.type = ControlFlowType::Branch;
|
||||
cf.target = get_branch_target(addr, instr);
|
||||
cf.has_delay_slot = true;
|
||||
cf.mnemonic = "bc2t";
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return cf;
|
||||
|
||||
@@ -24,15 +24,15 @@ bool FunctionAnalyzer::is_jr_ra(uint32_t instr) {
|
||||
}
|
||||
|
||||
bool FunctionAnalyzer::is_prologue(uint32_t instr, int32_t& stack_size) {
|
||||
// addiu $sp, $sp, -N
|
||||
// Format: 001001 11101 11101 <16-bit signed immediate>
|
||||
// Opcode: 0x27 (addiu), rs=$sp (29), rt=$sp (29)
|
||||
// addiu/addi $sp, $sp, -N
|
||||
// Format: 001001/001000 11101 11101 <16-bit signed immediate>
|
||||
// Opcode: 0x09 (addiu) or 0x08 (addi), rs=$sp (29), rt=$sp (29)
|
||||
uint32_t opcode = (instr >> 26) & 0x3F;
|
||||
uint32_t rs = (instr >> 21) & 0x1F;
|
||||
uint32_t rt = (instr >> 16) & 0x1F;
|
||||
int16_t imm = (int16_t)(instr & 0xFFFF);
|
||||
|
||||
if (opcode == 0x09 && rs == 29 && rt == 29 && imm < 0) {
|
||||
if ((opcode == 0x09 || opcode == 0x08) && rs == 29 && rt == 29 && imm < 0) {
|
||||
stack_size = -imm; // Store positive stack frame size
|
||||
return true;
|
||||
}
|
||||
@@ -40,13 +40,13 @@ bool FunctionAnalyzer::is_prologue(uint32_t instr, int32_t& stack_size) {
|
||||
}
|
||||
|
||||
bool FunctionAnalyzer::is_epilogue(uint32_t instr, int32_t& stack_size) {
|
||||
// addiu $sp, $sp, +N
|
||||
// addiu/addi $sp, $sp, +N
|
||||
uint32_t opcode = (instr >> 26) & 0x3F;
|
||||
uint32_t rs = (instr >> 21) & 0x1F;
|
||||
uint32_t rt = (instr >> 16) & 0x1F;
|
||||
int16_t imm = (int16_t)(instr & 0xFFFF);
|
||||
|
||||
if (opcode == 0x09 && rs == 29 && rt == 29 && imm > 0) {
|
||||
if ((opcode == 0x09 || opcode == 0x08) && rs == 29 && rt == 29 && imm > 0) {
|
||||
stack_size = imm;
|
||||
return true;
|
||||
}
|
||||
@@ -64,6 +64,7 @@ bool FunctionAnalyzer::is_valid_mips_word(uint32_t instr) {
|
||||
switch (funct) {
|
||||
case 0x00u: case 0x02u: case 0x03u: case 0x04u:
|
||||
case 0x06u: case 0x07u: case 0x08u: case 0x09u:
|
||||
case 0x0Au: case 0x0Bu: // MOVN, MOVZ
|
||||
case 0x0Cu: case 0x0Du:
|
||||
case 0x10u: case 0x11u: case 0x12u: case 0x13u:
|
||||
case 0x18u: case 0x19u: case 0x1Au: case 0x1Bu:
|
||||
@@ -76,7 +77,8 @@ bool FunctionAnalyzer::is_valid_mips_word(uint32_t instr) {
|
||||
}
|
||||
}
|
||||
if (opcode == 0x01u) {
|
||||
return rt == 0x00u || rt == 0x01u || rt == 0x10u || rt == 0x11u;
|
||||
return rt == 0x00u || rt == 0x01u || rt == 0x02u || rt == 0x03u ||
|
||||
rt == 0x10u || rt == 0x11u || rt == 0x12u || rt == 0x13u;
|
||||
}
|
||||
|
||||
switch (opcode) {
|
||||
@@ -85,6 +87,7 @@ bool FunctionAnalyzer::is_valid_mips_word(uint32_t instr) {
|
||||
case 0x08u: case 0x09u: case 0x0Au: case 0x0Bu:
|
||||
case 0x0Cu: case 0x0Du: case 0x0Eu: case 0x0Fu:
|
||||
case 0x10u: case 0x12u:
|
||||
case 0x14u: case 0x15u: case 0x16u: case 0x17u: // branch-likely
|
||||
case 0x20u: case 0x21u: case 0x22u: case 0x23u:
|
||||
case 0x24u: case 0x25u: case 0x26u:
|
||||
case 0x28u: case 0x29u: case 0x2Au: case 0x2Bu:
|
||||
@@ -102,6 +105,8 @@ bool FunctionAnalyzer::is_branch_or_jump(uint32_t instr) {
|
||||
if (opcode == 0x02 || opcode == 0x03) return true;
|
||||
// BEQ, BNE, BLEZ, BGTZ
|
||||
if (opcode >= 0x04 && opcode <= 0x07) return true;
|
||||
// BEQL, BNEL, BLEZL, BGTZL (branch-likely)
|
||||
if (opcode >= 0x14 && opcode <= 0x17) return true;
|
||||
// REGIMM: BLTZ, BGEZ, BLTZAL, BGEZAL
|
||||
if (opcode == 0x01) return true;
|
||||
// SPECIAL: JR, JALR
|
||||
@@ -117,7 +122,7 @@ bool FunctionAnalyzer::is_branch_or_jump(uint32_t instr) {
|
||||
static bool is_load_imm_zero_u16(uint32_t instr, uint32_t& rt_out, uint32_t& imm_out) {
|
||||
uint32_t opcode = (instr >> 26) & 0x3F;
|
||||
uint32_t rs = (instr >> 21) & 0x1F;
|
||||
if ((opcode != 0x09 && opcode != 0x0D) || rs != 0) {
|
||||
if ((opcode != 0x09 && opcode != 0x08 && opcode != 0x0D) || rs != 0) {
|
||||
return false;
|
||||
}
|
||||
rt_out = (instr >> 16) & 0x1F;
|
||||
@@ -224,7 +229,7 @@ static bool is_load_from_reg_base(uint32_t instr, uint32_t base_reg, uint32_t& r
|
||||
static bool is_load_imm_zero(uint32_t instr, uint32_t& rt_out) {
|
||||
uint32_t opcode = (instr >> 26) & 0x3F;
|
||||
uint32_t rs = (instr >> 21) & 0x1F;
|
||||
if ((opcode != 0x09 && opcode != 0x0D) || rs != 0) return false;
|
||||
if ((opcode != 0x09 && opcode != 0x08 && opcode != 0x0D) || rs != 0) return false;
|
||||
rt_out = (instr >> 16) & 0x1F;
|
||||
return rt_out != 0;
|
||||
}
|
||||
@@ -410,7 +415,7 @@ bool FunctionAnalyzer::is_likely_data_section(uint32_t start_addr, uint32_t end_
|
||||
static const bool valid_opcode[64] = {
|
||||
true, true, true, true, true, true, true, true, // 0x00-0x07
|
||||
true, true, true, true, true, true, true, true, // 0x08-0x0F
|
||||
true, false, true, false, false, false, false, false, // 0x10-0x17 (COP0=0x10, COP2=0x12)
|
||||
true, false, true, false, true, true, true, true, // 0x10-0x17 (COP0=0x10, COP2=0x12, branch-likely=0x14-0x17)
|
||||
false, false, false, false, false, false, false, false, // 0x18-0x1F
|
||||
true, true, true, true, true, true, true, false, // 0x20-0x27
|
||||
true, true, true, true, false, false, true, false, // 0x28-0x2F
|
||||
@@ -490,7 +495,7 @@ static bool exact_is_addiu_sp_neg(uint32_t instr) {
|
||||
uint32_t rs = (instr >> 21) & 0x1Fu;
|
||||
uint32_t rt = (instr >> 16) & 0x1Fu;
|
||||
int16_t imm = static_cast<int16_t>(instr & 0xFFFFu);
|
||||
return opcode == 0x09u && rs == 29u && rt == 29u && imm < 0;
|
||||
return (opcode == 0x09u || opcode == 0x08u) && rs == 29u && rt == 29u && imm < 0;
|
||||
}
|
||||
|
||||
static bool exact_is_valid_mips_word(uint32_t instr) {
|
||||
@@ -536,6 +541,12 @@ static ExactCf exact_classify_cf(uint32_t pc, uint32_t instr) {
|
||||
cf.target = exact_branch_target(pc, instr);
|
||||
return cf;
|
||||
}
|
||||
// COP2 branches (BC2F, BC2T): opcode 0x12, rs=0x08/0x09
|
||||
if (opcode == 0x12u && (rs == 0x08u || rs == 0x09u)) {
|
||||
cf.kind = ExactCfKind::Branch;
|
||||
cf.target = exact_branch_target(pc, instr);
|
||||
return cf;
|
||||
}
|
||||
return cf;
|
||||
}
|
||||
|
||||
@@ -1166,7 +1177,7 @@ FunctionAnalysisResult FunctionAnalyzer::analyze() {
|
||||
static const bool valid[64] = {
|
||||
true, true, true, true, true, true, true, true, // 0x00-0x07
|
||||
true, true, true, true, true, true, true, true, // 0x08-0x0F
|
||||
true, false, true, false, false, false, false, false, // 0x10-0x17
|
||||
true, false, true, false, true, true, true, true, // 0x10-0x17
|
||||
false, false, false, false, false, false, false, false, // 0x18-0x1F
|
||||
true, true, true, true, true, true, true, false, // 0x20-0x27
|
||||
true, true, true, true, false, false, true, false, // 0x28-0x2F
|
||||
|
||||
@@ -65,6 +65,8 @@ void MipsDecoder::decode_special(DecodedInstruction& out) {
|
||||
case 0x25: out.mnemonic = "OR"; break;
|
||||
case 0x26: out.mnemonic = "XOR"; break;
|
||||
case 0x27: out.mnemonic = "NOR"; break;
|
||||
case 0x0A: out.mnemonic = "MOVN"; break;
|
||||
case 0x0B: out.mnemonic = "MOVZ"; break;
|
||||
case 0x2A: out.mnemonic = "SLT"; break;
|
||||
case 0x2B: out.mnemonic = "SLTU"; break;
|
||||
default:
|
||||
@@ -82,8 +84,12 @@ void MipsDecoder::decode_regimm(DecodedInstruction& out) {
|
||||
switch (out.rt) {
|
||||
case 0x00: out.mnemonic = "BLTZ"; break;
|
||||
case 0x01: out.mnemonic = "BGEZ"; break;
|
||||
case 0x02: out.mnemonic = "BLTZL"; break;
|
||||
case 0x03: out.mnemonic = "BGEZL"; break;
|
||||
case 0x10: out.mnemonic = "BLTZAL"; break;
|
||||
case 0x11: out.mnemonic = "BGEZAL"; break;
|
||||
case 0x12: out.mnemonic = "BLTZALL"; break;
|
||||
case 0x13: out.mnemonic = "BGEZALL"; break;
|
||||
default:
|
||||
out.mnemonic = "REGIMM?";
|
||||
out.format = InstrFormat::UNKNOWN;
|
||||
@@ -119,6 +125,8 @@ void MipsDecoder::decode_cop2(DecodedInstruction& out) {
|
||||
case 0x02: out.mnemonic = "CFC2"; break;
|
||||
case 0x04: out.mnemonic = "MTC2"; break;
|
||||
case 0x06: out.mnemonic = "CTC2"; break;
|
||||
case 0x08: out.mnemonic = "BC2F"; break;
|
||||
case 0x09: out.mnemonic = "BC2T"; break;
|
||||
default:
|
||||
// CO bit (bit 25) set => GTE command word
|
||||
if (out.rs & 0x10) out.mnemonic = "GTE";
|
||||
@@ -157,6 +165,10 @@ void MipsDecoder::set_classification_flags(DecodedInstruction& out) {
|
||||
out.is_jump = true;
|
||||
out.is_delay_slot_user = true;
|
||||
break;
|
||||
case 0x0A: // MOVN
|
||||
case 0x0B: // MOVZ
|
||||
out.is_alu = true;
|
||||
break;
|
||||
case 0x0C: out.is_syscall = true; break;
|
||||
case 0x0D: out.is_break = true; break;
|
||||
default: out.is_alu = true; break;
|
||||
@@ -184,6 +196,10 @@ void MipsDecoder::set_classification_flags(DecodedInstruction& out) {
|
||||
case 0x05: // BNE
|
||||
case 0x06: // BLEZ
|
||||
case 0x07: // BGTZ
|
||||
case 0x14: // BEQL
|
||||
case 0x15: // BNEL
|
||||
case 0x16: // BLEZL
|
||||
case 0x17: // BGTZL
|
||||
out.is_branch = true;
|
||||
out.is_delay_slot_user = true;
|
||||
break;
|
||||
@@ -223,7 +239,15 @@ void MipsDecoder::set_classification_flags(DecodedInstruction& out) {
|
||||
break;
|
||||
|
||||
// COP0, COP2: neither load/store/alu/branch/jump
|
||||
// except BC2F/BC2T which are COP2 conditional branches
|
||||
default:
|
||||
if (out.opcode == 0x12) {
|
||||
uint32_t cop2_rs = (out.raw >> 21) & 0x1F;
|
||||
if (cop2_rs == 0x08 || cop2_rs == 0x09) { // BC2F, BC2T
|
||||
out.is_branch = true;
|
||||
out.is_delay_slot_user = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -270,6 +294,10 @@ DecodedInstruction MipsDecoder::decode(uint32_t instr, uint32_t address) {
|
||||
case 0x0F: out.mnemonic = "LUI"; out.format = InstrFormat::I; break;
|
||||
case 0x10: decode_cop0(out); break;
|
||||
case 0x12: decode_cop2(out); break;
|
||||
case 0x14: out.mnemonic = "BEQL"; out.format = InstrFormat::I; break;
|
||||
case 0x15: out.mnemonic = "BNEL"; out.format = InstrFormat::I; break;
|
||||
case 0x16: out.mnemonic = "BLEZL"; out.format = InstrFormat::I; break;
|
||||
case 0x17: out.mnemonic = "BGTZL"; out.format = InstrFormat::I; break;
|
||||
case 0x20: out.mnemonic = "LB"; out.format = InstrFormat::I; break;
|
||||
case 0x21: out.mnemonic = "LH"; out.format = InstrFormat::I; break;
|
||||
case 0x22: out.mnemonic = "LWL"; out.format = InstrFormat::I; break;
|
||||
@@ -297,8 +325,10 @@ DecodedInstruction MipsDecoder::decode(uint32_t instr, uint32_t address) {
|
||||
}
|
||||
if (out.opcode == 0x04 || out.opcode == 0x05 ||
|
||||
out.opcode == 0x06 || out.opcode == 0x07 ||
|
||||
out.opcode == 0x01) {
|
||||
// BEQ/BNE/BLEZ/BGTZ + REGIMM (BLTZ/BGEZ/etc.)
|
||||
out.opcode == 0x01 ||
|
||||
out.opcode == 0x14 || out.opcode == 0x15 ||
|
||||
out.opcode == 0x16 || out.opcode == 0x17) {
|
||||
// BEQ/BNE/BLEZ/BGTZ + REGIMM (BLTZ/BGEZ/etc.) + branch-likely variants
|
||||
out.branch_target = compute_branch_target(address, out.imm16);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
[game]
|
||||
name = "Tomba!"
|
||||
id = "SCUS-94236"
|
||||
exe = "tomba/SCUS_942.36"
|
||||
disc = "tomba/tomba.cue"
|
||||
load_address = "0x80010000"
|
||||
entry_pc = "0x8006B58C"
|
||||
text_size = "0x00088000"
|
||||
stack_base = "0x801FFFF0"
|
||||
|
||||
[recompiler]
|
||||
seeds = "seeds/ghidra_funcs.txt"
|
||||
bios_thunks = "seeds/tomba_bios_thunks.txt"
|
||||
out_dir = "generated"
|
||||
strict = true
|
||||
|
||||
[runtime]
|
||||
debug_port = 4470
|
||||
window_title = "Tomba! Recompiled"
|
||||
memcard_dir = "saves"
|
||||
disc_speed = "1x" # authentic CD timing — never perturbs game timing/audio
|
||||
# BIOS backend (CLAUDE.md §0 amendment 2026-07-02). Default false = LLE (the
|
||||
# recompiled BIOS, the reference implementation + oracle). true = HLE tier:
|
||||
# implemented kernel services (the B0 event family in v1) computed in-runtime
|
||||
# against the real guest kernel structures, everything else falling through to
|
||||
# LLE — and the BIOS shell (boot animation) skipped one-shot with kernel init +
|
||||
# EXE load still run by the real recompiled BIOS at host speed. Env overrides:
|
||||
# PSX_BIOS_HLE / PSX_BIOS_HLE_KEEP_INTRO. Observability: hle_dump TCP command.
|
||||
bios_hle = true
|
||||
bios_hle_keep_intro = false # HLE calls but keep the real boot intro
|
||||
# DEPRECATED: fast_boot is now an alias for the HLE boot shell-skip alone (the
|
||||
# old first-boot snapshot capture/restore mechanism is gone).
|
||||
fast_boot = false # set false to watch BIOS logos
|
||||
overlay_cache = true # native overlay-DLL cache (production path; gcc > tcc > interp)
|
||||
# Tier selection: "auto" (default) = gcc if a gcc toolchain is present (dev /
|
||||
# production shard authoring), else the bundled toolchain-free tcc. Other values:
|
||||
# "gcc", "tcc", "auto-no-gcc" (dev: force tcc even with gcc present, to test the
|
||||
# user path). Priority is static > gcc shard > tcc shard > interp. The deprecated
|
||||
# sljit tier is retired (removed in the 1915b5c framework refactor).
|
||||
overlay_backend = "auto"
|
||||
# Fast loads, done the safe way. disc_speed stays authentic 1x (speeding the
|
||||
# guest-visible CD device changes how many VBlanks/callbacks fall between the
|
||||
# game's state transitions, which desyncs CD-streamed audio and can wedge
|
||||
# timing-sensitive titles). The real speed comes from turbo_loads: while a CD
|
||||
# load is in progress (cdrom_load_in_progress: a sustained read, XA/FMV excluded,
|
||||
# post-handoff), the frontend drops wall-clock pacing so the guest runs every
|
||||
# authentic frame at host speed. All guest timing is preserved; only real-world
|
||||
# load time shrinks. Audio plays through (no mute). Launcher Settings -> Turbo
|
||||
# loads persists to settings.toml and overrides this.
|
||||
turbo_loads = true
|
||||
# Step 2.8: auto capture+compile of uncovered overlay variants (background;
|
||||
# run via cmd.exe /C with cwd = project root; needs python + gcc on PATH).
|
||||
# The compile's WRITE cache + READ captures are NOT specified here: the runtime
|
||||
# injects PSX_OVERLAY_CACHE_DIR / PSX_OVERLAY_CAPTURES (the loader's canonical
|
||||
# <exe>/cache and <exe>/overlay_captures.json) so the write can never drift from
|
||||
# where the loader reads (the framework owns the cache location, uniform for all
|
||||
# games, dev or prod). This command is just the compile RECIPE (which compiler,
|
||||
# tool paths). Coverage-vault backup is a separate manual dev step now (it must
|
||||
# not fail-couple the autocompile rescan).
|
||||
overlay_autocompile_cmd = "python F:/Projects/psxrecomp/psxrecomp/tools/compile_overlays.py --game-toml game.toml --recompiler F:/Projects/psxrecomp/psxrecomp/recompiler/build/psxrecomp-game.exe --runtime-include F:/Projects/psxrecomp/psxrecomp/runtime/include --gcc C:/msys64/mingw64/bin/gcc.exe --cps"
|
||||
|
||||
[video]
|
||||
# Visual enhancement options.
|
||||
# supersampling: internal-resolution SSAA factor (1 = native PSX, 2..4 render
|
||||
# the scene at N* the resolution and downsample on present -> true
|
||||
# supersampling + edge anti-aliasing). 2 is a good quality/perf default;
|
||||
# raise to 3 or 4 for sharper edges if your CPU holds 59.94 fps.
|
||||
# antialiasing: linear present filtering (smooths the downscale and any window
|
||||
# resize). Set false for sharp, unfiltered pixels.
|
||||
# texture_filtering: "nearest" (native PSX look) or "bilinear" (smooths
|
||||
# textures and 2D backgrounds). Off by default.
|
||||
# renderer: "software" (CPU rasterizer) or "opengl" (GPU-authoritative VRAM
|
||||
# FBO backend). OpenGL moves rasterization + supersampling onto the GPU, so
|
||||
# fill-heavy scenes (e.g. the mushroom forest) hold 59.94 fps even at
|
||||
# supersampling 2..4. Falls back to software automatically if GL init fails.
|
||||
# aspect_ratio: display aspect "W:H" ("4:3" = native, the default). "16:9"
|
||||
# enables the EXPERIMENTAL widescreen hack: the GTE projection is squashed
|
||||
# horizontally and the frame is presented stretched to 16:9, revealing extra
|
||||
# world at the screen edges. Tomba's world is GTE-projected 3D, so geometry
|
||||
# keeps correct proportions; screen-space 2D (HUD, sprite quads, FMV) looks
|
||||
# stretched, and world-space object culling can pop near the edges. Also
|
||||
# selectable in the launcher (Settings -> Aspect ratio), which persists to
|
||||
# settings.toml and overrides this value.
|
||||
supersampling = 2
|
||||
antialiasing = true
|
||||
texture_filtering = "nearest"
|
||||
renderer = "opengl"
|
||||
# auto_skip_fmv: auto-skip full-motion videos. When a streaming FMV is detected,
|
||||
# the runtime ends it via the game's OWN end-of-movie path (see fmv_skip_* below):
|
||||
# it writes the current movie's per-movie frame-total down so Tomba's MDEC player
|
||||
# tears the movie down on its next frame. Works for EVERY movie (Whoopee Camp
|
||||
# logo, opening, in-game cutscene movies) including ones the game won't let you
|
||||
# skip with a button. Ships OFF; the launcher's Settings → "Skip FMVs" toggle
|
||||
# turns it on (persisted to settings.toml).
|
||||
auto_skip_fmv = false
|
||||
# fmv_skip_total_table: base of Tomba's per-movie frame-total table (u16 per movie,
|
||||
# indexed by the current-movie-id byte). fmv_skip_movie_id: that id byte (PSX
|
||||
# scratchpad). fmv_skip_end_total: value written into the active movie's total so
|
||||
# the player ends it immediately (its loop tears down when frame# >= total - 3).
|
||||
# RE: player loop FUN_8001f1c0 / consumer FUN_8001efe8 (see ENHANCEMENTS.md E1).
|
||||
fmv_skip_total_table = 0x80077728
|
||||
fmv_skip_movie_id = 0x1F8001CD
|
||||
fmv_skip_end_total = 3
|
||||
aspect_ratio = "4:3"
|
||||
|
||||
# Controller input defaults. Tomba is analog-capable (the box reads "Analog
|
||||
# Control Compatible"): in analog mode the left stick gives variable run speed.
|
||||
# default_analog presents a DualShock/analog pad (id 0x73) out of the box so that
|
||||
# path is live without toggling DualShock in the launcher — the left stick is
|
||||
# variable, and the D-pad / keyboard arrows are folded onto the analog axes at
|
||||
# full deflection so they still move the character. This is the "seamless d-pad /
|
||||
# analog" behaviour of the Special Edition. The launcher's Player cards expose a
|
||||
# per-port DualShock toggle and Settings → Controller a deadzone control; both
|
||||
# persist to settings.toml and override these. deadzone is the analog centre
|
||||
# dead-band / stick->d-pad threshold in raw SDL axis units (0..32767; 12000≈37%).
|
||||
[controller]
|
||||
default_analog = true
|
||||
deadzone = 12000
|
||||
# LEGACY pad-config compatibility (Tomba "Hybrid" controller).
|
||||
#
|
||||
# The launcher "Hybrid" mode — our first controller implementation, built to
|
||||
# reproduce Tomba: Special Edition's seamless analog/digital feel — flips the
|
||||
# emulated pad's reported type (digital 0x41 <-> DualShock/analog 0x73) as you
|
||||
# move between the d-pad and the stick. That flip worked when the SIO pad answered
|
||||
# the DualShock config commands trivially (always config id 0xF3).
|
||||
#
|
||||
# We then matured the pad against Mega Man X6, which needs a REAL config-mode
|
||||
# state machine to probe the pad before polling (the trivial "always 0xF3" answer
|
||||
# wedged it / killed its input). That modern SM became the default. Under it,
|
||||
# Tomba's Hybrid flip regressed: a type change makes libpad re-detect the pad and
|
||||
# manufacture a 1-frame "pad unplugged", which Tomba reads as a disconnect (menu
|
||||
# unpause / phantom input) and MMX6 reads as a phantom dash. We could not make the
|
||||
# modern SM keep the flip benign, so this per-game opt-in restores the original
|
||||
# "always 0xF3" answers for Tomba ONLY — making the Hybrid flip benign exactly as
|
||||
# the real game expects. Every other title (MMX6, etc.) keeps the modern SM.
|
||||
#
|
||||
# This is LEGACY: once the controller re-detect is refactored so the flip is
|
||||
# benign for every game (real hardware tolerates unlimited analog-button presses),
|
||||
# this opt-in and its runtime machinery should be removed. See psxrecomp
|
||||
# runtime/src/sio.c g_pad_legacy_cfg for the full story. Default off.
|
||||
legacy_pad_config = true
|
||||
|
||||
# Widescreen proportion hooks (inert at 4:3; engaged when aspect_ratio is
|
||||
# wide). Evidence: Ghidra pass 2026-06-12 — every character/billboard render
|
||||
# function (RTPS cluster 0x800459E0-0x8004FB54, both the rotatable-POLY_FT4
|
||||
# and multi-part-SPRT patterns) calls the shared per-prim helper 0x8005E08C
|
||||
# with the prim pointer in $a0, after the RTPS preamble stores the projected
|
||||
# anchor SXY to scratchpad 0x1F800070. Sprite sizes are fixed pixels (never
|
||||
# depth-scaled). The HUD/menus build SPRT prims with no GTE involvement.
|
||||
[widescreen]
|
||||
sprite_tag_funcs = ["0x8005E08C"]
|
||||
sprite_anchor_addr = "0x1F800070"
|
||||
hud_sprt_squash = true
|
||||
# Native-wide HUD-at-16:9-corners is DEFERRED (see psxrecomp ENHANCEMENTS.md,
|
||||
# "Tomba HUD corner re-anchoring"): the plain thirds shift tears composites
|
||||
# that span zones (in-world dialog boxes split their end caps to the screen
|
||||
# edges) and the AP counter renders through the TAGGED sprite funnel so it
|
||||
# doesn't move. Needs composite-group awareness before it can ship.
|
||||
nw_hud_corners = false
|
||||
|
||||
# World-space draw-cull widening. The per-object classifier family tests
|
||||
# (objX - camX + BIAS) <u RANGE, where BIAS/RANGE are sized for the 4:3 screen
|
||||
# (320 + 64px margins). The GTE squash shows ~33% more world, collapsing the
|
||||
# effective margin to ~11px so objects pop near the wide-screen edges. The
|
||||
# recompiler emits these immediates with + psx_ws_x_margin() (bias) / + 2x
|
||||
# (range), which is 0 at 4:3 so the original cull is byte-identical. Sites are
|
||||
# Ghidra-evidenced (classifier family 0x80022C08-0x80023164; camX=0x1F800176).
|
||||
[widescreen.cull]
|
||||
# addiu rT,rS,imm → rT = rS + (imm + margin)
|
||||
bias_sites = ["0x80022E78", "0x80022F94", "0x80022FE8", "0x80023068"]
|
||||
# sltiu rT,rS,imm → rT = rS <u (imm + 2*margin)
|
||||
range_sites = ["0x80022E80", "0x80022F9C", "0x80022FF0", "0x80023070"]
|
||||
# nop (delay slot) → a1 += margin (caller-supplied-margin classifier variants)
|
||||
a1_sites = ["0x80022C34", "0x80022D68", "0x800230FC"]
|
||||
|
||||
# Automatic horizontal-FOV cull widening (the dominant fix for native-wide /
|
||||
# Issue #8). The GTE render funnel (RTPT tri/quad emitters ~0x80024CFC.. and
|
||||
# RTPS billboard emitters ~0x800487A4..) rejects a primitive only when ALL its
|
||||
# vertices fall off the 4:3 frame: a per-vertex `sltiu vN, SX, 0x140` (right
|
||||
# edge) paired with `sltiu vN, SY, 0xE0` (bottom edge) in the same function.
|
||||
# That hard 320 clip — not the world-space classifier above (which already
|
||||
# carries a ±64px margin and is a proven no-op for 16:9) — is what leaves the
|
||||
# revealed right margin empty. Rather than list all ~100 identical sites, the
|
||||
# recompiler auto-detects that signature and emits every width compare with
|
||||
# + 2*psx_ws_x_margin(), so geometry out to the wide-surface edge is submitted
|
||||
# instead of culled at 320. Generic to any PSX GTE title; 0 at 4:3 ⇒ the build
|
||||
# is byte-identical with widescreen off. Changing this requires a regen.
|
||||
auto_screen_x = true
|
||||
|
||||
# Far-backdrop column PRELOAD. The scrolling-background tile-grid generators
|
||||
# (sky / ground / cloud / flower-field layers) emit only a camera-windowed ~4:3
|
||||
# range of tile columns per frame, so the 16:9-revealed right/left margin shows
|
||||
# void until the camera moves. The recompiler auto-detects each column-window
|
||||
# generator by its invariant (/96 magic 0x66666667 dividing the 0x176 camera-X,
|
||||
# the sra-by-5 divide tail, and the move/addiu loop-bound finalize) and rewrites
|
||||
# the window START to 0 and END high; the generator's own low/high clamps then
|
||||
# pin the loop to the whole finite tile row, so the entire backdrop is emitted
|
||||
# regardless of camera. Generators are OVERLAY-resident (so the overlay compile
|
||||
# must see this via --ws-config). Generic to any PSX scroller; 0 at 4:3 ⇒
|
||||
# byte-identical with widescreen off. Changing this requires a regen + overlay
|
||||
# cache rebuild.
|
||||
auto_backdrop = true
|
||||
|
||||
# Parallax 2D backdrop layer (ocean / cloud / mountain / grassy-top). These
|
||||
# OVERLAY actor handlers compute screenX = (worldX - camX) >> parallax in pure
|
||||
# integer math and store it to the object's screen-X field (+0x12) WITHOUT the
|
||||
# GTE, so the GTE X-squash that gives 3D the wider 16:9 FOV never reaches them:
|
||||
# far pieces sit past the 320px framebuffer edge and are clipped, showing as the
|
||||
# blue void / half-drawn rectangles at the screen edges (Issue #8C). Each site
|
||||
# is the FINAL `sh rt,0x12(base)` store in a backdrop handler; the recompiler
|
||||
# emits it as write_half(.., psx_ws_backdrop_x(rt)) to squash screenX around the
|
||||
# screen centre by the same factor as the GTE (identity at 4:3). Overlay code,
|
||||
# so compile_overlays.py forwards this via --ws-config; changing it needs a
|
||||
# regen + overlay-cache rebuild. Each handler must be VERIFIED to draw far
|
||||
# backdrop (NOT UI) before enabling — a parallax+camera signature ALSO matches
|
||||
# screen-space UI positioned relative to the camera:
|
||||
# 0x801216BC type0 store 0x801217B4 (>>3, single prim) = far backdrop ✓
|
||||
# 0x80121810 type1 store 0x8012196C (>>1, animated tex) = midground row ✓
|
||||
# Draws a row of 4 evenly-spaced parallax pieces (worldX 540/680/820/960,
|
||||
# fixed screenY 130) that stay active with NO dialogue on screen — proven
|
||||
# backdrop, not the dialogue box. The a3 ±0x40 "tiling loop" only CYCLES the
|
||||
# texpage/CLUT word at +0x8 (animated-texture shimmer), it does not lay out
|
||||
# text tiles. 8C right-edge culling (slots 4/5) is this row clipping while
|
||||
# unsquashed; enabling the squash pulls them in (slot4 410→316 on-screen).
|
||||
# NB: 8F's dialogue-split is the SEPARATE sprite_tag/hud_sprt_squash path,
|
||||
# NOT this handler — an earlier session mis-blamed 0x80121810 for it.
|
||||
# (Data-confirmed firing; live in-scene dialogue smoke-check still pending.)
|
||||
# 0x80121A74 type2 store 0x80121BAC (>>3 + clamps) = UNVERIFIED
|
||||
# far-right area gate (camX>=0x719, not active in start area); left out
|
||||
# until confirmed backdrop vs UI.
|
||||
[widescreen.backdrop]
|
||||
x_sites = ["0x801217B4", "0x8012196C"]
|
||||
|
||||
# Far-backdrop GTE un-squash (8C). The far parallax backdrop (ocean / cloud /
|
||||
# distant mountain) is GTE-projected 3D drawn by FUN_8004db3c (sole caller of
|
||||
# the GTE emitter FUN_80027600). The global widescreen GTE X-squash repositions
|
||||
# it toward centre, so its 4:3-extent geometry falls short of the revealed 16:9
|
||||
# edges -> dark-blue void that "pops" as you walk (each layer crosses its 4:3
|
||||
# coverage threshold). A/B (ws_aspect 16/9 vs 1/1, same camX) confirmed: squash
|
||||
# OFF fills the frame, squash ON reveals beyond the backdrop's extent. Fix:
|
||||
# bracket the driver with gte_ws_set_suppress(1)/(0) so the far backdrop draws
|
||||
# UN-squashed (fills the stretched frame, skybox-style) while the near 3D world
|
||||
# keeps the wider FOV. Main-EXE address -> regen-class (native emit fires in dev).
|
||||
unsquash_funcs = ["0x8004DB3C"]
|
||||
|
||||
# ── Audit-specific config (consumed by psxrecomp/tools/*.py and tests) ──
|
||||
#
|
||||
# Schema mirrors psxrecomp/bios/SCPH1001.toml. Paths are relative to the
|
||||
# TombaRecomp project root.
|
||||
|
||||
[audit]
|
||||
# Dynamic discovery — no Ghidra function-starts file for Tomba; the
|
||||
# recompiler discovers functions by walking control flow from entry_pc.
|
||||
# function_starts left absent on purpose.
|
||||
|
||||
# Code region: Tomba's text segment is contiguous from load_address for
|
||||
# text_size bytes. Single region keeps L1/coverage audits scoped to code.
|
||||
[[audit.regions]]
|
||||
name = "Text"
|
||||
rom_start = "0x800" # offset within the PS-X EXE file (skip header)
|
||||
rom_end = "0x88800" # rom_start + text_size
|
||||
vaddr_base = "0x80010000"
|
||||
|
||||
# Address normalization: just KSEG masking. No remaps (game loads
|
||||
# contiguously at vaddr_base; no kernel-style relocation).
|
||||
[audit.normalize]
|
||||
kseg_mask = "0x1FFFFFFF"
|
||||
@@ -53,8 +53,12 @@ enum {
|
||||
AUDIO_EV_UNDERRUN = 4, /* host queue empty at pump entry; a=queued bytes */
|
||||
AUDIO_EV_MUTE = 5, /* turbo mute engaged; a=fade tail frames */
|
||||
AUDIO_EV_UNMUTE = 6, /* pumping resumed; a=fade-in frames pending */
|
||||
AUDIO_EV_CD_PUSH = 7, /* a=frames pushed, b=CD ring fill after */
|
||||
AUDIO_EV_CD_PUSH = 7, /* a=guest-cycle clock low32, b=ring fill after */
|
||||
AUDIO_EV_DMA_WRITE = 8, /* SPU RAM DMA; a=words, b=transfer addr after */
|
||||
AUDIO_EV_XA_ZERO = 9, /* zero-run in decoded XA PCM; a=lba,
|
||||
b=(stage<<28)|(start_frame<<14)|run_len.
|
||||
stage 0=post-ADPCM-decode (native rate),
|
||||
stage 1=post-resample+volume (44100). */
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -113,6 +113,16 @@ typedef struct CDROMDebugState {
|
||||
uint32_t last_sector_frame;
|
||||
uint8_t last_sector_mode;
|
||||
uint8_t last_sector_have_raw;
|
||||
/* Read-stream hold accounting: guest cycles the disc clock was frozen
|
||||
* waiting on guest INT-ack / buffer drain (unfaithful pause; real discs
|
||||
* never stop). Must stay 0 since the 2026-07-10 continuous-disc fix;
|
||||
* nonzero = regression. */
|
||||
uint64_t read_hold_cycles;
|
||||
uint64_t read_hold_events;
|
||||
/* One-deep pended data-ready INT1 accounting (Beetle SetAIP analog). */
|
||||
uint64_t int1_pended;
|
||||
uint64_t int1_lost;
|
||||
uint8_t int1_pending_now;
|
||||
} CDROMDebugState;
|
||||
|
||||
typedef struct CDROMSectorDebugState {
|
||||
|
||||
@@ -21,6 +21,8 @@ struct CPUState;
|
||||
#define IRQ_SIO1 8
|
||||
#define IRQ_SPU 9
|
||||
#define IRQ_PIO 10
|
||||
#define IRQ_MDEC_IN 11
|
||||
#define IRQ_MDEC_OUT 12
|
||||
|
||||
void interrupts_init(void);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
/**
|
||||
* ISO/BIN/CUE Reader for PS1 CD-ROM images
|
||||
@@ -19,6 +20,18 @@
|
||||
|
||||
namespace PS1 {
|
||||
|
||||
// PS1 CD-ROM sector size (Mode 2, Form 1 user data)
|
||||
constexpr size_t SECTOR_SIZE = 2048;
|
||||
|
||||
// Full sector size including headers/subchannel (2352 bytes for raw BIN files)
|
||||
constexpr size_t RAW_SECTOR_SIZE = 2352;
|
||||
|
||||
// Offset to user data in raw sector (Mode 2, Form 1)
|
||||
constexpr size_t RAW_DATA_OFFSET = 24;
|
||||
|
||||
// Primary Volume Descriptor location
|
||||
constexpr uint32_t PVD_SECTOR = 16;
|
||||
|
||||
/**
|
||||
* Information about a file entry in the ISO filesystem
|
||||
*/
|
||||
@@ -44,9 +57,10 @@ struct RootDirectoryInfo {
|
||||
* commands (GetTN/GetTD) must report them.
|
||||
*/
|
||||
struct CDTrack {
|
||||
int number; // 1-based track number
|
||||
int number; // 1-based track number
|
||||
bool is_audio; // true = CD-DA audio (Red Book); false = data
|
||||
uint32_t start_lba; // .bin-relative start LBA (cue INDEX 01; track 1 = 0)
|
||||
uint32_t start_lba; // absolute disc LBA (absolute from start of disc)
|
||||
std::string bin_path; // path to the .bin file for this track
|
||||
};
|
||||
|
||||
class ISOReader {
|
||||
@@ -187,12 +201,27 @@ private:
|
||||
*/
|
||||
std::vector<ISOFileEntry> ListFilesByLBA(uint32_t lba, uint32_t dir_size);
|
||||
|
||||
std::ifstream file_;
|
||||
/**
|
||||
* Find the track index for a given LBA
|
||||
* @param lba Logical Block Address
|
||||
* @return Index in tracks_ vector, or -1 if not found
|
||||
*/
|
||||
int FindTrackForLBA(uint32_t lba) const;
|
||||
|
||||
/**
|
||||
* Open a BIN file for a specific track
|
||||
* @param track_idx Index in tracks_ vector
|
||||
* @return true if opened successfully
|
||||
*/
|
||||
bool OpenTrackBIN(int track_idx);
|
||||
|
||||
std::ifstream file_;
|
||||
bool is_open_;
|
||||
std::string volume_id_;
|
||||
std::string bin_path_;
|
||||
RootDirectoryInfo root_dir_;
|
||||
std::vector<CDTrack> tracks_; // from the .cue TOC; >=1 entry after Open()
|
||||
};
|
||||
std::map<std::string, std::ifstream> bin_files_; // per-track BIN files
|
||||
};
|
||||
|
||||
} // namespace PS1
|
||||
|
||||
@@ -1134,7 +1134,7 @@ static void h_audio_events(int id, const char *json) {
|
||||
uint64_t total = audio_trace_events_total();
|
||||
static const char *kind_names[] = {
|
||||
"?", "REG", "RENDER", "SKIP", "UNDERRUN",
|
||||
"MUTE", "UNMUTE", "CD_PUSH", "DMA"
|
||||
"MUTE", "UNMUTE", "CD_PUSH", "DMA", "XA_ZERO"
|
||||
};
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"count\":%u,\"events\":[",
|
||||
id, (unsigned long long)total, (unsigned)got);
|
||||
|
||||
+201
-45
@@ -14,6 +14,7 @@
|
||||
#include "dma.h"
|
||||
#include "spu.h"
|
||||
#include "event_ring.h"
|
||||
#include "audio_trace.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#ifndef _WIN32
|
||||
@@ -169,8 +170,17 @@ static uint8_t filter_file;
|
||||
static uint8_t filter_channel;
|
||||
static uint8_t cd_muted;
|
||||
|
||||
#define XA_SUBHEADER_OFFSET 16
|
||||
static int cd_audio_sector_size; /* 2352 for raw audio sectors */
|
||||
|
||||
/* CD-DA (Red Book) audio playback state */
|
||||
static int cd_audio_playing;
|
||||
static int cd_audio_track;
|
||||
static int cd_audio_lba;
|
||||
static int cd_audio_end_lba;
|
||||
|
||||
/* FMV auto-skip detection hooks (cdrom.c / mdec.c). */
|
||||
#define XA_DATA_OFFSET 24
|
||||
#define XA_SUBHEADER_OFFSET 16
|
||||
#define XA_SOUND_GROUPS 18
|
||||
#define XA_NATIVE_FRAMES (XA_SOUND_GROUPS * 8 * 28)
|
||||
#define XA_MAX_44100_FRAMES 9408
|
||||
@@ -390,6 +400,19 @@ static void exec_command(uint8_t cmd);
|
||||
/* ISO reader */
|
||||
static void* iso_handle = NULL;
|
||||
|
||||
/* Convert LBA to track number (1-based, 0 if not found) */
|
||||
static int lba_to_track(int lba) {
|
||||
if (!iso_handle) return 0;
|
||||
int tracks = iso_track_count(iso_handle);
|
||||
for (int t = 1; t <= tracks; t++) {
|
||||
uint32_t start = iso_track_start_lba(iso_handle, t);
|
||||
uint32_t next_start = iso_track_start_lba(iso_handle, t + 1);
|
||||
if (next_start == 0) next_start = iso_sector_count(iso_handle);
|
||||
if (lba >= (int)start && lba < (int)next_start) return t;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static CDROMTraceEntry cdrom_trace[CDROM_TRACE_CAP];
|
||||
static uint64_t cdrom_trace_seq;
|
||||
|
||||
@@ -770,7 +793,31 @@ static int xa_resample_to_44100(const int16_t* in, int in_frames,
|
||||
return out_frames;
|
||||
}
|
||||
|
||||
static int maybe_deliver_xa_audio(const uint8_t* raw_data,
|
||||
/* Always-on XA zero-run scanner (audio_trace event ring). Decoded XA music
|
||||
* must not contain long exact-zero spans when the source sectors are dense;
|
||||
* a run here localizes corruption to a pipeline stage (stage 0 = straight
|
||||
* out of the ADPCM decoder at native rate, stage 1 = after resample +
|
||||
* decode-volume, i.e. exactly what spu_cd_audio_push receives). */
|
||||
static void xa_zero_scan(const int16_t *stereo, int frames, int lba,
|
||||
int stage) {
|
||||
int run = 0, start = 0;
|
||||
for (int i = 0; i <= frames; i++) {
|
||||
int z = (i < frames) && stereo[i * 2 + 0] == 0 && stereo[i * 2 + 1] == 0;
|
||||
if (z) {
|
||||
if (!run) start = i;
|
||||
run++;
|
||||
} else if (run) {
|
||||
if (run >= 64)
|
||||
audio_trace_event(AUDIO_EV_XA_ZERO, (uint32_t)lba,
|
||||
((uint32_t)stage << 28) |
|
||||
((uint32_t)(start & 0x3FFF) << 14) |
|
||||
(uint32_t)(run > 0x3FFF ? 0x3FFF : run));
|
||||
run = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int maybe_deliver_xa_audio(const uint8_t* raw_data, int lba,
|
||||
const CDROMSectorDelivery *delivery) {
|
||||
if (!(mode_reg & 0x40u) || !raw_data || !delivery || cd_muted) return 0;
|
||||
if (!xa_is_audio_realtime(delivery)) return 0;
|
||||
@@ -810,11 +857,13 @@ static int maybe_deliver_xa_audio(const uint8_t* raw_data,
|
||||
int native_frames = stereo
|
||||
? xa_decode_sector_4bit_stereo(raw_data + XA_DATA_OFFSET, native)
|
||||
: xa_decode_sector_4bit_mono(raw_data + XA_DATA_OFFSET, native);
|
||||
xa_zero_scan(native, native_frames, lba, 0);
|
||||
int out_frames = xa_resample_to_44100(native, native_frames, sample_rate,
|
||||
pcm_44100, XA_MAX_44100_FRAMES);
|
||||
/* Volume is applied after resampling, per PS1 hardware tests (Beetle
|
||||
* cdc.cpp GetCDAudio comment). */
|
||||
cd_apply_decode_volume(pcm_44100, out_frames);
|
||||
xa_zero_scan(pcm_44100, out_frames, lba, 1);
|
||||
spu_cd_audio_push(pcm_44100, out_frames);
|
||||
trace_cdrom('A', 0,
|
||||
((uint32_t)file << 24) | ((uint32_t)channel << 16) |
|
||||
@@ -845,7 +894,7 @@ static int read_sector_at(int min, int sec, int sect) {
|
||||
|
||||
delivery = classify_raw_sector(raw_data, have_raw);
|
||||
delivery.xa_audio_delivered =
|
||||
(uint8_t)maybe_deliver_xa_audio(raw_data, &delivery);
|
||||
(uint8_t)maybe_deliver_xa_audio(raw_data, lba, &delivery);
|
||||
delivery.data_delivered = 1;
|
||||
if (delivery.xa_audio_delivered ||
|
||||
((mode_reg & 0x08u) && xa_is_audio_realtime(&delivery))) {
|
||||
@@ -929,8 +978,31 @@ static void clear_sector_buffer(void) {
|
||||
request_reg &= (uint8_t)~CDROM_REQUEST_BFRD;
|
||||
}
|
||||
|
||||
/* One-deep asynchronous data-ready notification, mirroring Beetle
|
||||
* PS_CDC::SetAIP/CheckAIP (cdc.cpp:829,816): a data sector that comes due
|
||||
* while the guest still has an unacked controller INT does NOT stop disc
|
||||
* time — the sector buffer is overwritten on schedule (hardware clobbers
|
||||
* the FIFO the same way) and its INT1 pends here until the ack clears
|
||||
* irq_flag. If ANOTHER data sector lands while one is still pending, the
|
||||
* old notification is lost exactly like Beetle's "Previous notification
|
||||
* skipped" warning (counted, traced 'P'). */
|
||||
static uint8_t pending_dataready; /* 0/1: INT1 awaiting presentation */
|
||||
static uint8_t pending_dataready_stat; /* stat_reg snapshot at pend time */
|
||||
static uint64_t s_int1_pended; /* INT1s that had to wait for ack */
|
||||
static uint64_t s_int1_lost; /* pended INT1s replaced unseen */
|
||||
|
||||
/* Drive-state changes (Read/Play/Pause/Stop/Seek) cancel a pended
|
||||
* notification, matching Beetle's ClearAIP in every such command. */
|
||||
static void cdrom_clear_pending_dataready(void) {
|
||||
pending_dataready = 0;
|
||||
pending_dataready_stat = 0;
|
||||
}
|
||||
|
||||
static void start_read_stream(uint8_t cmd) {
|
||||
clear_sector_buffer();
|
||||
/* Drive-state change cancels any pended notification (Beetle clears
|
||||
* AIP on Play/Read/Pause/Stop/Seek alike). */
|
||||
cdrom_clear_pending_dataready();
|
||||
if (mode_reg & 0x40u) {
|
||||
xa_reset_decode();
|
||||
spu_cd_audio_reset();
|
||||
@@ -952,6 +1024,7 @@ static void stop_read_stream(void) {
|
||||
reading = 0;
|
||||
read_cmd = 0;
|
||||
read_delay = 0;
|
||||
cdrom_clear_pending_dataready();
|
||||
}
|
||||
|
||||
static int data_fifo_ready(void) {
|
||||
@@ -1138,19 +1211,23 @@ static void exec_command(uint8_t cmd) {
|
||||
pending.phase = 1;
|
||||
break;
|
||||
|
||||
case 0x08: /* Stop — stop the motor. Two-phase like Pause: INT3 (ACK) now
|
||||
* with the pre-stop status (motor still spinning), then a pending
|
||||
* INT2 (COMPLETE) after the motor spins down, reporting the new
|
||||
* status with the motor bit cleared (psx-spx "08h Stop").
|
||||
*
|
||||
* Previously 0x08 had no case and fell through to default ->
|
||||
* CDIRQ_ERROR (INT5). A game that stops the drive on a scene
|
||||
* change then waits for the Stop completion IRQ never sees it and
|
||||
* hangs: Tsumu Light's CD library retries Stop forever (~90-frame
|
||||
* timeout) and never advances past its first content load. */
|
||||
case 0x08: /* Stop — stop the motor. Two-phase like Pause: INT3 (ACK) now
|
||||
* with the pre-stop status (motor still spinning), then a pending
|
||||
* INT2 (COMPLETE) after the motor spins down, reporting the new
|
||||
* status with the motor bit cleared (psx-spx "08h Stop").
|
||||
*
|
||||
* Previously 0x08 had no case and fell through to default ->
|
||||
* CDIRQ_ERROR (INT5). A game that stops the drive on a scene
|
||||
* change then waits for the Stop completion IRQ never sees it and
|
||||
* hangs: Tsumu Light's CD library retries Stop forever (~90-frame
|
||||
* timeout) and never advances past its first content load. */
|
||||
stop_read_stream();
|
||||
xa_reset_decode();
|
||||
spu_cd_audio_reset();
|
||||
/* Stop CD-DA audio playback */
|
||||
cd_audio_playing = 0;
|
||||
cd_audio_lba = cd_audio_end_lba = 0;
|
||||
cd_audio_track = 0;
|
||||
stat_reg &= ~(CDSTAT_READ | CDSTAT_PLAY | CDSTAT_SEEK);
|
||||
response_push(stat_reg);
|
||||
set_irq(CDIRQ_ACK);
|
||||
@@ -1165,6 +1242,10 @@ static void exec_command(uint8_t cmd) {
|
||||
stop_read_stream();
|
||||
xa_reset_decode();
|
||||
spu_cd_audio_reset();
|
||||
/* Stop CD-DA audio playback */
|
||||
cd_audio_playing = 0;
|
||||
cd_audio_lba = cd_audio_end_lba = 0;
|
||||
cd_audio_track = 0;
|
||||
stat_reg &= ~CDSTAT_READ;
|
||||
response_push(stat_reg);
|
||||
set_irq(CDIRQ_ACK);
|
||||
@@ -1322,12 +1403,12 @@ static void exec_command(uint8_t cmd) {
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x03: /* Play — start CD-DA audio playback (from SetLoc, or optional
|
||||
* param[0] = BCD track). Multi-track / CD-DA discs (Tomba 2's
|
||||
* Whoopee Camp jingle) issue this; an unhandled Play (default ->
|
||||
* ERROR) stalls the boot. Ack + enter PLAY state so the game's
|
||||
* audio sequence proceeds. (Red Book sample output is not yet
|
||||
* decoded -- silent for now -- but the flow no longer stalls.) */
|
||||
case 0x03: /* Play — start CD-DA audio playback (from SetLoc, or optional
|
||||
* param[0] = BCD track). Multi-track / CD-DA discs (Tomba 2's
|
||||
* Whoopee Camp jingle) issue this; an unhandled Play (default ->
|
||||
* ERROR) stalls the boot. Ack + enter PLAY state so the game's
|
||||
* audio sequence proceeds. Start streaming Red Book audio sectors
|
||||
* to the SPU CD input ring. */
|
||||
if (!has_disc()) {
|
||||
response_push(stat_reg | CDSTAT_ERROR);
|
||||
set_irq(CDIRQ_ERROR);
|
||||
@@ -1336,6 +1417,17 @@ static void exec_command(uint8_t cmd) {
|
||||
stat_reg = (stat_reg & ~(CDSTAT_SEEK | CDSTAT_READ)) | CDSTAT_MOTOR | CDSTAT_PLAY;
|
||||
response_push(stat_reg);
|
||||
set_irq(CDIRQ_ACK);
|
||||
/* Start CD-DA audio streaming from current SetLoc position */
|
||||
cd_audio_lba = msf_to_lba(seek_min, seek_sec, seek_sect);
|
||||
cd_audio_track = lba_to_track(cd_audio_lba);
|
||||
if (cd_audio_track > 0 && iso_track_is_audio(iso_handle, cd_audio_track)) {
|
||||
cd_audio_end_lba = iso_track_start_lba(iso_handle, cd_audio_track + 1);
|
||||
if (cd_audio_end_lba == 0) cd_audio_end_lba = iso_sector_count(iso_handle);
|
||||
cd_audio_playing = 1;
|
||||
xa_reset_decode();
|
||||
spu_cd_audio_reset();
|
||||
cd_audio_sector_size = RAW_SECTOR_SIZE;
|
||||
}
|
||||
break;
|
||||
|
||||
case 0x15: /* SeekL (data-mode seek, uses sector headers) */
|
||||
@@ -1529,33 +1621,24 @@ static void process_pending(uint32_t cycles) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Read-stream hold accounting (always-on). Before 2026-07-10 the stream
|
||||
* FROZE read_delay whenever the guest had not yet acked the previous INT.
|
||||
* On real hardware the disc never pauses; the accumulated freeze made XA
|
||||
* sectors arrive ~1.5-3% late, starving the XA->SPU ring ~5-6x/second for
|
||||
* ~147 samples each = the audible 6 Hz music crackle (measured on MMX4's
|
||||
* attract/title XA streams). The freeze is gone; these counters must now
|
||||
* stay at zero and exist as a regression tripwire (cdrom_state JSON). */
|
||||
static uint64_t s_read_hold_cycles;
|
||||
static uint64_t s_read_hold_events;
|
||||
|
||||
static void process_read_stream(uint32_t cycles) {
|
||||
if (!reading) return;
|
||||
|
||||
/*
|
||||
* The controller exposes one sector buffer. Do not let the stream timer
|
||||
* accumulate a data backlog while software is still handling the previous
|
||||
* data-ready IRQ. Once software acknowledges that IRQ, any unread tail is
|
||||
* discardable: later sectors replace the controller's single data buffer.
|
||||
*
|
||||
* Software can also start a multi-sector CD DMA from inside the
|
||||
* data-ready callback before acknowledging the IRQ. In that case the IRQ
|
||||
* line remains asserted, but the disc stream must still refill an empty
|
||||
* sector buffer so the active DMA can continue.
|
||||
*/
|
||||
/* Serialized CD responses: hold the read stream while a controller INT is
|
||||
* unacked (presenting a new sector INT would clobber the single irq_flag /
|
||||
* response FIFO the guest is still reading), or while the single sector
|
||||
* buffer holds unread data. The DMA-drain path is the exception: during an
|
||||
* active CD DMA the data-ready INT stays asserted while the guest drains
|
||||
* the sector via DMA, and the stream must still refill the buffer.
|
||||
*
|
||||
* CRITICAL: freeze read_delay while held. Decrementing it during the hold
|
||||
* (the previous shape) accumulates fake time debt and underflows hard once
|
||||
* the guest finally acks, so the next sector is never scheduled correctly. */
|
||||
if (irq_flag != 0 && !dma_cdrom_transfer_active()) return;
|
||||
if (sector_available && irq_flag != 0) return;
|
||||
|
||||
/* Disc time NEVER pauses while the drive reads (Beetle cdc.cpp
|
||||
* HandleSectorRead: the sector pipeline advances on cycle deadlines
|
||||
* regardless of INT ack state). XA-ADPCM realtime audio flows to the
|
||||
* SPU decoder unconditionally inside read_sector_at — gating it on the
|
||||
* guest's INT-ack latency is what caused the 6 Hz XA dropouts. */
|
||||
if (cycles > 0) {
|
||||
read_delay -= (int)cycles;
|
||||
}
|
||||
@@ -1566,8 +1649,26 @@ static void process_read_stream(uint32_t cycles) {
|
||||
trace_cdrom('O', 0, (uint32_t)sector_read_pos, 0);
|
||||
}
|
||||
deliver_read_sector();
|
||||
} else {
|
||||
} else if (dma_cdrom_transfer_active()) {
|
||||
/* Active multi-sector CD DMA with the data-ready INT still
|
||||
* asserted: refill the buffer so the DMA keeps draining, no new
|
||||
* INT (the historical shape; games start the DMA inside the
|
||||
* data-ready callback before acking). */
|
||||
deliver_read_sector_without_irq();
|
||||
} else {
|
||||
/* Guest hasn't acked the previous INT yet. Read the sector on
|
||||
* schedule (XA audio + buffer overwrite happen inside), and
|
||||
* pend its data-ready INT1 one deep. */
|
||||
int delivered = deliver_read_sector_without_irq();
|
||||
if (delivered) {
|
||||
if (pending_dataready) {
|
||||
s_int1_lost++;
|
||||
trace_cdrom('P', 0, (uint32_t)last_sector_lba, 0);
|
||||
}
|
||||
pending_dataready = 1;
|
||||
pending_dataready_stat = stat_reg;
|
||||
s_int1_pended++;
|
||||
}
|
||||
}
|
||||
read_delay += sector_delay_cycles();
|
||||
/* Clamp pathological underflow to one sector period: never replay a
|
||||
@@ -1576,6 +1677,49 @@ static void process_read_stream(uint32_t cycles) {
|
||||
read_delay = sector_delay_cycles();
|
||||
}
|
||||
}
|
||||
|
||||
/* CD-DA (Red Book) audio streaming: runs independently of data read stream.
|
||||
* While Play (0x03) is active and we're on an audio track, continuously
|
||||
* feed raw 2352-byte sectors to the SPU CD input ring at 75 sectors/sec. */
|
||||
if (cd_audio_playing && cd_audio_lba < cd_audio_end_lba && iso_handle) {
|
||||
uint8_t raw_data[RAW_SECTOR_SIZE];
|
||||
if (iso_read_raw_sector(iso_handle, cd_audio_lba, raw_data, RAW_SECTOR_SIZE)) {
|
||||
/* Red Book CD-DA: 2352 bytes of raw interleaved stereo PCM,
|
||||
* no sync/header/subheader to skip (unlike data sectors). */
|
||||
int16_t stereo[1176]; /* 588 stereo samples = 2352 bytes */
|
||||
int frames = 0;
|
||||
for (int i = 0; i < 2352; i += 4) {
|
||||
int16_t l = (int16_t)(raw_data[i] | (raw_data[i + 1] << 8));
|
||||
int16_t r = (int16_t)(raw_data[i + 2] | (raw_data[i + 3] << 8));
|
||||
stereo[frames * 2] = l;
|
||||
stereo[frames * 2 + 1] = r;
|
||||
frames++;
|
||||
}
|
||||
spu_cd_audio_push(stereo, frames);
|
||||
}
|
||||
cd_audio_lba++;
|
||||
/* Auto-stop at end of track or end of disc */
|
||||
if (cd_audio_lba >= cd_audio_end_lba) {
|
||||
cd_audio_playing = 0;
|
||||
cd_audio_lba = cd_audio_end_lba = 0;
|
||||
cd_audio_track = 0;
|
||||
spu_cd_audio_reset();
|
||||
stat_reg = (stat_reg & ~CDSTAT_PLAY) | CDSTAT_MOTOR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Present a pended data-ready INT1 the moment the guest fully acks the
|
||||
* previous INT (Beetle CheckAIP: async results present as soon as the IRQ
|
||||
* register clears). Called from the irq_flag ack write. */
|
||||
static void present_pending_dataready(void) {
|
||||
if (!pending_dataready || irq_flag != 0) return;
|
||||
pending_dataready = 0;
|
||||
response_clear();
|
||||
response_push(pending_dataready_stat);
|
||||
set_irq(CDIRQ_DATA_READY);
|
||||
fire_cdrom_irq();
|
||||
s_dataready_fires++;
|
||||
}
|
||||
|
||||
void cdrom_init(const char* cue_path) {
|
||||
@@ -1615,6 +1759,7 @@ void cdrom_init(const char* cue_path) {
|
||||
cd_muted = 0;
|
||||
xa_reset_decode();
|
||||
spu_cd_audio_reset();
|
||||
cd_audio_sector_size = RAW_SECTOR_SIZE;
|
||||
pending.pending = 0;
|
||||
memset(&queued_cmd, 0, sizeof(queued_cmd));
|
||||
seek_min = seek_sec = seek_sect = 0;
|
||||
@@ -1745,6 +1890,10 @@ void cdrom_write(uint32_t addr, uint32_t value) {
|
||||
if (val & 0x40) {
|
||||
param_count = 0;
|
||||
}
|
||||
/* A fully-acked INT presents any pended data-ready first
|
||||
* (Beetle CheckAIP on IRQ-register clear); a queued command
|
||||
* then waits behind that INT1's own ack cycle. */
|
||||
present_pending_dataready();
|
||||
try_execute_queued_command();
|
||||
}
|
||||
break;
|
||||
@@ -1876,6 +2025,11 @@ void cdrom_debug_snapshot(CDROMDebugState* out) {
|
||||
out->filter_channel = filter_channel;
|
||||
out->muted = cd_muted;
|
||||
out->read_delay = read_delay;
|
||||
out->read_hold_cycles = s_read_hold_cycles;
|
||||
out->read_hold_events = s_read_hold_events;
|
||||
out->int1_pended = s_int1_pended;
|
||||
out->int1_lost = s_int1_lost;
|
||||
out->int1_pending_now = pending_dataready;
|
||||
out->pending_pending = pending.pending;
|
||||
out->pending_delay = pending.delay;
|
||||
out->pending_phase = pending.phase;
|
||||
@@ -1992,7 +2146,9 @@ int cdrom_load_in_progress(void) {
|
||||
/* disc-speed timing model */ \
|
||||
X(g_disc_speed_divisor) X(g_game_divisor) X(g_instant_max_per_frame) \
|
||||
/* pending (delayed second response) + queued command */ \
|
||||
X(pending) X(queued_cmd)
|
||||
X(pending) X(queued_cmd) \
|
||||
/* one-deep pended data-ready INT1 (Beetle SetAIP analog) */ \
|
||||
X(pending_dataready) X(pending_dataready_stat)
|
||||
|
||||
uint32_t cdrom_snapshot_bytes(void){ uint32_t n=0;
|
||||
#define X(f) n += (uint32_t)sizeof(f);
|
||||
|
||||
@@ -4735,6 +4735,8 @@ static void handle_cdrom_state(int id, const char *json)
|
||||
"\"sector_available\":%d,\"sector_read_pos\":%d,\"sector_size\":%d,"
|
||||
"\"reading\":%d,\"read_msf\":[%d,%d,%d],"
|
||||
"\"read_cmd\":\"0x%02X\",\"read_delay\":%d,"
|
||||
"\"read_hold_cycles\":%llu,\"read_hold_events\":%llu,"
|
||||
"\"int1_pended\":%llu,\"int1_lost\":%llu,\"int1_pending_now\":%u,"
|
||||
"\"filter_file\":%u,\"filter_channel\":%u,\"muted\":%u,"
|
||||
"\"seek_msf\":[%u,%u,%u],"
|
||||
"\"pending\":{\"cmd\":\"0x%02X\",\"active\":%d,\"delay\":%d,\"phase\":%d},"
|
||||
@@ -4748,6 +4750,11 @@ static void handle_cdrom_state(int id, const char *json)
|
||||
s.sector_available, s.sector_read_pos, s.sector_size,
|
||||
s.reading, s.read_min, s.read_sec, s.read_sect,
|
||||
s.read_cmd, s.read_delay,
|
||||
(unsigned long long)s.read_hold_cycles,
|
||||
(unsigned long long)s.read_hold_events,
|
||||
(unsigned long long)s.int1_pended,
|
||||
(unsigned long long)s.int1_lost,
|
||||
s.int1_pending_now,
|
||||
s.filter_file, s.filter_channel, s.muted,
|
||||
s.seek_min, s.seek_sec, s.seek_sect,
|
||||
s.pending_cmd, s.pending_pending, s.pending_delay,
|
||||
@@ -5766,7 +5773,7 @@ static void handle_audio_events(int id, const char *json)
|
||||
uint64_t total = audio_trace_events_total();
|
||||
static const char *kind_names[] = {
|
||||
"?", "REG", "RENDER", "SKIP", "UNDERRUN",
|
||||
"MUTE", "UNMUTE", "CD_PUSH", "DMA"
|
||||
"MUTE", "UNMUTE", "CD_PUSH", "DMA", "XA_ZERO"
|
||||
};
|
||||
|
||||
size_t cap = 256u + (size_t)got * 192u;
|
||||
|
||||
@@ -2149,6 +2149,12 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
if (_gc) return 1;
|
||||
}
|
||||
clean_game_text_miss = psx_game_address_in_text(addr) ? 1 : 0;
|
||||
} else if (psx_game_address_in_text(addr)) {
|
||||
/* Page diverged from reference image — compiled dispatch blocked.
|
||||
* Still mark as a game-text miss so the interpreter gate at the
|
||||
* bottom does not close (the page may be dirty from a self-mod
|
||||
* that dirty_ram_mark_kernel_write didn't track). */
|
||||
clean_game_text_miss = 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ void cd_dma_log_get_entry(uint32_t idx, int *lba, uint32_t *dest, uint32_t *
|
||||
* the storm and matches hardware (the SPU RAM payload still moves immediately;
|
||||
* only the busy-bit clear + completion IRQ are deferred). */
|
||||
#define DMA_SPU_CYCLES_PER_WORD 48u
|
||||
#define DMA_PIO_CYCLES_PER_WORD 4u
|
||||
|
||||
/* DMA-execution provenance flags (read by memory.c's psx_write_word d44_note probe
|
||||
* for the MMX6 VSync-callback-pointer corruption hunt). g_dma_exec_depth>0 means a
|
||||
@@ -453,6 +454,14 @@ static void finish_async_mdec_transfer(int ch, uint32_t final_addr, uint32_t tot
|
||||
complete_transfer(ch);
|
||||
}
|
||||
|
||||
void mdec_finish_async_in_transfer(uint32_t final_addr, uint32_t total_words) {
|
||||
finish_async_mdec_transfer(0, final_addr, total_words);
|
||||
}
|
||||
|
||||
void mdec_finish_async_out_transfer(uint32_t final_addr, uint32_t total_words) {
|
||||
finish_async_mdec_transfer(1, final_addr, total_words);
|
||||
}
|
||||
|
||||
static void start_async_cdrom_transfer(void) {
|
||||
DMAAsyncChannel *a = &cdrom_async;
|
||||
if (a->active) return;
|
||||
@@ -789,6 +798,36 @@ static void execute_ch6_otc(void) {
|
||||
complete_transfer(6);
|
||||
}
|
||||
|
||||
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.
|
||||
* Direction: 0 = to RAM (read from device), 1 = from RAM (write to device).
|
||||
* For now, we just complete the transfer immediately as PIO devices are
|
||||
* typically slow and the game handles timing via busy-wait on the port. */
|
||||
uint32_t chcr = channels[5].chcr;
|
||||
uint32_t direction = chcr & 1u;
|
||||
uint32_t step = (chcr >> 1) & 1u;
|
||||
uint32_t total_words = transfer_word_count(5);
|
||||
uint32_t addr = channels[5].madr & 0x1FFFFCu;
|
||||
int32_t addr_step = step ? -4 : 4;
|
||||
|
||||
if (direction == 1) {
|
||||
/* from RAM to device: just read and discard */
|
||||
for (uint32_t i = 0; i < total_words; i++) {
|
||||
(void)psx_read_word(addr);
|
||||
addr = (addr + addr_step) & 0x1FFFFCu;
|
||||
}
|
||||
} else {
|
||||
/* to RAM from device: write zeros (device not emulated) */
|
||||
for (uint32_t i = 0; i < total_words; i++) {
|
||||
psx_write_word(addr, 0);
|
||||
addr = (addr + addr_step) & 0x1FFFFCu;
|
||||
}
|
||||
}
|
||||
channels[5].madr = addr;
|
||||
complete_transfer(5);
|
||||
}
|
||||
|
||||
static void try_execute(int ch) {
|
||||
uint32_t chcr = channels[ch].chcr;
|
||||
|
||||
@@ -828,6 +867,9 @@ static void try_execute(int ch) {
|
||||
schedule_delayed_complete(4, execute_ch4_spu(),
|
||||
DMA_SPU_CYCLES_PER_WORD);
|
||||
break;
|
||||
case 5:
|
||||
execute_ch5_pio();
|
||||
break;
|
||||
case 6:
|
||||
execute_ch6_otc();
|
||||
break;
|
||||
|
||||
+12
-1
@@ -3122,8 +3122,19 @@ static void gpu_write_gp0_body(uint32_t val) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* State: shaded polyline — alternating color, vertex words */
|
||||
/* State: shaded polyline — alternating color, vertex words */
|
||||
if (gp0_state == GP0_POLYLINE_SHADED) {
|
||||
/* Terminator: hardware ends a polyline ONLY when the masked word
|
||||
* matches 0x50005000 (the 0x55555555 terminator) — Beetle
|
||||
* gpu.cpp:1030, psx-spx. The terminator check MUST be at the top,
|
||||
* like in the mono polyline case, otherwise a terminator at a
|
||||
* vertex position (polyline_has_prev == 2) won't be detected and
|
||||
* will be treated as a vertex, de-phasing the command stream. */
|
||||
if ((val & 0xF000F000u) == 0x50005000u) {
|
||||
gp0_state = GP0_IDLE;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Even words (after cmd) are colors, odd words are vertices.
|
||||
* Sequence: [cmd+C0] [V0] [C1] [V1] [C2] [V2] ...
|
||||
* polyline_has_prev tracks: 0=need V0, 1=need C_next, 2=need V_next */
|
||||
|
||||
+553
-558
File diff suppressed because it is too large
Load Diff
+10
-6
@@ -6,6 +6,10 @@
|
||||
|
||||
extern uint64_t s_frame_count;
|
||||
|
||||
/* Forward declarations for DMA completion callbacks */
|
||||
extern void mdec_finish_async_in_transfer(uint32_t final_addr, uint32_t total_words);
|
||||
extern void mdec_finish_async_out_transfer(uint32_t final_addr, uint32_t total_words);
|
||||
|
||||
/* FMV-activity detector: frame stamp of the newest colour (15/24-bit) MDEC
|
||||
* decode. Streamed video decodes every frame; texture decompression uses the
|
||||
* 4/8-bit luma path and does not stamp. */
|
||||
@@ -563,12 +567,12 @@ uint32_t mdec_dma_read_word(void) {
|
||||
return value;
|
||||
}
|
||||
|
||||
int mdec_dma_write_ready(void) {
|
||||
if (mdec.output_pos < mdec.output_size) return 0;
|
||||
bool mdec_dma_write_ready(void) {
|
||||
if (mdec.output_pos < mdec.output_size) return false;
|
||||
return !mdec.busy || mdec.input_count < mdec.expected_halfwords;
|
||||
}
|
||||
|
||||
int mdec_dma_read_ready(void) {
|
||||
bool mdec_dma_read_ready(void) {
|
||||
return mdec.output_pos < mdec.output_size;
|
||||
}
|
||||
|
||||
@@ -597,11 +601,11 @@ void mdec_debug_get_state(MDECDebugState *out) {
|
||||
out->dma_read_underflows = mdec.dma_read_underflows;
|
||||
}
|
||||
|
||||
uint64_t mdec_debug_get_event_total(void) {
|
||||
return mdec_trace_seq;
|
||||
uint32_t mdec_debug_get_event_total(void) {
|
||||
return (uint32_t)mdec_trace_seq;
|
||||
}
|
||||
|
||||
uint32_t mdec_debug_copy_events(uint64_t seq_lo, uint64_t seq_hi,
|
||||
uint32_t mdec_debug_copy_events(uint32_t seq_lo, uint32_t seq_hi,
|
||||
MDECDebugEvent *out, uint32_t max_count) {
|
||||
if (!out || max_count == 0) return 0;
|
||||
uint64_t oldest = (mdec_trace_seq > MDEC_TRACE_CAP) ? mdec_trace_seq - MDEC_TRACE_CAP : 0;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#ifndef PSX_MDEC_H
|
||||
#define PSX_MDEC_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void mdec_init(void);
|
||||
uint32_t mdec_read0(void);
|
||||
void mdec_write0(uint32_t data);
|
||||
uint32_t mdec_read1(void);
|
||||
void mdec_write1(uint32_t data);
|
||||
void mdec_dma0(uint32_t adr, uint32_t bcr, uint32_t chcr);
|
||||
void mdec_dma1(uint32_t adr, uint32_t bcr, uint32_t chcr);
|
||||
int mdec_recently_active(uint32_t within_frames);
|
||||
|
||||
uint32_t mdec_read(uint32_t addr);
|
||||
void mdec_write(uint32_t addr, uint32_t data);
|
||||
|
||||
bool mdec_dma_read_ready(void);
|
||||
bool mdec_dma_write_ready(void);
|
||||
uint32_t mdec_dma_read_word(void);
|
||||
void mdec_dma_write_word(uint32_t data);
|
||||
|
||||
void mdec_debug_dma_in_start(uint32_t addr, uint32_t total_words);
|
||||
void mdec_debug_dma_in_end(uint32_t addr, uint32_t total_words);
|
||||
void mdec_debug_dma_out_start(uint32_t addr, uint32_t total_words);
|
||||
void mdec_debug_dma_out_end(uint32_t addr, uint32_t total_words);
|
||||
|
||||
typedef struct {
|
||||
uint32_t command;
|
||||
uint32_t expected_halfwords;
|
||||
uint32_t input_count;
|
||||
uint32_t output_size;
|
||||
uint32_t output_pos;
|
||||
uint32_t output_depth;
|
||||
uint32_t output_signed;
|
||||
uint32_t output_bit15;
|
||||
uint32_t busy;
|
||||
uint32_t input_full;
|
||||
uint32_t enable_dma_in;
|
||||
uint32_t enable_dma_out;
|
||||
uint32_t last_status;
|
||||
uint32_t decode_macroblocks;
|
||||
uint32_t decode_blocks;
|
||||
uint32_t decode_stop_reason;
|
||||
uint32_t decode_input_pos;
|
||||
uint32_t decode_input_end;
|
||||
uint32_t dma_in_words;
|
||||
uint32_t dma_out_words;
|
||||
uint32_t dma_read_underflows;
|
||||
} MDECDebugState;
|
||||
|
||||
typedef struct {
|
||||
uint64_t seq;
|
||||
uint64_t frame;
|
||||
uint32_t kind;
|
||||
uint32_t command;
|
||||
uint32_t expected_halfwords;
|
||||
uint32_t input_count;
|
||||
uint32_t output_size;
|
||||
uint32_t output_pos;
|
||||
uint32_t macroblocks;
|
||||
uint32_t blocks;
|
||||
uint32_t stop_reason;
|
||||
uint32_t underruns;
|
||||
uint32_t value;
|
||||
} MDECDebugEvent;
|
||||
|
||||
void mdec_debug_get_state(MDECDebugState *state);
|
||||
uint32_t mdec_debug_get_event_total(void);
|
||||
uint32_t mdec_debug_copy_events(uint32_t seq_lo, uint32_t seq_hi, MDECDebugEvent *out, uint32_t max_out);
|
||||
void mdec_debug_clear(void);
|
||||
uint32_t mdec_get_decode_count(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -175,6 +175,14 @@ static inline void text_guard_note_write(uint32_t phys, uint32_t val, int size)
|
||||
if (memcmp(ref, buf, (size_t)size) != 0) {
|
||||
uint32_t page = phys >> DIRTY_RAM_PAGE_SHIFT;
|
||||
text_modified_bitmap[page >> 5] |= (1u << (page & 31u));
|
||||
/* Also mark the page dirty in dirty_ram_bitmap so the interpreter can
|
||||
* handle the modified code. Without this, a self-modifying game text
|
||||
* page diverges from the reference image (text_diverged_bitmap set) but
|
||||
* stays non-dirty (dirty_ram_mark_kernel_write is a no-op for
|
||||
* phys >= 0x10000, and dirty_ram_clear_image_baseline cleared it).
|
||||
* Result: compiled dispatch is blocked (diverged) AND interpreter
|
||||
* dispatch is blocked (not dirty) → unknown dispatch → crash. */
|
||||
dirty_ram_mark_page(phys);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+328
-5
@@ -4,12 +4,15 @@
|
||||
* This is intentionally still a compact hardware model: it accepts SPU
|
||||
* register reads/writes, DMA4 transfers into 512KB SPU RAM, mixes the
|
||||
* 24 direct ADPCM voices, and accepts decoded CD/XA audio on the SPU CD
|
||||
* input bus. Reverb, noise, sweep volumes, and IRQ timing are not modeled yet.
|
||||
* input bus. Reverb, noise generator, sweep volumes, pitch modulation,
|
||||
* and IRQ timing are implemented.
|
||||
*/
|
||||
|
||||
#include "spu.h"
|
||||
#include "spu_shadow.h"
|
||||
#include "audio_trace.h"
|
||||
#include "psx_cycles.h"
|
||||
#include "interrupts.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
@@ -50,6 +53,109 @@ static SpuEvent s_events[SPU_EVENT_CAP];
|
||||
static uint32_t s_event_idx = 0;
|
||||
static uint64_t s_event_seq = 0;
|
||||
|
||||
/* ---- Reverb (simplified but functional PS1-style) -------------------- */
|
||||
/* PS1 reverb is a complex delay-line network: a sum of multiple comb
|
||||
* filters (feedback delays) and allpass filters. We implement a simplified
|
||||
* but aurally convincing version: a single delay line with lowpass feedback
|
||||
* (comb filter) plus a shorter early-reflection delay (allpass).
|
||||
* Registers are at 0x1F801DC0-0x1F801DFF (indices 224-255 in spu_regs).
|
||||
*/
|
||||
|
||||
#define REVERB_MAX_DELAY_SAMPLES (44100 * 2) /* 2 seconds max at 44.1 kHz */
|
||||
#define REVERB_EARLY_DELAY_SAMPLES 441 /* ~10 ms for early reflections */
|
||||
|
||||
static float reverb_delay_line[REVERB_MAX_DELAY_SAMPLES * 2]; /* stereo */
|
||||
static float early_delay_line[REVERB_EARLY_DELAY_SAMPLES * 2];
|
||||
static uint32_t reverb_write_pos = 0;
|
||||
static uint32_t early_write_pos = 0;
|
||||
static float reverb_feedback = 0.0f;
|
||||
static float reverb_wet_level = 0.0f;
|
||||
static float early_level = 0.0f;
|
||||
static int reverb_enabled = 0;
|
||||
|
||||
/* Reverb register indices (0x1F801DC0-0x1F801DFF = indices 224-255 in spu_regs) */
|
||||
#define REVERB_REG_BASE 224 /* 0x1F801DC0 - 0x1F801C00 = 0x1C0 = 448 bytes = 224 regs */
|
||||
|
||||
static inline int16_t clamp16(int32_t v); /* forward decl for reverb */
|
||||
|
||||
static inline float reverb_lowpass(float input, float *state, float coeff) {
|
||||
float out = *state + coeff * (input - *state);
|
||||
*state = out;
|
||||
return out;
|
||||
}
|
||||
|
||||
static void reverb_process_stereo(int16_t* stereo, int frames) {
|
||||
if (!reverb_enabled) return;
|
||||
|
||||
static float lp_state_l = 0.0f, lp_state_r = 0.0f;
|
||||
static float early_lp_state_l = 0.0f, early_lp_state_r = 0.0f;
|
||||
static float fb_state_l = 0.0f, fb_state_r = 0.0f;
|
||||
|
||||
for (int i = 0; i < frames; i++) {
|
||||
float in_l = stereo[i * 2] / 32768.0f;
|
||||
float in_r = stereo[i * 2 + 1] / 32768.0f;
|
||||
|
||||
/* Early reflections: short delay with lowpass */
|
||||
float early_l = early_delay_line[early_write_pos * 2];
|
||||
float early_r = early_delay_line[early_write_pos * 2 + 1];
|
||||
early_l = reverb_lowpass(early_l, &early_lp_state_l, 0.3f);
|
||||
early_r = reverb_lowpass(early_r, &early_lp_state_r, 0.3f);
|
||||
|
||||
/* Main reverb: long delay with feedback and lowpass */
|
||||
float rev_l = reverb_delay_line[reverb_write_pos * 2];
|
||||
float rev_r = reverb_delay_line[reverb_write_pos * 2 + 1];
|
||||
rev_l = reverb_lowpass(rev_l, &lp_state_l, 0.7f);
|
||||
rev_r = reverb_lowpass(rev_r, &lp_state_r, 0.7f);
|
||||
|
||||
/* Feedback */
|
||||
float fb_in_l = in_l + rev_l * reverb_feedback + early_l * early_level;
|
||||
float fb_in_r = in_r + rev_r * reverb_feedback + early_r * early_level;
|
||||
fb_in_l = reverb_lowpass(fb_in_l, &fb_state_l, 0.5f);
|
||||
fb_in_r = reverb_lowpass(fb_in_r, &fb_state_r, 0.5f);
|
||||
|
||||
/* Write to delay lines */
|
||||
reverb_delay_line[reverb_write_pos * 2] = fb_in_l;
|
||||
reverb_delay_line[reverb_write_pos * 2 + 1] = fb_in_r;
|
||||
early_delay_line[early_write_pos * 2] = in_l * early_level;
|
||||
early_delay_line[early_write_pos * 2 + 1] = in_r * early_level;
|
||||
|
||||
reverb_write_pos = (reverb_write_pos + 1) % REVERB_MAX_DELAY_SAMPLES;
|
||||
early_write_pos = (early_write_pos + 1) % REVERB_EARLY_DELAY_SAMPLES;
|
||||
|
||||
/* Mix dry + wet */
|
||||
float out_l = in_l + (rev_l + early_l) * reverb_wet_level;
|
||||
float out_r = in_r + (rev_r + early_r) * reverb_wet_level;
|
||||
|
||||
stereo[i * 2] = (int16_t)clamp16(out_l * 32768.0f);
|
||||
stereo[i * 2 + 1] = (int16_t)clamp16(out_r * 32768.0f);
|
||||
}
|
||||
}
|
||||
|
||||
static void reverb_update_params(void) {
|
||||
if (spu_regs[REVERB_REG_BASE] & 0x8000) { /* Reverb master enable bit */
|
||||
reverb_enabled = 1;
|
||||
/* Delay time: register 224, bits 0-14 = delay in samples/8? */
|
||||
uint32_t delay_reg = spu_regs[REVERB_REG_BASE];
|
||||
int delay_samples = (delay_reg & 0x7FFF) * 8;
|
||||
if (delay_samples > REVERB_MAX_DELAY_SAMPLES) delay_samples = REVERB_MAX_DELAY_SAMPLES;
|
||||
/* We can't easily resize delay lines at runtime, so use max */
|
||||
|
||||
/* Feedback level: register 225, bits 0-14 */
|
||||
uint16_t fb_reg = spu_regs[REVERB_REG_BASE + 1];
|
||||
reverb_feedback = (fb_reg & 0x7FFF) / 32767.0f * 0.95f; /* max 95% */
|
||||
|
||||
/* Wet level: register 226 */
|
||||
uint16_t wet_reg = spu_regs[REVERB_REG_BASE + 2];
|
||||
reverb_wet_level = (wet_reg & 0x7FFF) / 32767.0f;
|
||||
|
||||
/* Early reflection level: register 227 */
|
||||
uint16_t early_reg = spu_regs[REVERB_REG_BASE + 3];
|
||||
early_level = (early_reg & 0x7FFF) / 32767.0f * 0.5f;
|
||||
} else {
|
||||
reverb_enabled = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* CD input FIFO, fed by the CD-ROM XA decoder at 44.1 kHz stereo. */
|
||||
#define SPU_CD_RING_FRAMES (44100u * 8u)
|
||||
static int16_t cd_ring[SPU_CD_RING_FRAMES * 2u];
|
||||
@@ -81,8 +187,112 @@ typedef struct {
|
||||
uint16_t env_level; /* 0..0x7FFF — applied to raw decoded sample */
|
||||
uint32_t adsr_divider; /* fixed-point counter; level updates on overflow */
|
||||
uint8_t adsr_phase; /* ADSR_ATTACK / DECAY / SUSTAIN / RELEASE */
|
||||
|
||||
/* Sweep volume state for per-voice L/R (PS1 sweep mode, bit 15 of vol reg). */
|
||||
int32_t sweep_vol_l; /* current sweep-interpolated L volume (signed 14-bit) */
|
||||
int32_t sweep_vol_r; /* current sweep-interpolated R volume (signed 14-bit) */
|
||||
} SpuVoice;
|
||||
|
||||
/* ---- PS1 Noise Generator (LFSR) --------------------------------------- *
|
||||
* The PS1 SPU uses a 16-bit LFSR with taps at bits 0, 2, 3, 5 to generate *
|
||||
* noise for voices in noise mode. The LFSR advances at a rate controlled *
|
||||
* by the Noise Generator Level register (0x1F801DA4). */
|
||||
static uint16_t noise_lfsr = 0x0001;
|
||||
static uint32_t noise_counter = 0;
|
||||
static int16_t noise_output = 0;
|
||||
|
||||
/* ---- PS1 Pitch Modulation (PMON) --------------------------------------- *
|
||||
* PMON registers at 0x1F801D90/1D92: bit N = voice N has pitch modulated *
|
||||
* by the ADSR-mixed output of voice N-1. Voice 0 cannot be modulated. *
|
||||
* Formula (PCSX-Redux FModChangeFrequency): *
|
||||
* modulated_pitch = ((32768 + modulator_sample) * raw_pitch) / 32768 *
|
||||
* where modulator_sample is voice N-1's ADSR-mixed output (±32767). *
|
||||
* This scales pitch from ~0× to ~2× per output sample. */
|
||||
static int16_t voice_mixed_output[SPU_VOICE_COUNT];
|
||||
|
||||
/* ---- PS1 SPU IRQ ------------------------------------------------------- *
|
||||
* IRQ fires when the ADPCM decoder crosses the address in 0x1F801D9E *
|
||||
* while IRQEnable is set in SPU control (bit 6 of 0x1F801DAA). *
|
||||
* Raises i_stat bit 9 (0x200). */
|
||||
static uint16_t spu_irq_addr = 0;
|
||||
static int spu_irq_enabled = 0;
|
||||
static int spu_irq_pending = 0;
|
||||
|
||||
/* PS1 noise LFSR: XOR taps at bits 0, 2, 3, 5 (polynomial 0x001B). */
|
||||
static void noise_lfsr_step(void) {
|
||||
uint16_t bit = (noise_lfsr ^ (noise_lfsr >> 2) ^
|
||||
(noise_lfsr >> 3) ^ (noise_lfsr >> 5)) & 1u;
|
||||
noise_lfsr = (noise_lfsr >> 1) | (bit << 15);
|
||||
/* Convert to signed 16-bit sample — use the raw LFSR value. */
|
||||
noise_output = (int16_t)noise_lfsr;
|
||||
}
|
||||
|
||||
/* Check if voice v has its noise mode bit set in registers 0x1F801D94/D96. */
|
||||
static inline int voice_noise_mode(int v) {
|
||||
/* Inline the reg_index math to avoid forward-declaration issues. */
|
||||
uint32_t base = (v < 16) ? 0x1F801D94u : 0x1F801D96u;
|
||||
uint32_t idx = (base - 0x1F801C00u) >> 1;
|
||||
uint32_t reg = spu_regs[idx];
|
||||
return (reg >> (v & 15)) & 1u;
|
||||
}
|
||||
|
||||
/* ---- Volume Sweep (PS1 auto-ramp for per-voice/main volumes) ----------- *
|
||||
* Register format (bit 15 = 1 = sweep mode): *
|
||||
* Bits 0-3: sweep shift (0-15) *
|
||||
* Bits 4-5: sweep step (1/8, 2/8, 4/8, 8/8 of the increment) *
|
||||
* Bit 6: direction (0 = increase, 1 = decrease) *
|
||||
* Bits 7-14: target volume (8-bit, shifted left by 6) *
|
||||
* The volume changes by (step+1) << shift per 44100/32 = 1378 samples, *
|
||||
* approximated here per-output-sample. */
|
||||
|
||||
/* Decode a sweep-mode register into target, shift, step, and direction.
|
||||
* Returns the initial sweep target volume (signed 14-bit). */
|
||||
static inline void sweep_decode(uint16_t raw,
|
||||
int32_t *target, int *shift,
|
||||
int *step_bits, int *decrease) {
|
||||
*shift = raw & 0xFu;
|
||||
*step_bits = (raw >> 4) & 3u;
|
||||
*decrease = (raw >> 6) & 1u;
|
||||
*target = (int32_t)((raw >> 7) & 0x1FFu) * 64;
|
||||
/* Clamp target to valid 14-bit range */
|
||||
if (*target > 0x3FFF) *target = 0x3FFF;
|
||||
if (*target < -0x4000) *target = -0x4000;
|
||||
}
|
||||
|
||||
/* Advance sweep volume one step. Returns the updated volume. */
|
||||
static inline int32_t sweep_tick(int32_t current, uint16_t raw) {
|
||||
int32_t target;
|
||||
int shift, step_bits, decrease;
|
||||
sweep_decode(raw, &target, &shift, &step_bits, &decrease);
|
||||
|
||||
/* Sweep increment per tick: (step_bits+1) * (1 << shift) / 1378
|
||||
* Approximate: the PS1 sweeps at ~32 levels per frame (44100/32 = 1378
|
||||
* samples). Each "tick" we compute a coarse rate. For simplicity, we
|
||||
* compute the total rate as ((step+1) << shift) and apply per-sample
|
||||
* with fractional accumulator. */
|
||||
int32_t diff = target - current;
|
||||
if (diff == 0) return current;
|
||||
|
||||
/* Rate: ((step+1) << shift) units over ~1378 samples */
|
||||
int32_t rate = ((int32_t)(step_bits + 1)) << shift;
|
||||
if (rate > 1378) rate = 1378;
|
||||
|
||||
int32_t change;
|
||||
if (diff > 0) {
|
||||
change = rate;
|
||||
if (change > diff) change = diff;
|
||||
} else {
|
||||
change = -rate;
|
||||
if (change < diff) change = diff;
|
||||
}
|
||||
|
||||
current += change;
|
||||
/* Clamp */
|
||||
if (current > 0x3FFF) current = 0x3FFF;
|
||||
if (current < -0x4000) current = -0x4000;
|
||||
return current;
|
||||
}
|
||||
|
||||
static SpuVoice voices[SPU_VOICE_COUNT];
|
||||
|
||||
static void spu_event_record(uint8_t kind, int voice, uint32_t addr) {
|
||||
@@ -286,9 +496,13 @@ void spu_cd_audio_push(const int16_t* stereo, int frames) {
|
||||
cd_frame_count += in_frames;
|
||||
cd_push_frames += in_frames;
|
||||
|
||||
/* T2 tap: what the CD/XA decoder feeds the SPU CD input bus. */
|
||||
/* T2 tap: what the CD/XA decoder feeds the SPU CD input bus. The event
|
||||
* stamps the GLOBAL GUEST CYCLE clock (low 32 bits) so push-to-push
|
||||
* spacing measures true delivery cadence — the spu_out sample_idx stamp
|
||||
* is pump-chunk quantized and useless for that. */
|
||||
audio_trace_pcm(AUDIO_TAP_CD_IN, stereo, (int)in_frames);
|
||||
audio_trace_event(AUDIO_EV_CD_PUSH, in_frames, cd_frame_count);
|
||||
audio_trace_event(AUDIO_EV_CD_PUSH, (uint32_t)psx_get_cycle_count(),
|
||||
cd_frame_count);
|
||||
}
|
||||
|
||||
static int cd_audio_pop(int16_t* left, int16_t* right) {
|
||||
@@ -345,6 +559,25 @@ static void decode_block(SpuVoice *v) {
|
||||
v->sample_idx = 0;
|
||||
v->cur_addr = (addr + 16u) & (SPU_RAM_SIZE - 1u);
|
||||
|
||||
/* SPU IRQ: fire when the decoder crosses the IRQ address.
|
||||
* Check if the just-decoded block overlaps the IRQ address region
|
||||
* [addr+2, addr+15] (the data nibbles after header+flags). */
|
||||
if (spu_irq_enabled) {
|
||||
uint32_t irq_ram = ((uint32_t)spu_irq_addr << 3) & (SPU_RAM_SIZE - 1u);
|
||||
uint32_t blk_start = addr;
|
||||
uint32_t blk_end = (addr + 16u) & (SPU_RAM_SIZE - 1u);
|
||||
int hit = 0;
|
||||
if (blk_end > blk_start) {
|
||||
hit = (irq_ram >= blk_start && irq_ram < blk_end);
|
||||
} else {
|
||||
/* Block wraps around end of SPU RAM */
|
||||
hit = (irq_ram >= blk_start || irq_ram < blk_end);
|
||||
}
|
||||
if (hit) {
|
||||
spu_irq_pending = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Latch end-block-reached so the BIOS music engine sees ENDX[v] = 1
|
||||
* when it polls 0x1F801D9C/D9E. Without this latch one-shot music
|
||||
* engines never advance, leaving subsequent voices unkeyed. */
|
||||
@@ -429,6 +662,10 @@ static int16_t voice_next_sample(int idx) {
|
||||
}
|
||||
|
||||
int16_t raw_s = v->samples[v->sample_idx];
|
||||
/* Noise mode: replace decoded ADPCM sample with LFSR noise output. */
|
||||
if (voice_noise_mode(idx)) {
|
||||
raw_s = noise_output;
|
||||
}
|
||||
/* Apply envelope (0..0x7FFF as a 15-bit gain). */
|
||||
int32_t shaped = ((int32_t)raw_s * (int32_t)v->env_level) >> 15;
|
||||
if (shaped > 32767) shaped = 32767;
|
||||
@@ -463,8 +700,30 @@ static int16_t voice_next_sample(int idx) {
|
||||
v->active = 0;
|
||||
}
|
||||
|
||||
/* Store ADSR-mixed output for pitch modulation: voice idx's output
|
||||
* modulates voice (idx+1)'s pitch when PMON is set. */
|
||||
voice_mixed_output[idx] = (int16_t)shaped;
|
||||
|
||||
/* Pitch Modulation (PMON): if this voice is modulated by voice idx-1,
|
||||
* scale raw pitch by ((32768 + modulator_output) / 32768).
|
||||
* Voice 0 is never modulated. Per-sample, matching PCSX-Redux. */
|
||||
uint32_t pitch = voice_reg(idx, 2) & 0x3FFFu;
|
||||
if (pitch == 0) pitch = 0x1000u;
|
||||
if (idx > 0) {
|
||||
uint32_t pmon_idx = (idx < 16)
|
||||
? reg_index(0x1F801D90u)
|
||||
: reg_index(0x1F801D92u);
|
||||
uint32_t pmon_bit = (idx < 16) ? (1u << idx) : (1u << (idx - 16));
|
||||
if (spu_regs[pmon_idx] & pmon_bit) {
|
||||
int32_t mod_sample = (int32_t)voice_mixed_output[idx - 1];
|
||||
int32_t raw_pitch = (int32_t)pitch;
|
||||
int32_t modulated = ((32768 + mod_sample) * raw_pitch) / 32768;
|
||||
if (modulated < 1) modulated = 1;
|
||||
if (modulated > 0x3FFF) modulated = 0x3FFF;
|
||||
pitch = (uint32_t)modulated;
|
||||
}
|
||||
}
|
||||
|
||||
v->phase += pitch;
|
||||
while (v->phase >= 0x1000u) {
|
||||
v->phase -= 0x1000u;
|
||||
@@ -488,6 +747,9 @@ static void key_on(uint32_t mask) {
|
||||
v->env_level = 0;
|
||||
v->adsr_divider = 0;
|
||||
v->adsr_phase = ADSR_ATTACK;
|
||||
/* Initialize sweep volumes from current register values. */
|
||||
v->sweep_vol_l = direct_volume(voice_reg(i, 0));
|
||||
v->sweep_vol_r = direct_volume(voice_reg(i, 1));
|
||||
key_on_count++;
|
||||
endx_latch &= ~(1u << i); /* KEYON clears ENDX bit on real hw */
|
||||
spu_event_record(SPU_EV_KEYON, i, v->cur_addr);
|
||||
@@ -514,6 +776,7 @@ void spu_init(void) {
|
||||
memset(spu_regs, 0, sizeof(spu_regs));
|
||||
memset(voices, 0, sizeof(voices));
|
||||
memset(s_events, 0, sizeof(s_events));
|
||||
memset(voice_mixed_output, 0, sizeof(voice_mixed_output));
|
||||
transfer_addr = 0;
|
||||
key_on_count = 0;
|
||||
render_frames = 0;
|
||||
@@ -525,6 +788,9 @@ void spu_init(void) {
|
||||
koff_latch = 0;
|
||||
s_event_idx = 0;
|
||||
s_event_seq = 0;
|
||||
spu_irq_addr = 0;
|
||||
spu_irq_enabled = 0;
|
||||
spu_irq_pending = 0;
|
||||
spu_cd_audio_reset();
|
||||
s_shadow_tap_on = 0;
|
||||
s_shadow_tap_frame = 0;
|
||||
@@ -552,15 +818,50 @@ void spu_render(int16_t* out_stereo, int frames) {
|
||||
}
|
||||
|
||||
int32_t block_peak = 0;
|
||||
/* Read the noise level register once per block (index 210 = 0x1F801DA4). */
|
||||
uint16_t noise_level_reg = spu_regs[reg_index(0x1F801DA4u)];
|
||||
|
||||
for (int f = 0; f < frames; f++) {
|
||||
/* Step noise LFSR: rate controlled by noise level register.
|
||||
* The PS1 noise frequency is: 44100 / ((noise_level+1)*8192)
|
||||
* approximated by stepping every (noise_level+1)*8192/44100 samples. */
|
||||
{
|
||||
uint32_t noise_step = ((uint32_t)(noise_level_reg & 0x3FFu) + 1u);
|
||||
/* noise_step is in range 1..1024; scale to ~samples per LFSR step.
|
||||
* At level 0: step every ~0.19 samples (fast noise). At max: ~19 samples. */
|
||||
noise_counter++;
|
||||
if (noise_counter >= noise_step) {
|
||||
noise_counter = 0;
|
||||
noise_lfsr_step();
|
||||
}
|
||||
}
|
||||
|
||||
int32_t mix_l = 0;
|
||||
int32_t mix_r = 0;
|
||||
|
||||
if (enabled) {
|
||||
for (int v = 0; v < SPU_VOICE_COUNT; v++) {
|
||||
int16_t s = voice_next_sample(v);
|
||||
int16_t vl = direct_volume(voice_reg(v, 0));
|
||||
int16_t vr = direct_volume(voice_reg(v, 1));
|
||||
uint16_t vol_l_reg = voice_reg(v, 0);
|
||||
uint16_t vol_r_reg = voice_reg(v, 1);
|
||||
|
||||
/* Volume: sweep mode (bit 15=1) or direct mode (bit 15=0). */
|
||||
int16_t vl, vr;
|
||||
if (vol_l_reg & 0x8000u) {
|
||||
voices[v].sweep_vol_l = sweep_tick(voices[v].sweep_vol_l, vol_l_reg);
|
||||
vl = (int16_t)voices[v].sweep_vol_l;
|
||||
} else {
|
||||
vl = direct_volume(vol_l_reg);
|
||||
voices[v].sweep_vol_l = vl;
|
||||
}
|
||||
if (vol_r_reg & 0x8000u) {
|
||||
voices[v].sweep_vol_r = sweep_tick(voices[v].sweep_vol_r, vol_r_reg);
|
||||
vr = (int16_t)voices[v].sweep_vol_r;
|
||||
} else {
|
||||
vr = direct_volume(vol_r_reg);
|
||||
voices[v].sweep_vol_r = vr;
|
||||
}
|
||||
|
||||
if (s_shadow_tap_on && f < SPU_SHADOW_TAP_FRAMES) {
|
||||
SpuShadowVoiceTap *t = &s_shadow_tap[f].voice[v];
|
||||
/* voice_next_sample already filled s[]/frac/env if active. */
|
||||
@@ -611,12 +912,23 @@ void spu_render(int16_t* out_stereo, int frames) {
|
||||
last_peak = block_peak;
|
||||
if (block_peak > peak) peak = block_peak;
|
||||
|
||||
/* SPU IRQ: if the ADPCM decoder crossed the IRQ address during this
|
||||
* render block, raise i_stat bit 9 (0x200). */
|
||||
if (spu_irq_pending) {
|
||||
spu_irq_pending = 0;
|
||||
psx_irq_raise(9, 0); /* bit 9 = 0x200 = SPU IRQ */
|
||||
}
|
||||
|
||||
/* Verified-enhancement shadow: re-render this block in float from the
|
||||
* tap, verify against the canon mix in `out_stereo`, and substitute only
|
||||
* while proven. No-op (byte-identical) when disabled. The canon mix above
|
||||
* stays the authoritative output AND the verify oracle. */
|
||||
spu_shadow_process(out_stereo, frames);
|
||||
|
||||
/* Reverb (PS1-style delay-line): process the final mix in-place */
|
||||
reverb_update_params();
|
||||
reverb_process_stereo(out_stereo, frames);
|
||||
|
||||
/* T1 tap: the SPU's final output block as handed to the host layer
|
||||
* (post-shadow, pre host fade/mute). Placed here so every spu_render
|
||||
* caller — the vblank pump and the turbo fade tail — is covered. */
|
||||
@@ -724,11 +1036,22 @@ void spu_write(uint32_t addr, uint32_t value) {
|
||||
key_off((uint32_t)(uint16_t)value << 16);
|
||||
}
|
||||
|
||||
/* SPU control register (0x1F801DAA): bit 6 = IRQ enable. */
|
||||
if (addr == 0x1F801DAAu) {
|
||||
spu_irq_enabled = ((uint16_t)value >> 6) & 1u;
|
||||
}
|
||||
|
||||
if (addr == 0x1F801DA6u) {
|
||||
transfer_addr = ((uint32_t)(uint16_t)value) << 3;
|
||||
if (transfer_addr >= SPU_RAM_SIZE) transfer_addr = 0;
|
||||
}
|
||||
|
||||
/* SPU IRQ address register (0x1F801D9E): the ADPCM decoder
|
||||
* raises i_stat bit 9 when it crosses this RAM address. */
|
||||
if (addr == 0x1F801D9Eu) {
|
||||
spu_irq_addr = (uint16_t)value;
|
||||
}
|
||||
|
||||
if (addr == 0x1F801DA8u) {
|
||||
if (transfer_addr + 1 < SPU_RAM_SIZE) {
|
||||
spu_ram[transfer_addr] = (uint8_t)(value & 0xFF);
|
||||
|
||||
Reference in New Issue
Block a user