diff --git a/docs/internal/upstream/kem0x-pr14-cdrom-encoded-interrupts.md b/docs/internal/upstream/kem0x-pr14-cdrom-encoded-interrupts.md new file mode 100644 index 00000000..ef6a92b5 --- /dev/null +++ b/docs/internal/upstream/kem0x-pr14-cdrom-encoded-interrupts.md @@ -0,0 +1,29 @@ +# kem0x PR 14: encoded CD-ROM interrupt matching + +Evaluated and adapted on 2026-07-13. + +## Provenance + +The CD-ROM interrupt mask fix was rebuilt from Kareem Olim's +[PSXrecomp PR 14](https://github.com/mstan/psxrecomp/pull/14), specifically +commit +[`dd9e5c65073dd41fbac2d965d2f945a866ec59e2`](https://github.com/mstan/psxrecomp/commit/dd9e5c65073dd41fbac2d965d2f945a866ec59e2) +(`Deliver encoded CD interrupt reasons correctly`). The implementation here +centralizes the mask/status comparison in a tested internal helper rather than +copying the source patch verbatim. + +The behavior follows the +[PSX-SPX CD-ROM register documentation](https://psx-spx.consoledev.net/cdromdrive/#0x1f801803-read-banks-1-and-3-hintsts): +HINTSTS bits 0..2 are an encoded HC05 interrupt reason, but the interrupt line +is asserted whenever `HINTMSK & HINTSTS` is nonzero. Converting the reason to a +one-hot bit before applying the mask is therefore incorrect. A common `0x07` +mask must deliver encoded INT4 (DataEnd) and INT5 (DiskError) as well as +INT1--INT3. + +## Deliberately excluded + +This branch does not import PR 14's WebAssembly/web frontend, Pepsiman-specific +hooks or patches, CD-DA playback changes, SPU changes, raster/rendering changes, +or any other runtime and build-system material. Those changes have different +scope and review requirements and are not needed for encoded interrupt mask +matching. diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index 79682f7b..b8ce75bd 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -11,6 +11,7 @@ */ #include "cdrom.h" +#include "cdrom_irq.h" #include "dma.h" #include "spu.h" #include "event_ring.h" @@ -578,7 +579,8 @@ static void present_cdrom_irq(void) { * the guest's command/ack write — and thus from being lost to that ISR's * own trailing INTC ack. */ if (cdrom_irq_present_delay > 0) return; - if (irq_flag && (irq_enable & (1 << (irq_flag - 1))) && !cdrom_intc_request_latched) { + if (cdrom_irq_mask_matches_reason(irq_enable, irq_flag) && + !cdrom_intc_request_latched) { psx_irq_raise(2, irq_flag); /* IRQ_CDROM; detail = CD response/IRQ type */ cdrom_intc_request_latched = 1; cdrom_intc_latched_generation = cdrom_irq_generation; @@ -590,7 +592,7 @@ static void present_cdrom_irq(void) { /* Fire CDROM IRQ into the interrupt controller (explicit, per command/response). * Trace the masked case here only (low-frequency); refresh stays silent. */ static void fire_cdrom_irq(void) { - if (irq_flag && !(irq_enable & (1 << (irq_flag - 1)))) { + if (irq_flag && !cdrom_irq_mask_matches_reason(irq_enable, irq_flag)) { trace_cdrom('f', 0, irq_flag, 0); } present_cdrom_irq(); @@ -1841,7 +1843,7 @@ uint32_t cdrom_cycles_to_irq(uint32_t i_mask) { if (!(i_mask & (1u << 2))) return 0xFFFFFFFFu; /* IRQ_CDROM masked */ uint32_t best = 0xFFFFFFFFu; /* Armed response awaiting presentation (will raise bit2 when delay hits 0). */ - if (irq_flag && (irq_enable & (1u << (irq_flag - 1)))) { + if (cdrom_irq_mask_matches_reason(irq_enable, irq_flag)) { uint32_t d = cdrom_irq_present_delay > 0 ? (uint32_t)cdrom_irq_present_delay : 0u; if (d < best) best = d; } diff --git a/runtime/src/cdrom_irq.h b/runtime/src/cdrom_irq.h new file mode 100644 index 00000000..82e03e76 --- /dev/null +++ b/runtime/src/cdrom_irq.h @@ -0,0 +1,15 @@ +#ifndef PSXRECOMP_CDROM_IRQ_H +#define PSXRECOMP_CDROM_IRQ_H + +#include + +/* HINTSTS bits 0..2 contain an encoded interrupt reason (INT1..INT5), not + * independent one-hot flags. The controller nevertheless gates its IRQ line + * by directly ANDing HINTSTS with HINTMSK. In particular, mask 0x07 enables + * all five encoded reasons, including INT4 and INT5. */ +static inline int cdrom_irq_mask_matches_reason(uint8_t irq_enable, + uint8_t irq_reason) { + return (irq_enable & irq_reason & 0x07u) != 0; +} + +#endif /* PSXRECOMP_CDROM_IRQ_H */ diff --git a/runtime/tests/test_cdrom_irq_mask.c b/runtime/tests/test_cdrom_irq_mask.c new file mode 100644 index 00000000..645702eb --- /dev/null +++ b/runtime/tests/test_cdrom_irq_mask.c @@ -0,0 +1,50 @@ +/* + * Validate PS1 CD-ROM interrupt mask matching. + * + * Build: + * cc -std=c99 -Wall -Wextra -Werror -I../src \ + * -o test_cdrom_irq_mask test_cdrom_irq_mask.c + */ +#include "cdrom_irq.h" + +#include +#include + +static int failures; + +#define CHECK(expected, mask, reason, label) do { \ + int actual = cdrom_irq_mask_matches_reason((uint8_t)(mask), \ + (uint8_t)(reason)); \ + if (actual != (expected)) { \ + fprintf(stderr, "FAIL: %s (mask=0x%02X reason=%u expected=%d got=%d)\n",\ + (label), (unsigned)(mask), (unsigned)(reason), \ + (expected), actual); \ + failures++; \ + } \ +} while (0) + +int main(void) { + /* The conventional 0x07 mask must deliver every encoded HC05 reason. */ + for (uint8_t reason = 1; reason <= 5; reason++) { + CHECK(1, 0x07, reason, "mask 0x07 enables INT1 through INT5"); + } + + /* The mask/status comparison is bitwise, not a reason-to-one-hot lookup. */ + CHECK(0, 0x01, 2, "bit 0 does not match INT2"); + CHECK(1, 0x01, 3, "bit 0 matches encoded INT3"); + CHECK(0, 0x02, 4, "bit 1 does not match INT4"); + CHECK(1, 0x04, 4, "bit 2 matches encoded INT4"); + CHECK(1, 0x01, 5, "bit 0 matches encoded INT5"); + CHECK(1, 0x04, 5, "bit 2 matches encoded INT5"); + + CHECK(0, 0x00, 1, "zero mask suppresses an interrupt"); + CHECK(0, 0x07, 0, "reason zero means no interrupt"); + CHECK(0, 0x18, 5, "audio-buffer mask bits do not match an HC05 reason"); + + if (failures) { + fprintf(stderr, "FAILED (%d)\n", failures); + return 1; + } + puts("ALL PASS"); + return 0; +}