Persist memcards and add SPU audio output

This commit is contained in:
Matthew Stanley
2026-05-09 06:32:22 -07:00
parent 3ef8b72791
commit 9b332ba5be
6 changed files with 445 additions and 36 deletions
+1
View File
@@ -20,6 +20,7 @@ int memcard_write_sector(int card, int sector, const uint8_t* buf);
/* Flush pending writes to disk */
void memcard_flush(int card);
void memcard_flush_all(void);
/* Check if card is present */
int memcard_is_present(int card);
+15
View File
@@ -8,6 +8,21 @@ extern "C" {
#endif
void spu_init(void);
void spu_render(int16_t* out_stereo, int frames);
typedef struct SpuDebugInfo {
uint32_t ctrl;
uint32_t active_mask;
int16_t main_l;
int16_t main_r;
uint32_t key_on_count;
uint64_t render_frames;
uint64_t nonzero_frames;
int32_t last_peak;
int32_t peak;
} SpuDebugInfo;
void spu_debug_info(SpuDebugInfo* out);
/* MMIO read/write (0x1F801C00-0x1F801FFF) */
uint32_t spu_read(uint32_t addr);
+58 -2
View File
@@ -13,6 +13,7 @@
#include "gpu.h"
#include "sio.h"
#include "memcard.h"
#include "spu.h"
#include "interrupts.h"
#include "psx_cycles.h"
#include "card_read_summary.h"
@@ -2032,30 +2033,84 @@ static void handle_sio_state(int id, const char *json)
}
/* ---- Memory card disk-load status (per-slot) ---- */
static void json_escape_string(char *dst, size_t dst_size, const char *src)
{
static const char hex[] = "0123456789ABCDEF";
size_t out = 0;
if (!dst || dst_size == 0) return;
if (!src) src = "";
for (size_t in = 0; src[in] && out + 1 < dst_size; in++) {
unsigned char c = (unsigned char)src[in];
if (c == '"' || c == '\\') {
if (out + 2 >= dst_size) break;
dst[out++] = '\\';
dst[out++] = (char)c;
} else if (c < 0x20) {
if (out + 6 >= dst_size) break;
dst[out++] = '\\';
dst[out++] = 'u';
dst[out++] = '0';
dst[out++] = '0';
dst[out++] = hex[c >> 4];
dst[out++] = hex[c & 0x0F];
} else {
dst[out++] = (char)c;
}
}
dst[out] = '\0';
}
static void handle_mc_status(int id, const char *json)
{
(void)json;
const char *p0 = "", *p1 = "";
char p0_json[1024], p1_json[1024];
uint8_t m0[2] = {0,0}, m1[2] = {0,0};
int pres0 = 0, pres1 = 0, dirty0 = 0, dirty1 = 0;
memcard_debug_info(0, &p0, m0, &pres0, &dirty0);
memcard_debug_info(1, &p1, m1, &pres1, &dirty1);
json_escape_string(p0_json, sizeof(p0_json), p0);
json_escape_string(p1_json, sizeof(p1_json), p1);
send_fmt("{\"id\":%d,\"ok\":true,"
"\"slot0\":{\"present\":%s,\"dirty\":%s,\"path\":\"%s\","
"\"magic\":\"%c%c\",\"magic_hex\":\"%02X%02X\"},"
"\"slot1\":{\"present\":%s,\"dirty\":%s,\"path\":\"%s\","
"\"magic\":\"%c%c\",\"magic_hex\":\"%02X%02X\"}}",
id,
pres0 ? "true" : "false", dirty0 ? "true" : "false", p0,
pres0 ? "true" : "false", dirty0 ? "true" : "false", p0_json,
(m0[0] >= 0x20 && m0[0] < 0x7F) ? m0[0] : '?',
(m0[1] >= 0x20 && m0[1] < 0x7F) ? m0[1] : '?',
m0[0], m0[1],
pres1 ? "true" : "false", dirty1 ? "true" : "false", p1,
pres1 ? "true" : "false", dirty1 ? "true" : "false", p1_json,
(m1[0] >= 0x20 && m1[0] < 0x7F) ? m1[0] : '?',
(m1[1] >= 0x20 && m1[1] < 0x7F) ? m1[1] : '?',
m1[0], m1[1]);
}
static void handle_spu_status(int id, const char *json)
{
(void)json;
SpuDebugInfo info;
spu_debug_info(&info);
send_fmt("{\"id\":%d,\"ok\":true,"
"\"ctrl\":\"0x%04X\",\"active_mask\":\"0x%06X\","
"\"main_l\":%d,\"main_r\":%d,"
"\"key_on_count\":%u,"
"\"render_frames\":%llu,\"nonzero_frames\":%llu,"
"\"last_peak\":%d,\"peak\":%d}",
id,
info.ctrl & 0xFFFFu,
info.active_mask & 0xFFFFFFu,
info.main_l,
info.main_r,
info.key_on_count,
(unsigned long long)info.render_frames,
(unsigned long long)info.nonzero_frames,
info.last_peak,
info.peak);
}
/* ---- SIO IRQ-arm audit -----------------------------------------------
* Reports counts of TX writes that reached the IRQ-arm decision in
* sio_write SIO_TX_DATA, partitioned by active_device. Tells us at the
@@ -3844,6 +3899,7 @@ static const CmdEntry s_commands[] = {
{ "irq_state", handle_irq_state },
{ "sio_state", handle_sio_state },
{ "mc_status", handle_mc_status },
{ "spu_status", handle_spu_status },
{ "card_buffer_dump", handle_card_buffer_dump },
{ "sio_arm_audit", handle_sio_arm_audit },
{ "sio_burst_stats", handle_sio_burst_stats },
+128 -6
View File
@@ -18,6 +18,9 @@
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <filesystem>
#include <string>
#include <vector>
extern "C" uint64_t gte_get_exec_count(void);
@@ -45,6 +48,101 @@ static SDL_Window* sdl_window;
static SDL_Renderer* sdl_renderer;
static SDL_Texture* sdl_texture;
static uint32_t sdl_pixel_buf[640 * 512]; /* ARGB8888 staging buffer */
static SDL_AudioDeviceID sdl_audio_device;
static int16_t sdl_audio_buf[2048 * 2];
static std::filesystem::path find_upward(std::filesystem::path start,
const std::filesystem::path& marker) {
std::error_code ec;
start = std::filesystem::absolute(start, ec);
if (ec) start = std::filesystem::current_path();
if (!std::filesystem::is_directory(start, ec)) start = start.parent_path();
for (;;) {
if (std::filesystem::exists(start / marker, ec)) return start;
if (!start.has_parent_path() || start.parent_path() == start) break;
start = start.parent_path();
}
return {};
}
static std::filesystem::path resolve_bios_path(const char* requested, const char* argv0) {
namespace fs = std::filesystem;
std::error_code ec;
fs::path p(requested);
if (fs::exists(p, ec)) {
fs::path abs = fs::absolute(p, ec);
return ec ? p : abs;
}
if (p.is_absolute()) return p;
std::vector<fs::path> roots;
roots.push_back(fs::current_path());
if (argv0 && argv0[0]) roots.push_back(fs::absolute(argv0, ec).parent_path());
for (const fs::path& root : roots) {
fs::path found = find_upward(root, p);
if (!found.empty()) return found / p;
}
return p;
}
static std::filesystem::path resolve_memcard_dir(const char* requested,
const std::filesystem::path& bios_path,
const char* argv0) {
namespace fs = std::filesystem;
std::error_code ec;
if (requested && requested[0]) return fs::path(requested);
fs::path absolute_bios = fs::absolute(bios_path, ec);
if (ec) absolute_bios = bios_path;
fs::path project_root = absolute_bios.parent_path().parent_path();
if (fs::exists(project_root / "card1.mcd", ec) ||
fs::exists(project_root / "card2.mcd", ec))
return project_root;
fs::path cwd = fs::current_path();
if (fs::exists(cwd / "card1.mcd", ec) || fs::exists(cwd / "card2.mcd", ec))
return cwd;
if (argv0 && argv0[0]) {
fs::path exe_dir = fs::absolute(argv0, ec).parent_path();
if (fs::exists(exe_dir / "card1.mcd", ec) ||
fs::exists(exe_dir / "card2.mcd", ec))
return exe_dir;
}
return cwd;
}
static void shutdown_runtime(void) {
memcard_flush_all();
if (sdl_audio_device) {
SDL_ClearQueuedAudio(sdl_audio_device);
SDL_CloseAudioDevice(sdl_audio_device);
sdl_audio_device = 0;
}
debug_server_shutdown();
}
static void sdl_audio_pump(void) {
if (!sdl_audio_device) return;
const uint32_t bytes_per_frame = sizeof(int16_t) * 2u;
const uint32_t max_queue_bytes = 44100u * bytes_per_frame / 5u;
if (SDL_GetQueuedAudioSize(sdl_audio_device) > max_queue_bytes) return;
static double sample_accum = 0.0;
sample_accum += 44100.0 / 60.0;
int frames = (int)sample_accum;
sample_accum -= (double)frames;
if (frames <= 0) return;
if (frames > 2048) frames = 2048;
spu_render(sdl_audio_buf, frames);
SDL_QueueAudio(sdl_audio_device, sdl_audio_buf,
(uint32_t)frames * bytes_per_frame);
}
/* Convert PS1 16-bit color (1-5-5-5) to ARGB8888 */
static inline uint32_t psx16_to_argb(uint16_t c) {
@@ -111,7 +209,7 @@ static void sdl_vblank_present(void) {
SDL_Event ev;
while (SDL_PollEvent(&ev)) {
if (ev.type == SDL_QUIT) {
debug_server_shutdown();
shutdown_runtime();
std::exit(0);
}
}
@@ -126,6 +224,7 @@ static void sdl_vblank_present(void) {
pad_buttons_this_frame = sio_get_pad_buttons();
}
sio_set_pad_state(pad_buttons_this_frame);
sdl_audio_pump();
/* Turbo mode: while TAB is held, skip both VRAM->ARGB conversion and
* SDL_RenderPresent. The recompiled BIOS still advances simulated
@@ -182,15 +281,24 @@ int main(int argc, char** argv) {
psx_crash_trace_install_handlers();
const char* bios_path = "bios/SCPH1001.BIN";
const char* memcard_dir_arg = nullptr;
/* Parse positional. First non-flag arg = bios_path. */
for (int i = 1; i < argc; i++) {
if (argv[i][0] != '-') {
if (std::strcmp(argv[i], "--memcard-dir") == 0 && i + 1 < argc) {
memcard_dir_arg = argv[++i];
} else if (argv[i][0] != '-') {
bios_path = argv[i];
}
}
std::fprintf(stdout, "psxrecomp-v4 runtime: loading BIOS from %s\n", bios_path);
memory_init(bios_path);
std::filesystem::path resolved_bios = resolve_bios_path(bios_path, argv[0]);
std::filesystem::path memcard_dir =
resolve_memcard_dir(memcard_dir_arg, resolved_bios, argv[0]);
std::string bios_path_str = resolved_bios.string();
std::string memcard_dir_str = memcard_dir.string();
std::fprintf(stdout, "psxrecomp-v4 runtime: loading BIOS from %s\n", bios_path_str.c_str());
memory_init(bios_path_str.c_str());
gpu_init();
dma_init();
timers_init();
@@ -199,7 +307,8 @@ int main(int argc, char** argv) {
sio_connect_pad(0); /* Controller on port 1 */
spu_init();
cdrom_init(NULL); /* No disc for BIOS-only boot */
memcard_init("."); /* Look for card1.mcd / card2.mcd in working directory */
memcard_init(memcard_dir_str.c_str());
std::atexit(memcard_flush_all);
#ifdef DEFAULT_DEBUG_PORT
debug_server_init(DEFAULT_DEBUG_PORT);
#else
@@ -211,6 +320,19 @@ int main(int argc, char** argv) {
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
return 1;
}
if (SDL_InitSubSystem(SDL_INIT_AUDIO) == 0) {
SDL_AudioSpec want;
SDL_AudioSpec have;
SDL_zero(want);
want.freq = 44100;
want.format = AUDIO_S16SYS;
want.channels = 2;
want.samples = 1024;
sdl_audio_device = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (sdl_audio_device) {
SDL_PauseAudioDevice(sdl_audio_device, 0);
}
}
sdl_window = SDL_CreateWindow(
"psxrecomp-v4 — BIOS",
@@ -355,7 +477,7 @@ int main(int argc, char** argv) {
/* If we reach here, all execution completed without MMIO abort. */
std::fprintf(stdout, "psxrecomp-v4 runtime: execution completed, PC=0x%08X\n", cpu.pc);
debug_server_shutdown();
shutdown_runtime();
SDL_DestroyTexture(sdl_texture);
SDL_DestroyRenderer(sdl_renderer);
SDL_DestroyWindow(sdl_window);
+12 -3
View File
@@ -129,9 +129,18 @@ void memcard_flush(int card) {
FILE* f = fopen(cards[card].filepath, "wb");
if (f) {
fwrite(cards[card].data, 1, MEMCARD_SIZE, f);
fclose(f);
cards[card].dirty = 0;
size_t n = fwrite(cards[card].data, 1, MEMCARD_SIZE, f);
int flush_ok = (fflush(f) == 0);
int close_ok = (fclose(f) == 0);
if (n == MEMCARD_SIZE && flush_ok && close_ok) {
cards[card].dirty = 0;
}
}
}
void memcard_flush_all(void) {
for (int i = 0; i < MAX_CARDS; i++) {
memcard_flush(i);
}
}
+231 -25
View File
@@ -1,63 +1,270 @@
/*
* spu.c — PS1 Sound Processing Unit register-level simulation
* spu.c - PS1 Sound Processing Unit register and direct ADPCM voice model.
*
* Accepts all SPU register reads/writes at 0x1F801C00-0x1F801FFF and
* DMA4 transfers into 512KB SPU RAM. No audio synthesis yet — this
* exists so the BIOS and games can write SPU registers without crashing.
*
* Audio output will be added in a later phase using SDL_audio with the
* ADPCM decoder from psxrecomp-v2/runner/src/spu.cpp as reference.
*
* Reference: nocash PSX specs — SPU section
* Register map: v2 spu.cpp header comment (voice regs, global regs)
* This is intentionally still a compact hardware model: it accepts SPU
* register reads/writes, DMA4 transfers into 512KB SPU RAM, and mixes the
* 24 direct ADPCM voices. Reverb, noise, sweep volumes, XA/CD input and IRQ
* timing are not modeled yet.
*/
#include "spu.h"
#include <string.h>
#define SPU_RAM_SIZE (512 * 1024)
#define SPU_REG_COUNT 256 /* 0x1F801C00-0x1F801DFF as uint16_t[256] */
#define SPU_RAM_SIZE (512 * 1024)
#define SPU_REG_COUNT 256
#define SPU_VOICE_COUNT 24
#define SPU_BLOCK_SAMPLES 28
static uint8_t spu_ram[SPU_RAM_SIZE];
static uint16_t spu_regs[SPU_REG_COUNT];
static uint32_t transfer_addr; /* byte address in SPU RAM */
static uint32_t transfer_addr;
static uint32_t key_on_count;
static uint64_t render_frames;
static uint64_t nonzero_frames;
static int32_t last_peak;
static int32_t peak;
typedef struct {
int active;
uint32_t cur_addr;
uint32_t repeat_addr;
int16_t samples[SPU_BLOCK_SAMPLES];
int sample_idx;
uint32_t phase;
int16_t hist1;
int16_t hist2;
uint8_t flags;
} SpuVoice;
static SpuVoice voices[SPU_VOICE_COUNT];
static inline int16_t clamp16(int32_t v) {
if (v > 32767) return 32767;
if (v < -32768) return -32768;
return (int16_t)v;
}
static inline int32_t abs32(int32_t v) {
return v < 0 ? -v : v;
}
static inline uint32_t reg_index(uint32_t addr) {
return (addr - 0x1F801C00u) >> 1;
}
static inline uint16_t voice_reg(int voice, int reg) {
return spu_regs[(uint32_t)voice * 8u + (uint32_t)reg];
}
static inline int16_t direct_volume(uint16_t raw) {
int32_t v;
if (raw & 0x8000u) {
/* Sweep mode is not modeled; use the magnitude as a direct volume. */
v = (int32_t)(raw & 0x7FFFu);
} else {
v = (int32_t)(raw & 0x7FFFu);
if (v & 0x4000) v -= 0x8000;
}
if (v > 0x3FFF) v = 0x3FFF;
if (v < -0x4000) v = -0x4000;
return (int16_t)v;
}
static void decode_block(SpuVoice *v) {
static const int f0[5] = { 0, 60, 115, 98, 122 };
static const int f1[5] = { 0, 0, -52, -55, -60 };
uint32_t addr = v->cur_addr & (SPU_RAM_SIZE - 1u);
if (addr + 16u > SPU_RAM_SIZE) addr = 0;
uint8_t header = spu_ram[addr + 0u];
uint8_t flags = spu_ram[addr + 1u];
int shift = header & 0x0F;
int filter = (header >> 4) & 0x0F;
if (filter > 4) filter = 0;
if (shift > 12) shift = 12;
int out = 0;
for (int b = 0; b < 14; b++) {
uint8_t packed = spu_ram[addr + 2u + (uint32_t)b];
for (int n = 0; n < 2; n++) {
int sample4 = (n == 0) ? (packed & 0x0F) : (packed >> 4);
if (sample4 & 0x08) sample4 -= 0x10;
int32_t s = sample4 << 12;
s >>= shift;
s += ((int32_t)v->hist1 * f0[filter] +
(int32_t)v->hist2 * f1[filter] + 32) >> 6;
s = clamp16(s);
v->hist2 = v->hist1;
v->hist1 = (int16_t)s;
v->samples[out++] = (int16_t)s;
}
}
if (flags & 0x04u) v->repeat_addr = addr;
v->flags = flags;
v->sample_idx = 0;
v->cur_addr = (addr + 16u) & (SPU_RAM_SIZE - 1u);
}
static int16_t voice_next_sample(int idx) {
SpuVoice *v = &voices[idx];
if (!v->active) return 0;
if (v->sample_idx >= SPU_BLOCK_SAMPLES) {
if (v->flags & 0x01u) {
if (v->flags & 0x02u) {
v->cur_addr = v->repeat_addr & (SPU_RAM_SIZE - 1u);
} else {
v->active = 0;
return 0;
}
}
decode_block(v);
}
int16_t s = v->samples[v->sample_idx];
uint32_t pitch = voice_reg(idx, 2) & 0x3FFFu;
if (pitch == 0) pitch = 0x1000u;
v->phase += pitch;
while (v->phase >= 0x1000u) {
v->phase -= 0x1000u;
v->sample_idx++;
if (v->sample_idx >= SPU_BLOCK_SAMPLES) break;
}
return s;
}
static void key_on(uint32_t mask) {
for (int i = 0; i < SPU_VOICE_COUNT; i++) {
if (!(mask & (1u << i))) continue;
SpuVoice *v = &voices[i];
memset(v, 0, sizeof(*v));
v->active = 1;
v->cur_addr = ((uint32_t)voice_reg(i, 3) << 3) & (SPU_RAM_SIZE - 1u);
v->repeat_addr = ((uint32_t)voice_reg(i, 7) << 3) & (SPU_RAM_SIZE - 1u);
v->sample_idx = SPU_BLOCK_SAMPLES;
key_on_count++;
}
}
static void key_off(uint32_t mask) {
for (int i = 0; i < SPU_VOICE_COUNT; i++) {
if (mask & (1u << i)) voices[i].active = 0;
}
}
void spu_init(void) {
memset(spu_ram, 0, sizeof(spu_ram));
memset(spu_regs, 0, sizeof(spu_regs));
memset(voices, 0, sizeof(voices));
transfer_addr = 0;
key_on_count = 0;
render_frames = 0;
nonzero_frames = 0;
last_peak = 0;
peak = 0;
}
void spu_render(int16_t* out_stereo, int frames) {
if (!out_stereo || frames <= 0) return;
uint16_t ctrl = spu_regs[reg_index(0x1F801DAAu)];
int enabled = (ctrl & 0x8000u) != 0;
int16_t main_l = direct_volume(spu_regs[reg_index(0x1F801D80u)]);
int16_t main_r = direct_volume(spu_regs[reg_index(0x1F801D82u)]);
int32_t block_peak = 0;
for (int f = 0; f < frames; f++) {
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);
if (!s) continue;
int16_t vl = direct_volume(voice_reg(v, 0));
int16_t vr = direct_volume(voice_reg(v, 1));
mix_l += ((int32_t)s * vl) >> 14;
mix_r += ((int32_t)s * vr) >> 14;
}
mix_l = (mix_l * main_l) >> 14;
mix_r = (mix_r * main_r) >> 14;
}
out_stereo[f * 2 + 0] = clamp16(mix_l);
out_stereo[f * 2 + 1] = clamp16(mix_r);
int32_t frame_peak = abs32(out_stereo[f * 2 + 0]);
int32_t right_peak = abs32(out_stereo[f * 2 + 1]);
if (right_peak > frame_peak) frame_peak = right_peak;
if (frame_peak) nonzero_frames++;
if (frame_peak > block_peak) block_peak = frame_peak;
}
render_frames += (uint64_t)frames;
last_peak = block_peak;
if (block_peak > peak) peak = block_peak;
}
void spu_debug_info(SpuDebugInfo* out) {
if (!out) return;
memset(out, 0, sizeof(*out));
out->ctrl = spu_regs[reg_index(0x1F801DAAu)];
out->main_l = direct_volume(spu_regs[reg_index(0x1F801D80u)]);
out->main_r = direct_volume(spu_regs[reg_index(0x1F801D82u)]);
for (int i = 0; i < SPU_VOICE_COUNT; i++) {
if (voices[i].active) out->active_mask |= (1u << i);
}
out->key_on_count = key_on_count;
out->render_frames = render_frames;
out->nonzero_frames = nonzero_frames;
out->last_peak = last_peak;
out->peak = peak;
}
uint32_t spu_read(uint32_t addr) {
if (addr >= 0x1F801C00u && addr <= 0x1F801DFFu) {
uint32_t idx = (addr - 0x1F801C00u) >> 1;
uint32_t idx = reg_index(addr);
if (idx < SPU_REG_COUNT) {
/* SPUSTAT (0x1F801DAE): report ready, not busy */
if (addr == 0x1F801DAEu) {
return 0x0000;
return 0x0400; /* SPUSTAT: ready */
}
if (addr == 0x1F801D9Cu) {
uint32_t bits = 0;
for (int i = 0; i < 16; i++)
if (voices[i].active) bits |= (1u << i);
return bits;
}
if (addr == 0x1F801D9Eu) {
uint32_t bits = 0;
for (int i = 16; i < SPU_VOICE_COUNT; i++)
if (voices[i].active) bits |= (1u << (i - 16));
return bits;
}
return spu_regs[idx];
}
}
/* 0x1F801E00-0x1F801FFF: voice current volume (read-only, return 0) */
return 0;
}
void spu_write(uint32_t addr, uint32_t value) {
if (addr >= 0x1F801C00u && addr <= 0x1F801DFFu) {
uint32_t idx = (addr - 0x1F801C00u) >> 1;
uint32_t idx = reg_index(addr);
if (idx < SPU_REG_COUNT) {
spu_regs[idx] = (uint16_t)value;
/* Transfer Address register (0x1F801DA6):
* value = byte_addr >> 3 (units of 8 bytes) */
if (addr == 0x1F801D88u) key_on((uint32_t)(uint16_t)value);
if (addr == 0x1F801D8Au) key_on((uint32_t)(uint16_t)value << 16);
if (addr == 0x1F801D8Cu) key_off((uint32_t)(uint16_t)value);
if (addr == 0x1F801D8Eu) key_off((uint32_t)(uint16_t)value << 16);
if (addr == 0x1F801DA6u) {
transfer_addr = ((uint32_t)(uint16_t)value) << 3;
if (transfer_addr >= SPU_RAM_SIZE)
transfer_addr = 0;
if (transfer_addr >= SPU_RAM_SIZE) transfer_addr = 0;
}
/* Transfer Data FIFO (0x1F801DA8): write 16-bit to SPU RAM */
if (addr == 0x1F801DA8u) {
if (transfer_addr + 1 < SPU_RAM_SIZE) {
spu_ram[transfer_addr] = (uint8_t)(value & 0xFF);
@@ -70,7 +277,6 @@ void spu_write(uint32_t addr, uint32_t value) {
}
void spu_dma_write(uint32_t word) {
/* DMA4 writes 32-bit words (two 16-bit samples) to SPU RAM */
if (transfer_addr + 3 < SPU_RAM_SIZE) {
spu_ram[transfer_addr] = (uint8_t)(word & 0xFF);
spu_ram[transfer_addr + 1] = (uint8_t)((word >> 8) & 0xFF);
@@ -81,7 +287,7 @@ void spu_dma_write(uint32_t word) {
}
int spu_dma_ready(void) {
return 1; /* always ready — synchronous model */
return 1;
}
const uint8_t* spu_get_ram(void) {