From 783014e53254fe17102a34c30120eeabf5227a47 Mon Sep 17 00:00:00 2001 From: spicyjpeg Date: Wed, 19 Oct 2022 14:15:28 +0200 Subject: Clean up SDK debug logging, fix getTPage() --- examples/graphics/gte/main.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'examples') diff --git a/examples/graphics/gte/main.c b/examples/graphics/gte/main.c index 6907c84..3c85d84 100644 --- a/examples/graphics/gte/main.c +++ b/examples/graphics/gte/main.c @@ -100,9 +100,9 @@ INDEX cube_indices[] = { /* source color when using gte_nccs(). 4096 is 1.0 in this matrix */ /* A column of zeroes disables the light source. */ MATRIX color_mtx = { - ONE / 2, 0, 0, /* Red */ - ONE / 2, 0, 0, /* Green */ - ONE / 2, 0, 0 /* Blue */ + ONE * 3/4, 0, 0, /* Red */ + ONE * 3/4, 0, 0, /* Green */ + ONE * 3/4, 0, 0 /* Blue */ }; /* Light matrix */ -- cgit v1.2.3 From f6c41f3783c4fce49a9899b710ebb50ba9f647ab Mon Sep 17 00:00:00 2001 From: spicyjpeg Date: Thu, 27 Oct 2022 16:55:08 +0200 Subject: Refactor sound examples, add new spustream example --- CHANGELOG.md | 14 + examples/README.md | 7 +- examples/sound/cdstream/CMakeLists.txt | 23 ++ examples/sound/cdstream/iso.xml | 28 ++ examples/sound/cdstream/main.c | 439 ++++++++++++++++++++++++++++ examples/sound/cdstream/stream.vag | Bin 0 -> 4646912 bytes examples/sound/cdstream/system.cnf | 4 + examples/sound/spustream/CMakeLists.txt | 16 +- examples/sound/spustream/convert_stream.py | 112 ------- examples/sound/spustream/interleave.py | 152 ++++++++++ examples/sound/spustream/iso.xml | 28 -- examples/sound/spustream/main.c | 360 +++++++++-------------- examples/sound/spustream/stream.bin | Bin 4685824 -> 0 bytes examples/sound/spustream/stream.vag | Bin 0 -> 1140736 bytes examples/sound/spustream/system.cnf | 4 - examples/sound/vagsample/3dfx.vag | Bin 227936 -> 227968 bytes examples/sound/vagsample/main.c | 455 +++++++++++++---------------- examples/sound/vagsample/proyt.vag | Bin 189264 -> 189248 bytes libpsn00b/include/hwregs_a.inc | 22 +- libpsn00b/include/hwregs_c.h | 21 +- libpsn00b/include/psxspu.h | 14 +- libpsn00b/psxspu/common.c | 15 +- 22 files changed, 1049 insertions(+), 665 deletions(-) create mode 100644 examples/sound/cdstream/CMakeLists.txt create mode 100644 examples/sound/cdstream/iso.xml create mode 100644 examples/sound/cdstream/main.c create mode 100644 examples/sound/cdstream/stream.vag create mode 100644 examples/sound/cdstream/system.cnf delete mode 100644 examples/sound/spustream/convert_stream.py create mode 100644 examples/sound/spustream/interleave.py delete mode 100644 examples/sound/spustream/iso.xml delete mode 100644 examples/sound/spustream/stream.bin create mode 100644 examples/sound/spustream/stream.vag delete mode 100644 examples/sound/spustream/system.cnf (limited to 'examples') diff --git a/CHANGELOG.md b/CHANGELOG.md index e9d7e16..d6b49e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,20 @@ to ensure the changelog can be parsed correctly. ------------------------------------------------------------------------------- +# 2022-10-27 + +spicyjpeg: + +- psxspu: Fixed bugs in `SpuInit()` and in `SpuWrite()` when using manual + transfer mode (`SPU_TRANSFER_BY_IO`). Added `SpuWritePartly()`. + +- psxetc: Added `IRQ_Channel` and `DMA_Channel` enums. + +- examples: Refactored and bugfixed all sound examples. Renamed the old + `spustream` example to `cdstream` and added a new `spustream` example + demonstrating SPU audio streaming from main RAM. Both streaming examples now + make use of the interleaved .VAG file format. + ## 2022-10-21: 0.21 spicyjpeg: diff --git a/examples/README.md b/examples/README.md index ade94b0..ae601f1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -26,8 +26,9 @@ Additional information may be found in the source code of each example. | [`lowlevel/cartrom`](./lowlevel/cartrom) | ROM firmware for cheat devices written using GNU GAS | ROM | 4 | | [`mdec/mdecimage`](./mdec/mdecimage) | Displays a (raw) MDEC format image | EXE | | | [`mdec/strvideo`](./mdec/strvideo) | Plays a .STR video file using the MDEC | CD | 1 | -| [`sound/spustream`](./sound/spustream) | Custom (non XA) CD-ROM audio streaming using the SPU | CD | | -| [`sound/vagsample`](./sound/vagsample) | Demonstrates playing VAG sound files using the SPU | EXE | | +| [`sound/cdstream`](./sound/cdstream) | Streams an interleaved .VAG file from the CD-ROM | CD | | +| [`sound/spustream`](./sound/spustream) | Streams an interleaved .VAG file from main RAM | EXE | | +| [`sound/vagsample`](./sound/vagsample) | Loads and plays .VAG sound files using the SPU | EXE | | | [`system/childexec`](./system/childexec) | Loading a child program and returning to parent | EXE | | | [`system/console`](./system/console) | TTY based text console that interrupts gameplay | EXE | | | [`system/dynlink`](./system/dynlink) | Demonstrates dynamically linked libraries | CD | | @@ -85,4 +86,4 @@ are for rebuilding the examples *after* the SDK has been installed. CD images for each example. ----------------------------------------- -_Last updated on 2022-10-16 by spicyjpeg_ +_Last updated on 2022-10-27 by spicyjpeg_ diff --git a/examples/sound/cdstream/CMakeLists.txt b/examples/sound/cdstream/CMakeLists.txt new file mode 100644 index 0000000..e569449 --- /dev/null +++ b/examples/sound/cdstream/CMakeLists.txt @@ -0,0 +1,23 @@ +# PSn00bSDK example CMake script +# (C) 2021 spicyjpeg - MPL licensed + +cmake_minimum_required(VERSION 3.21) + +project( + cdstream + LANGUAGES C + VERSION 1.0.0 + DESCRIPTION "PSn00bSDK SPU CD audio streaming example" + HOMEPAGE_URL "http://lameguy64.net/?page=psn00bsdk" +) + +file(GLOB _sources *.c) +psn00bsdk_add_executable(cdstream GPREL ${_sources}) +psn00bsdk_add_cd_image(cdstream_iso cdstream iso.xml DEPENDS cdstream) + +install( + FILES + ${PROJECT_BINARY_DIR}/cdstream.bin + ${PROJECT_BINARY_DIR}/cdstream.cue + TYPE BIN +) diff --git a/examples/sound/cdstream/iso.xml b/examples/sound/cdstream/iso.xml new file mode 100644 index 0000000..66f1f74 --- /dev/null +++ b/examples/sound/cdstream/iso.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + diff --git a/examples/sound/cdstream/main.c b/examples/sound/cdstream/main.c new file mode 100644 index 0000000..636ef10 --- /dev/null +++ b/examples/sound/cdstream/main.c @@ -0,0 +1,439 @@ +/* + * PSn00bSDK SPU CD-ROM streaming example + * (C) 2022 spicyjpeg - MPL licensed + * + * This is an extended version of the sound/spustream example demonstrating + * playback of a large multi-channel audio file from the CD using the SPU, + * without having to rely on the CD drive's own ability to play CD-DA or XA + * tracks. + * + * The main difference from spustream is that the SPU IRQ handler does not + * upload a chunk from main RAM to SPU RAM immediately, it only sets a flag. + * The main loop checks if the flag has been set and starts reading the next + * chunk from the CD into a buffer in RAM asynchronously; the chunk is then + * uploaded to the SPU and the IRQ is re-enabled. + * + * Chunks are read once again from an interleaved .VAG file, laid out on the + * disc as follows: + * + * +--Sector--+--Sector--+--Sector--+--Sector--+--Sector--+--Sector--+---- + * | | +--------------------+---------------------+ | + * | .VAG | | Left channel data | Right channel data | Padding | ... + * | header | +--------------------+---------------------+ | + * +----------+----------+----------+----------+----------+----------+---- + * \__________________Chunk___________________/ + * + * Note that chunks have to be large enough to give the drive enough time to + * seek from one chunk to another. The included .VAG file has been encoded with + * a chunk size of 0x7000 bytes, however you might want to try smaller sizes to + * reduce SPU RAM usage. Chunk size can be set by passing the -b option to the + * .VAG interleaving script included in the spustream directory. + * + * Implementing SPU streaming might seem pointless, but it actually has a + * number of advantages over CD-DA or XA: + * + * - Any sample rate up to 44.1 kHz can be used. The sample rate can also be + * changed on-the-fly to play the stream at different speeds and pitches (as + * long as the CD drive can keep up), or even interpolated for effects like + * tape stops. + * - Manual streaming is not limited to mono or stereo but can be expanded to + * as many channels as needed, only limited by the amount of SPU RAM required + * for chunks and CD bandwidth. Having more than 2 channels can be useful for + * e.g. smoothly crossfading between tracks (not possible with XA) or + * controlling volume and panning of each instrument separately. + * - XA playback tends to skip on consoles with a worn out drive, as XA sectors + * cannot have any error correction data. SPU streaming is not subject to + * this limitation since sectors are read and processed in software. + * - Depending on how streaming/interleaving is implemented it is possible to + * have 500-1000ms idle periods during which the CD drive isn't buffering the + * stream, that can be used to read small amounts of other data without ever + * interrupting playback. This is different from XA-style interleaving as the + * drive is free to seek to *any* region of the disc during these periods (it + * must seek back to the stream's next chunk afterwards though). + * - It is also possible to seek back to the beginning of the stream and load + * the first chunk before the end is reached, allowing for seamless looping + * without having to resort to tricks like separate filler samples. + * - Finally, SPU streaming can be used on some PS1-based arcade boards that + * use IDE/SCSI drives or flash memory for storage and thus lack support for + * XA or CD-DA playback. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern const uint8_t stream_data[]; + +#define NUM_CHANNELS 2 + +/* Display/GPU context utilities */ + +#define SCREEN_XRES 320 +#define SCREEN_YRES 240 + +#define BGCOLOR_R 48 +#define BGCOLOR_G 24 +#define BGCOLOR_B 0 + +typedef struct { + DISPENV disp; + DRAWENV draw; +} Framebuffer; + +typedef struct { + Framebuffer db[2]; + int db_active; +} RenderContext; + +void init_context(RenderContext *ctx) { + Framebuffer *db; + + ResetGraph(0); + ctx->db_active = 0; + + db = &(ctx->db[0]); + SetDefDispEnv(&(db->disp), 0, 0, SCREEN_XRES, SCREEN_YRES); + SetDefDrawEnv(&(db->draw), SCREEN_XRES, 0, SCREEN_XRES, SCREEN_YRES); + setRGB0(&(db->draw), BGCOLOR_R, BGCOLOR_G, BGCOLOR_B); + db->draw.isbg = 1; + db->draw.dtd = 1; + + db = &(ctx->db[1]); + SetDefDispEnv(&(db->disp), SCREEN_XRES, 0, SCREEN_XRES, SCREEN_YRES); + SetDefDrawEnv(&(db->draw), 0, 0, SCREEN_XRES, SCREEN_YRES); + setRGB0(&(db->draw), BGCOLOR_R, BGCOLOR_G, BGCOLOR_B); + db->draw.isbg = 1; + db->draw.dtd = 1; + + PutDrawEnv(&(db->draw)); + //PutDispEnv(&(db->disp)); + + // Create a text stream at the top of the screen. + FntLoad(960, 0); + FntOpen(8, 16, 304, 208, 2, 512); +} + +void display(RenderContext *ctx) { + Framebuffer *db; + + DrawSync(0); + VSync(0); + ctx->db_active ^= 1; + + db = &(ctx->db[ctx->db_active]); + PutDrawEnv(&(db->draw)); + PutDispEnv(&(db->disp)); + SetDispMask(1); +} + +/* .VAG header structure */ + +typedef struct { + uint32_t magic; // 0x69474156 ("VAGi") for interleaved files + uint32_t version; + uint32_t interleave; // Little-endian, size of each channel buffer + uint32_t size; // Big-endian, in bytes + uint32_t sample_rate; // Big-endian, in Hertz + uint32_t _reserved[3]; + char name[16]; +} VAG_Header; + +#define SWAP_ENDIAN(x) ( \ + (((uint32_t) (x) & 0x000000ff) << 24) | \ + (((uint32_t) (x) & 0x0000ff00) << 8) | \ + (((uint32_t) (x) & 0x00ff0000) >> 8) | \ + (((uint32_t) (x) & 0xff000000) >> 24) \ +) + +/* Interrupt callbacks */ + +// The first 4 KB of SPU RAM are reserved for capture buffers and psxspu +// additionally uploads a dummy sample (16 bytes) at 0x1000 by default, so the +// chunks must be placed after those. The dummy sample is going to be used to +// keep unused SPU channels busy, preventing them from accidentally triggering +// the SPU IRQ and throwing off the timing (all channels are always reading +// from SPU RAM, even when "stopped"). +// https://problemkaputt.de/psx-spx.htm#spuinterrupt +#define DUMMY_BLOCK_ADDR 0x1000 +#define BUFFER_START_ADDR 0x1010 + +typedef enum { + STATE_IDLE, + STATE_DATA_NEEDED, + STATE_READING, + STATE_BUFFERING +} StreamState; + +typedef struct { + uint32_t *read_buffer; + int lba, chunk_secs; + int buffer_size, num_chunks, sample_rate; + + volatile int next_chunk, spu_addr; + volatile int8_t db_active, state; +} StreamContext; + +static StreamContext str_ctx; + +void spu_irq_handler(void) { + // Acknowledge the interrupt to ensure it can be triggered again. The only + // way to do this is actually to disable the interrupt entirely; we'll + // enable it again once the chunk is ready. + SPU_CTRL &= 0xffbf; + + int chunk_size = str_ctx.buffer_size * NUM_CHANNELS; + int chunk = (str_ctx.next_chunk + 1) % (uint32_t) str_ctx.num_chunks; + + str_ctx.db_active ^= 1; + str_ctx.state = STATE_DATA_NEEDED; + str_ctx.next_chunk = chunk; + + // Configure to SPU to trigger an IRQ once the chunk that is going to be + // filled now starts playing (so the next buffer can be loaded) and + // override both channels' loop addresses to make them "jump" to the new + // buffers, rather than actually looping when they encounter the loop flag + // at the end of the currently playing buffers. + int addr = BUFFER_START_ADDR + (str_ctx.db_active ? chunk_size : 0); + str_ctx.spu_addr = addr; + + SPU_IRQ_ADDR = getSPUAddr(addr); + for (int i = 0; i < NUM_CHANNELS; i++) + SPU_CH_LOOP_ADDR(i) = getSPUAddr(addr + str_ctx.buffer_size * i); + + // Note that we can't call CdRead() here as it requires interrupts to be + // enabled. Instead, feed_stream() (called from the main loop) will check + // if str_ctx.state is set to STATE_DATA_NEEDED and fetch the next chunk. +} + +void cd_read_handler(int event, uint8_t *payload) { + // Attempt to read the chunk again if an error has occurred, otherwise + // start uploading it to SPU RAM. + if (event == CdlDiskError) { + str_ctx.state = STATE_DATA_NEEDED; + return; + } + + SpuSetTransferStartAddr(str_ctx.spu_addr); + SpuWrite(str_ctx.read_buffer, str_ctx.buffer_size * NUM_CHANNELS); + + str_ctx.state = STATE_BUFFERING; +} + +void spu_dma_handler(void) { + // Re-enable the SPU IRQ once the new chunk has been fully uploaded. + SPU_CTRL |= 0x0040; + + str_ctx.state = STATE_IDLE; +} + +/* Helper functions */ + +// This isn't actually required for this example, however it is necessary if +// you want to allocate the stream buffers into a region of SPU RAM that was +// previously used (to make sure the IRQ isn't going to be triggered by any +// inactive channels). +void reset_spu_channels(void) { + SpuSetKey(0, 0x00ffffff); + + for (int i = 0; i < 24; i++) { + SPU_CH_ADDR(i) = getSPUAddr(DUMMY_BLOCK_ADDR); + SPU_CH_FREQ(i) = 0x1000; + } + + SpuSetKey(1, 0x00ffffff); +} + +void feed_stream(void) { + if (str_ctx.state != STATE_DATA_NEEDED) + return; + + // Start reading the next chunk from the CD. + int lba = str_ctx.lba + str_ctx.next_chunk * str_ctx.chunk_secs; + + CdlLOC pos; + CdIntToPos(lba, &pos); + CdControl(CdlSetloc, &pos, 0); + + CdReadCallback(&cd_read_handler); + CdRead(str_ctx.chunk_secs, str_ctx.read_buffer, CdlModeSpeed); + + str_ctx.state = STATE_READING; +} + +void init_stream(const CdlLOC *pos) { + EnterCriticalSection(); + InterruptCallback(IRQ_SPU, &spu_irq_handler); + DMACallback(DMA_SPU, &spu_dma_handler); + ExitCriticalSection(); + + // Read the header. Note that in interleaved .VAG files the first sector. + uint32_t header[512]; + CdControl(CdlSetloc, pos, 0); + + CdReadCallback(0); + CdRead(1, header, CdlModeSpeed); + CdReadSync(0, 0); + + VAG_Header *vag = (VAG_Header *) header; + int buf_size = vag->interleave; + int chunk_secs = ((buf_size * NUM_CHANNELS) + 2047) / 2048; + + str_ctx.read_buffer = malloc(chunk_secs * 2048); + str_ctx.lba = CdPosToInt(pos) + 1; + str_ctx.chunk_secs = chunk_secs; + str_ctx.buffer_size = buf_size; + str_ctx.num_chunks = (SWAP_ENDIAN(vag->size) + buf_size - 1) / buf_size; + str_ctx.sample_rate = SWAP_ENDIAN(vag->sample_rate); + + str_ctx.db_active = 1; + str_ctx.next_chunk = -1; + + // Ensure at least one chunk is in SPU RAM by invoking the IRQ handler + // manually and blocking until the chunk has loaded. + spu_irq_handler(); + while (str_ctx.state != STATE_IDLE) + feed_stream(); +} + +void start_stream(void) { + int bits = 0x00ffffff >> (24 - NUM_CHANNELS); + + for (int i = 0; i < NUM_CHANNELS; i++) { + SPU_CH_ADDR(i) = getSPUAddr(str_ctx.spu_addr + str_ctx.buffer_size * i); + SPU_CH_FREQ(i) = getSPUSampleRate(str_ctx.sample_rate); + SPU_CH_ADSR1(i) = 0x80ff; + SPU_CH_ADSR2(i) = 0x1fee; + } + + // Unmute the channels and route them for stereo output. You'll want to + // edit this if you are using more than 2 channels, and/or if you want to + // provide an option to output mono audio instead of stereo. + SPU_CH_VOL_L(0) = 0x3fff; + SPU_CH_VOL_R(0) = 0x0000; + SPU_CH_VOL_L(1) = 0x0000; + SPU_CH_VOL_R(1) = 0x3fff; + + spu_irq_handler(); + SpuSetKey(1, bits); +} + +// This is basically a variant of reset_spu_channels() that only resets the +// channels used to play the stream, to (again) prevent them from triggering +// the SPU IRQ while the stream is paused. +void stop_stream(void) { + int bits = 0x00ffffff >> (24 - NUM_CHANNELS); + + SpuSetKey(0, bits); + + for (int i = 0; i < NUM_CHANNELS; i++) + SPU_CH_ADDR(i) = getSPUAddr(DUMMY_BLOCK_ADDR); + + SpuSetKey(1, bits); +} + +/* Main */ + +static RenderContext ctx; + +#define SHOW_STATUS(...) { FntPrint(-1, __VA_ARGS__); FntFlush(-1); display(&ctx); } +#define SHOW_ERROR(...) { SHOW_STATUS(__VA_ARGS__); while (1) __asm__("nop"); } + +static const char *state_strings[] = { "IDLE", "DATA NEEDED", "READING", "BUFFERING" }; + +int main(int argc, const char* argv[]) { + init_context(&ctx); + SpuInit(); + CdInit(); + reset_spu_channels(); + SHOW_STATUS(""); + + // Set up controller polling. + uint8_t pad_buff[2][34]; + InitPAD(pad_buff[0], 34, pad_buff[1], 34); + StartPAD(); + ChangeClearPAD(0); + + CdlFILE file; + SHOW_STATUS("OPENING STREAM FILE\n"); + if (!CdSearchFile(&file, "\\STREAM.VAG")) + SHOW_ERROR("FAILED TO FIND STREAM.VAG\n"); + + SHOW_STATUS("BUFFERING STREAM\n"); + init_stream(&file.pos); + start_stream(); + + int paused = 0, sample_rate = getSPUSampleRate(str_ctx.sample_rate); + + uint16_t last_buttons = 0xffff; + + while (1) { + feed_stream(); + + FntPrint(-1, "PLAYING SPU STREAM\n\n"); + FntPrint(-1, "BUFFER: %d\n", str_ctx.db_active); + FntPrint(-1, "STATUS: %s\n\n", state_strings[str_ctx.state]); + + FntPrint(-1, "POSITION: %d/%d\n", str_ctx.next_chunk, str_ctx.num_chunks); + FntPrint(-1, "SMP RATE: %5d HZ\n\n", (sample_rate * 44100) >> 12); + + FntPrint(-1, "[START] %s\n", paused ? "RESUME" : "PAUSE"); + FntPrint(-1, "[LEFT/RIGHT] SEEK\n"); + FntPrint(-1, "[O] RESET POSITION\n"); + FntPrint(-1, "[UP/DOWN] CHANGE SAMPLE RATE\n"); + FntPrint(-1, "[X] RESET SAMPLE RATE\n"); + + FntFlush(-1); + display(&ctx); + + // Check if a compatible controller is connected and handle button + // presses. + PADTYPE *pad = (PADTYPE *) pad_buff[0]; + if (pad->stat) + continue; + if ( + (pad->type != PAD_ID_DIGITAL) && + (pad->type != PAD_ID_ANALOG_STICK) && + (pad->type != PAD_ID_ANALOG) + ) + continue; + + if ((last_buttons & PAD_START) && !(pad->btn & PAD_START)) { + paused ^= 1; + if (paused) + stop_stream(); + else + start_stream(); + } + + if (!(pad->btn & PAD_LEFT)) + str_ctx.next_chunk--; + if (!(pad->btn & PAD_RIGHT)) + str_ctx.next_chunk++; + if ((last_buttons & PAD_CIRCLE) && !(pad->btn & PAD_CIRCLE)) + str_ctx.next_chunk = -1; + + if (!(pad->btn & PAD_DOWN) && (sample_rate > 0x400)) + sample_rate -= 0x40; + if (!(pad->btn & PAD_UP) && (sample_rate < 0x2000)) + sample_rate += 0x40; + if ((last_buttons & PAD_CROSS) && !(pad->btn & PAD_CROSS)) + sample_rate = getSPUSampleRate(str_ctx.sample_rate); + + // Only set the sample rate registers if necessary. + if (pad->btn != 0xffff) { + for (int i = 0; i < NUM_CHANNELS; i++) + SPU_CH_FREQ(i) = sample_rate; + } + + last_buttons = pad->btn; + } + + return 0; +} diff --git a/examples/sound/cdstream/stream.vag b/examples/sound/cdstream/stream.vag new file mode 100644 index 0000000..a6faf74 Binary files /dev/null and b/examples/sound/cdstream/stream.vag differ diff --git a/examples/sound/cdstream/system.cnf b/examples/sound/cdstream/system.cnf new file mode 100644 index 0000000..11ca055 --- /dev/null +++ b/examples/sound/cdstream/system.cnf @@ -0,0 +1,4 @@ +BOOT=cdrom:\cdstream.exe;1 +TCB=4 +EVENT=10 +STACK=801FFFF0 diff --git a/examples/sound/spustream/CMakeLists.txt b/examples/sound/spustream/CMakeLists.txt index 63d113b..465e291 100644 --- a/examples/sound/spustream/CMakeLists.txt +++ b/examples/sound/spustream/CMakeLists.txt @@ -5,20 +5,16 @@ cmake_minimum_required(VERSION 3.21) project( spustream - LANGUAGES C + LANGUAGES C ASM VERSION 1.0.0 - DESCRIPTION "PSn00bSDK SPU custom streaming example" + DESCRIPTION "PSn00bSDK SPU audio streaming example" HOMEPAGE_URL "http://lameguy64.net/?page=psn00bsdk" ) -# TODO: add rules to actually generate a valid STREAM.BIN file file(GLOB _sources *.c) psn00bsdk_add_executable(spustream GPREL ${_sources}) -psn00bsdk_add_cd_image(spustream_iso spustream iso.xml DEPENDS spustream) +#psn00bsdk_add_cd_image(spustream_iso spustream iso.xml DEPENDS spustream) -install( - FILES - ${PROJECT_BINARY_DIR}/spustream.bin - ${PROJECT_BINARY_DIR}/spustream.cue - TYPE BIN -) +psn00bsdk_target_incbin(spustream PRIVATE stream_data stream.vag) + +install(FILES ${PROJECT_BINARY_DIR}/spustream.exe TYPE BIN) diff --git a/examples/sound/spustream/convert_stream.py b/examples/sound/spustream/convert_stream.py deleted file mode 100644 index 1b1696f..0000000 --- a/examples/sound/spustream/convert_stream.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env python3 -# Simple .VAG to STREAM.BIN interleaving tool -# (C) 2021 spicyjpeg - MPL licensed - -import sys -from warnings import warn -from struct import Struct -from itertools import zip_longest -from argparse import ArgumentParser, FileType - -VAG_HEADER = Struct("> 4s I 4x 2I 12x 16s") -VAG_MAGIC = b"VAGp" -SAMPLE_RATE = 44100 -BUFFER_SIZE = 26624 # (26624 / 16 * 28) / 44100 = 1.05 seconds -ALIGN_SIZE = 2048 - -## Helpers - -def align(data, size): - chunks = (len(data) + size - 1) // size - - return data.ljust(chunks * size, b"\x00") - -def set_loop_flag(data): - last_block = bytearray(data[-16:]) - last_block[1] = 0x03 # Jump to loop point + sustain - - return data[:-16] + last_block - -## .VAG file reader - -def read_vag(_file, chunk_size): - with _file: - header = _file.read(VAG_HEADER.size) - ( - magic, - version, - size, - sample_rate, - name - ) = VAG_HEADER.unpack(header) - - #if magic != VAG_MAGIC: - #raise RuntimeError(f"{_file.name} is not a valid .VAG file") - if sample_rate != SAMPLE_RATE: - warn(RuntimeWarning(f"{_file.name} sample rate is not {SAMPLE_RATE} Hz")) - - for i in range(0, size, chunk_size): - chunk = _file.read(chunk_size) - - if len(chunk) % 16: - warn(RuntimeWarning(f"{_file.name} is not 16-byte aligned, trimming")) - chunk = chunk[0:len(chunk) // 16 * 16] - - chunk = set_loop_flag(chunk) - - yield chunk.ljust(chunk_size, b"\x00") - -## Main - -def get_args(): - parser = ArgumentParser( - description = "Generates interleaved stream data from one or more .VAG files." - ) - parser.add_argument( - "input_file", - nargs = "+", - type = FileType("rb"), - help = f"mono input files for each channel (must be {SAMPLE_RATE} Hz .VAG)" - ) - parser.add_argument( - "-o", "--output", - type = FileType("wb"), - default = "stream.bin", - help = "where to output converted stream data (stream.bin by default)", - metavar = "file" - ) - parser.add_argument( - "-b", "--buffer-size", - type = int, - default = BUFFER_SIZE, - help = f"size of each interleaved chunk (one per channel, default {BUFFER_SIZE})", - metavar = "bytes" - ) - parser.add_argument( - "-a", "--align", - type = int, - default = ALIGN_SIZE, - help = f"align each group of chunks to N bytes (default {ALIGN_SIZE})", - metavar = "bytes" - ) - - return parser.parse_args() - -def main(): - args = get_args() - if args.buffer_size % 16: - raise ValueError("buffer size must be a multiple of 16 bytes") - - interleave = zip_longest( - *( read_vag(_file, args.buffer_size) for _file in args.input_file ), - fillvalue = b"\x00" * args.buffer_size - ) - - with args.output as _file: - for chunks in interleave: - data = b"".join(chunks) - - _file.write(align(data, args.align)) - -if __name__ == "__main__": - main() diff --git a/examples/sound/spustream/interleave.py b/examples/sound/spustream/interleave.py new file mode 100644 index 0000000..4e68974 --- /dev/null +++ b/examples/sound/spustream/interleave.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# Simple .VAG interleaving tool +# (C) 2021-2022 spicyjpeg - MPL licensed + +import os, sys +from warnings import warn +from struct import Struct +from itertools import zip_longest +from argparse import ArgumentParser, FileType + +VAG_HEADER = Struct("> 4s I 4s 2I 12x 16s") +VAG_MAGIC = b"VAGp" +VAGI_MAGIC = b"VAGi" +VAG_VERSION = 0x20 +BUFFER_SIZE = 0x1000 +CHUNK_ALIGN = 0x800 + +## Helpers + +def align(data, size): + chunks = (len(data) + size - 1) // size + + return data.ljust(chunks * size, b"\x00") + +def get_loop_offset(data): + for index, flag in enumerate(data[1::16]): + if flag & 0x01: + return index * 16 + + return len(data) - 16 + +## .VAG file reader + +class VAGReader: + def __init__(self, _file): + self.file = _file + header = _file.read(VAG_HEADER.size) + + ( + magic, _, _, + self.size, + self.sample_rate, + self.name + ) = VAG_HEADER.unpack(header) + + if magic == VAGI_MAGIC: + raise RuntimeError(f"{_file.name} is an interleaved .VAG file (must be mono)") + if magic != VAG_MAGIC: + raise RuntimeError(f"{_file.name} is not a valid .VAG file") + + def read(self, chunk_size): + for _ in range(0, self.size, chunk_size): + chunk = self.file.read(chunk_size) + + if len(chunk) < 16: + break + if len(chunk) % 16: + warn(RuntimeWarning(f"{self.file.name} is not 16-byte aligned, trimming")) + chunk = chunk[0:(len(chunk) // 16) * 16] + + # If there already is an end flag in the chunk replace it with a + # loop flag, otherwise add a new loop flag at the end. + end = get_loop_offset(chunk) + chunk = bytearray(chunk) + + chunk[end + 1] = 0x03 # Jump to loop point + sustain + yield chunk.ljust(chunk_size, b"\x00") + +## Main + +def get_args(): + parser = ArgumentParser( + description = "Generates interleaved audio stream data from one or more .VAG files." + ) + parser.add_argument( + "input_file", + nargs = "+", + type = FileType("rb"), + help = "mono input files for each channel in .VAG format" + ) + parser.add_argument( + "output_file", + type = FileType("wb"), + help = "where to output converted stream data" + ) + parser.add_argument( + "-b", "--buffer-size", + type = int, + default = BUFFER_SIZE, + help = f"size of each channel buffer in each chunk (default {BUFFER_SIZE})", + metavar = "bytes" + ) + parser.add_argument( + "-a", "--align", + type = int, + default = CHUNK_ALIGN, + help = f"pad each chunk to a multiple of the given size (default {CHUNK_ALIGN})", + metavar = "bytes" + ) + parser.add_argument( + "-r", "--raw", + action = "store_true", + help = "do not add an interleaved .VAG header to the output file" + ) + + return parser.parse_args() + +def main(): + args = get_args() + if args.buffer_size % 16: + raise ValueError("buffer size must be a multiple of 16 bytes") + if args.buffer_size % args.align: + warn(RuntimeWarning(f"buffer size should be a multiple of {args.align}")) + + input_files = tuple(map(VAGReader, args.input_file)) + size = input_files[0].size + sample_rate = input_files[0].sample_rate + + if (not args.raw) and (len(input_files) != 2): + warn(RuntimeWarning("interleaved .VAG only supports stereo (2 input files)")) + + for vag in input_files[1:]: + if vag.size != size: + warn(RuntimeWarning(f"{vag.file.name} has a different file size")) + if vag.sample_rate != sample_rate: + warn(RuntimeWarning(f"{vag.file.name} has a different sample rate")) + + interleave = zip_longest( + *( vag.read(args.buffer_size) for vag in input_files ), + fillvalue = b"\x00" * args.buffer_size + ) + + with args.output_file as _file: + if not args.raw: + header = VAG_HEADER.pack( + VAGI_MAGIC, + VAG_VERSION, + args.buffer_size.to_bytes(4, "little"), + size, + sample_rate, + os.path.basename(_file.name).encode()[0:16] + ) + + _file.write(align(header, args.align)) + + for chunks in interleave: + data = b"".join(chunks) + + _file.write(align(data, args.align)) + +if __name__ == "__main__": + main() diff --git a/examples/sound/spustream/iso.xml b/examples/sound/spustream/iso.xml deleted file mode 100644 index 050d673..0000000 --- a/examples/sound/spustream/iso.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/examples/sound/spustream/main.c b/examples/sound/spustream/main.c index 1fee883..68cf9b0 100644 --- a/examples/sound/spustream/main.c +++ b/examples/sound/spustream/main.c @@ -1,111 +1,60 @@ /* - * PSn00bSDK SPU audio streaming example - * (C) 2021 spicyjpeg - MPL licensed + * PSn00bSDK SPU .VAG streaming example + * (C) 2022 spicyjpeg - MPL licensed * - * This example demonstrates how to play a large multi-channel audio file - * "manually" by streaming it through the SPU, without having to rely on the CD - * drive's ability to play audio tracks or XA files. + * This example shows how to play arbitrarily long sounds, which normally would + * not fit into SPU RAM in their entirety, by streaming them to the SPU from + * main RAM. In this example audio data is streamed from an in-memory file, + * however the code can easily be modified to stream from the CD instead (see + * the cdstream example). * - * The way this works is by splitting the audio file into a series of ~1 second - * "chunks", each of which in turn is an array of concatenated buffers holding - * SPU ADPCM data (one for each channel, so a stereo stream would have 2 - * buffers per chunk). All buffers in a chunk are played simultaneously using - * multiple SPU channels; each buffer has the loop flag set at the end, so each - * channel will jump to its loop address (SPU_CH_LOOP_ADDR(n)) once the chunk - * is played. + * The way SPU streaming works is by splitting the audio data into a series of + * small "chunks", each of which in turn is an array of concatenated buffers + * holding SPU ADPCM data (one for each channel, so a stereo stream would have + * 2 buffers per chunk). All buffers in a chunk are played simultaneously using + * multiple SPU channels; each buffer has the loop flag set at the end, so the + * SPU will jump to the loop point set in the SPU_CH_LOOP_ADDR registers after + * the chunk is played. * - * Since the loop point doesn't necessarily have to be within the chunk itself, - * we can abuse it to "queue" another set of buffers to be played immediately - * after the currently playing chunk. This allows us to fetch a chunk from the - * CD, upload it to SPU RAM (2048 bytes at a time to avoid having to keep - * another large buffer in main RAM) and queue it for playback while a - * previously buffered chunk is playing in the background. SPU RAM always holds - * two chunks, one of which is played while the other one is buffered. This is - * the layout used in this example: + * As the loop point doesn't necessarily have to be within the chunk itself, it + * can be used to "queue" another chunk to be played immediately after the + * current one. This allows for double buffering: two chunks are always kept in + * SPU RAM and one is overwritten with while the other is playing. Chunks are + * laid out in SPU RAM as follows: * - * /================================================\ - * | /==================\ | - * v Loop point | v Loop point | + * ________________________________________________ + * / __________________ \ + * | / \ | + * v Loop point | Loop flag v Loop point | Loop flag * +-------+----------------+----------------+----------------+----------------+ * | Dummy | Left buffer 0 | Right buffer 0 | Left buffer 1 | Right buffer 1 | * +-------+----------------+----------------+----------------+----------------+ * \____________Chunk 0____________/ \____________Chunk 1____________/ * - * It's pretty much the same thing as GPU double buffering (aka page flipping), - * just with chunks instead of framebuffers. + * In order to keep streaming continuously we need to know when each chunk + * actually starts playing. The SPU can be configured to trigger an interrupt + * whenever a specific address in SPU RAM is read by a channel, so we can just + * point it to the beginning of the buffered chunk's first buffer and wait + * until the IRQ is fired before loading the next chunk. * - * We need to know when the chunk we've buffered actually starts playing in - * order to start buffering the next one. The SPU can be configured to trigger - * an interrupt whenever a specific address in SPU RAM is read by a channel, so - * we can just point it to the beginning of the buffered chunk's first buffer. - * The interrupt callback will then kick off CD reading and adjust the loop/IRQ - * addresses to the ones of the chunk that is going to be buffered next. - * - * Chunks are read from a STREAM.BIN file which is just a series of sector - * aligned chunks, arranged as follows: - * - * +--Sector--+--Sector--+--Sector--+--Sector--+--Sector--+--Sector--+---- - * | +--------------------------+--------------------------+ | - * | | Left channel data | Right channel data | Padding | ... - * | +--------------------------+--------------------------+ | - * +----------+----------+----------+----------+----------+----------+---- - * \________________________Chunk________________________/ - * - * A Python script is included to generate STREAM.BIN from one or more SPU - * ADPCM (.VAG) files, one for each channel (the .VAG format only supports - * mono). - * - * Of course SPU streaming isn't the only way to play music, as the CD drive - * can play CD-DA tracks and XA files natively with zero CPU overhead. However - * streaming has a number of advantages over CD audio or XA: - * - * - Any sample rate up to 44.1 kHz can be used. The sample rate can also be - * changed on-the-fly to play the stream at different speeds and pitches (as - * long as the CD drive can keep up of course), or even interpolated for - * effects like tape stops or DJ scratches. - * - Manual streaming is not limited to mono or stereo but can be expanded to - * as many channels as needed, only limited by the amount of SPU RAM required - * for chunks and CD bandwidth. Having more than 2 channels can be useful for - * e.g. crossfading between tracks (not possible with XA) or controlling - * volume and panning of each individual instrument. - * - Depending on how streaming/interleaving is implemented it is possible to - * have 500-1000ms idle periods during which the CD drive isn't buffering the - * stream, that can be used to read small amounts of other data without ever - * interrupting playback. This is different from XA-style interleaving as the - * drive is free to seek to *any* region of the disc during these periods - * (it must seek back to the stream's next chunk afterwards though). - * - Thanks to the idle periods it is possible to seek back to the beginning of - * the stream and preload the first chunk before the end is reached, allowing - * the track to be looped seamlessly without having to resort to tricks like - * filler samples. - * - Unlike XA, SPU streaming can be used on some PS1-based arcade boards such - * as the Konami System 573. These systems usually use IDE/SCSI CD drives or - * flash memory, neither of which supports XA playback. + * Chunks are read from a special type of .VAG file which has been interleaved + * ahead-of-time and already contains the loop flags required to make streaming + * work. A Python script is provided to generate such file from one or more + * mono .VAG files. */ #include -#include -#include -#include +#include #include #include #include #include #include -#include #include -// To maximize STREAM.BIN packing efficiency and get rid of padding between -// chunks, buffer size should be a multiple of sector size (2048 bytes). Buffer -// size can be increased to get more idle time between CD reads, however it is -// usually best to keep it to 1-2 seconds as SPU RAM is only 512 KB. -#define SAMPLE_RATE 0x1000 // 44100 Hz -#define BUFFER_SIZE 0x6800 // (0x6800 / 16 * 28) / 44100 = 1.05 seconds - -#define NUM_CHANNELS 2 -#define CHANNEL_MASK 0x03 +extern const uint8_t stream_data[]; -#define SPU_RAM_ADDR(x) ((uint16_t) (((uint32_t) (x)) >> 3)) +#define NUM_CHANNELS 2 /* Display/GPU context utilities */ @@ -167,159 +116,137 @@ void display(RenderContext *ctx) { SetDispMask(1); } -/* Stream interrupt handlers */ +/* .VAG header structure */ -// The first 4 KB of SPU RAM are reserved for capture buffers, so we have to -// place stream buffers after those. A dummy sample is additionally placed by -// default by the SPU library at 0x1000; it is going to be used here to keep -// unused SPU channels busy, preventing them from accidentally triggering the -// SPU RAM interrupt and throwing off the timing (all channels are always -// reading sample data, even when "stopped"). +typedef struct { + uint32_t magic; // 0x69474156 ("VAGi") for interleaved files + uint32_t version; + uint32_t interleave; // Little-endian, size of each channel buffer + uint32_t size; // Big-endian, in bytes + uint32_t sample_rate; // Big-endian, in Hertz + uint32_t _reserved[3]; + char name[16]; +} VAG_Header; + +#define SWAP_ENDIAN(x) ( \ + (((uint32_t) (x) & 0x000000ff) << 24) | \ + (((uint32_t) (x) & 0x0000ff00) << 8) | \ + (((uint32_t) (x) & 0x00ff0000) >> 8) | \ + (((uint32_t) (x) & 0xff000000) >> 24) \ +) + +/* Interrupt callbacks */ + +// The first 4 KB of SPU RAM are reserved for capture buffers and psxspu +// additionally uploads a dummy sample (16 bytes) at 0x1000 by default, so the +// chunks must be placed after those. The dummy sample is going to be used to +// keep unused SPU channels busy, preventing them from accidentally triggering +// the SPU IRQ and throwing off the timing (all channels are always reading +// from SPU RAM, even when "stopped"). // https://problemkaputt.de/psx-spx.htm#spuinterrupt -#define DUMMY_BLOCK_ADDR 0x1000 -#define BUFFER_START_ADDR 0x1010 -#define CHUNK_SIZE (BUFFER_SIZE * NUM_CHANNELS) +#define DUMMY_BLOCK_ADDR 0x1000 +#define BUFFER_START_ADDR 0x1010 + +typedef enum { + STATE_IDLE, + STATE_BUFFERING +} StreamState; typedef struct { - int lba, length; + const uint8_t *data; + int buffer_size, num_chunks, sample_rate; - volatile int pos; - volatile int spu_addr, spu_pos; - volatile int db_active; + volatile int next_chunk, spu_addr; + volatile int8_t db_active, state; } StreamContext; static StreamContext str_ctx; -// This buffer is used by cd_event_handler() as a temporary area for sectors -// read from the CD and uploaded to SPU RAM. Due to DMA limitations it can't be -// allocated on the stack (especially not in the interrupt callbacks' stack, -// whose size is very limited). -static uint32_t sector_buffer[512]; - void spu_irq_handler(void) { // Acknowledge the interrupt to ensure it can be triggered again. The only // way to do this is actually to disable the interrupt entirely; we'll - // enable it again once the buffer is ready. + // enable it again once the chunk is ready. SPU_CTRL &= 0xffbf; - str_ctx.db_active ^= 1; - str_ctx.spu_pos = 0; + int chunk_size = str_ctx.buffer_size * NUM_CHANNELS; + int chunk = (str_ctx.next_chunk + 1) % (uint32_t) str_ctx.num_chunks; - // Align the sector counter to the size of a chunk (to prevent glitches - // after seeking) and reset it if it exceeds the stream's length. - str_ctx.pos %= str_ctx.length; - str_ctx.pos -= str_ctx.pos % ((CHUNK_SIZE + 2047) / 2048); + str_ctx.db_active ^= 1; + str_ctx.state = STATE_BUFFERING; + str_ctx.next_chunk = chunk; - // Configure to SPU to trigger an IRQ once the buffer that is going to be + // Configure to SPU to trigger an IRQ once the chunk that is going to be // filled now starts playing (so the next buffer can be loaded) and // override both channels' loop addresses to make them "jump" to the new - // buffer rather than actually looping when they encounter the loop flag at - // the end of the currently playing buffer. - str_ctx.spu_addr = BUFFER_START_ADDR + CHUNK_SIZE * str_ctx.db_active; - SPU_IRQ_ADDR = SPU_RAM_ADDR(str_ctx.spu_addr); + // buffers, rather than actually looping when they encounter the loop flag + // at the end of the currently playing buffers. + int addr = BUFFER_START_ADDR + (str_ctx.db_active ? chunk_size : 0); + str_ctx.spu_addr = addr; + SPU_IRQ_ADDR = getSPUAddr(addr); for (int i = 0; i < NUM_CHANNELS; i++) - SPU_CH_LOOP_ADDR(i) = SPU_RAM_ADDR(str_ctx.spu_addr + BUFFER_SIZE * i); + SPU_CH_LOOP_ADDR(i) = getSPUAddr(addr + str_ctx.buffer_size * i); - // Start loading the next chunk. cd_event_handler() will be called - // repeatedly for each sector until the entire chunk is read. - CdlLOC pos; - CdIntToPos(str_ctx.lba + str_ctx.pos, &pos); - CdControlF(CdlReadN, &pos); + // Start uploading the next chunk to the SPU. + SpuSetTransferStartAddr(addr); + SpuWrite((const uint32_t *) &str_ctx.data[chunk * chunk_size], chunk_size); } -void cd_event_handler(int event, uint8_t *payload) { - // Ignore all events other than a sector being ready. - // TODO: read errors should be handled properly - if (event != CdlDataReady) - return; - - // Fetch the sector that has been read from the drive. - CdGetSector(sector_buffer, 512); - str_ctx.pos++; - - // Set loop flags to make sure the buffer will loop (actually jump to the - // other buffer, as we're overriding loop addresses) at the end. - // NOTE: this isn't actually necessary here as the stream converter script - // already sets these flags in the file. - /*for (int i = 0; i < NUM_CHANNELS; i++) { - if ( - str_ctx.spu_pos >= (BUFFER_SIZE * i - 2048) && - str_ctx.spu_pos < (BUFFER_SIZE * i) - ) - sector_buffer[(BUFFER_SIZE * i - str_ctx.spu_pos) - 15] = 0x03; - }*/ - - // Copy the sector to SPU RAM, appending it to the buffer that is not - // playing currently. As the left and right buffers are adjacent, we can - // just treat the chunk as a single blob of data and copy it as-is; we only - // have to trim the padding at the end (if any) to avoid overwriting other - // data in SPU RAM. - size_t length = CHUNK_SIZE - str_ctx.spu_pos; - if (length > 2048) - length = 2048; - - SpuSetTransferStartAddr(str_ctx.spu_addr + str_ctx.spu_pos); - SpuWrite(sector_buffer, length); - str_ctx.spu_pos += length; - - // If the buffer has been filled completely, stop reading and re-enable the - // SPU IRQ. - if (str_ctx.spu_pos >= CHUNK_SIZE) { - CdControlF(CdlPause, 0); - SPU_CTRL |= 0x0040; - } +void spu_dma_handler(void) { + // Re-enable the SPU IRQ once the new chunk has been fully uploaded. + SPU_CTRL |= 0x0040; + + str_ctx.state = STATE_IDLE; } -/* Stream helpers */ +/* Helper functions */ // This isn't actually required for this example, however it is necessary if // you want to allocate the stream buffers into a region of SPU RAM that was // previously used (to make sure the IRQ isn't going to be triggered by any // inactive channels). void reset_spu_channels(void) { - SPU_KEY_OFF = 0x00ffffff; + SpuSetKey(0, 0x00ffffff); for (int i = 0; i < 24; i++) { - SPU_CH_ADDR(i) = SPU_RAM_ADDR(DUMMY_BLOCK_ADDR); + SPU_CH_ADDR(i) = getSPUAddr(DUMMY_BLOCK_ADDR); SPU_CH_FREQ(i) = 0x1000; } - SPU_KEY_ON = 0x00ffffff; + SpuSetKey(1, 0x00ffffff); } -void init_stream(CdlFILE *file) { +void init_stream(const VAG_Header *vag) { EnterCriticalSection(); - InterruptCallback(9, &spu_irq_handler); - CdReadyCallback(&cd_event_handler); + InterruptCallback(IRQ_SPU, &spu_irq_handler); + DMACallback(DMA_SPU, &spu_dma_handler); ExitCriticalSection(); - // Configure the CD drive to read 2048-byte sectors at 2x speed. - uint8_t mode = CdlModeSpeed; - CdControl(CdlSetmode, (const uint8_t *) &mode, 0); + int buf_size = vag->interleave; + + str_ctx.data = &((const uint8_t *) vag)[2048]; + str_ctx.buffer_size = buf_size; + str_ctx.num_chunks = (SWAP_ENDIAN(vag->size) + buf_size - 1) / buf_size; + str_ctx.sample_rate = SWAP_ENDIAN(vag->sample_rate); - // Set the initial LBA of the stream file, which is going to be incremented - // as the stream is played. - str_ctx.lba = CdPosToInt(&(file->pos)); - str_ctx.length = file->size / 2048; - str_ctx.pos = 0; + str_ctx.db_active = 1; + str_ctx.next_chunk = -1; - // Ensure at least one chunk is in SPU RAM by invoking the SPU IRQ handler + // Ensure at least one chunk is in SPU RAM by invoking the IRQ handler // manually and blocking until the chunk has loaded. - str_ctx.db_active = 1; spu_irq_handler(); - - while (str_ctx.spu_pos < CHUNK_SIZE) + while (str_ctx.state != STATE_IDLE) __asm__ volatile(""); } void start_stream(void) { - uint32_t addr = BUFFER_START_ADDR + CHUNK_SIZE * str_ctx.db_active; + int bits = 0x00ffffff >> (24 - NUM_CHANNELS); for (int i = 0; i < NUM_CHANNELS; i++) { - SPU_CH_ADDR(i) = SPU_RAM_ADDR(addr + BUFFER_SIZE * i); - SPU_CH_FREQ(i) = SAMPLE_RATE; - SPU_CH_ADSR(i) = 0x1fee80ff; + SPU_CH_ADDR(i) = getSPUAddr(str_ctx.spu_addr + str_ctx.buffer_size * i); + SPU_CH_FREQ(i) = getSPUSampleRate(str_ctx.sample_rate); + SPU_CH_ADSR1(i) = 0x80ff; + SPU_CH_ADSR2(i) = 0x1fee; } // Unmute the channels and route them for stereo output. You'll want to @@ -330,35 +257,31 @@ void start_stream(void) { SPU_CH_VOL_L(1) = 0x0000; SPU_CH_VOL_R(1) = 0x3fff; - SPU_KEY_ON = CHANNEL_MASK; spu_irq_handler(); + SpuSetKey(1, bits); } // This is basically a variant of reset_spu_channels() that only resets the // channels used to play the stream, to (again) prevent them from triggering // the SPU IRQ while the stream is paused. void stop_stream(void) { - SPU_KEY_OFF = CHANNEL_MASK; + int bits = 0x00ffffff >> (24 - NUM_CHANNELS); + + SpuSetKey(0, bits); for (int i = 0; i < NUM_CHANNELS; i++) - SPU_CH_ADDR(i) = SPU_RAM_ADDR(DUMMY_BLOCK_ADDR); + SPU_CH_ADDR(i) = getSPUAddr(DUMMY_BLOCK_ADDR); - SPU_KEY_ON = CHANNEL_MASK; + SpuSetKey(1, bits); } /* Main */ static RenderContext ctx; -#define SHOW_STATUS(...) { FntPrint(-1, __VA_ARGS__); FntFlush(-1); display(&ctx); } -#define SHOW_ERROR(...) { SHOW_STATUS(__VA_ARGS__); while (1) __asm__("nop"); } - int main(int argc, const char* argv[]) { init_context(&ctx); - - SHOW_STATUS("INITIALIZING\n"); SpuInit(); - CdInit(); reset_spu_channels(); // Set up controller polling. @@ -367,34 +290,19 @@ int main(int argc, const char* argv[]) { StartPAD(); ChangeClearPAD(0); - SHOW_STATUS("OPENING STREAM FILE\n"); - - CdlFILE file; - if (!CdSearchFile(&file, "\\STREAM.BIN")) - SHOW_ERROR("FAILED TO FIND STREAM.BIN\n"); - - SHOW_STATUS("BUFFERING STREAM\n"); - init_stream(&file); + init_stream((const VAG_Header *) stream_data); start_stream(); - int paused = 0; + int paused = 0, sample_rate = getSPUSampleRate(str_ctx.sample_rate); - uint16_t sample_rate = SAMPLE_RATE; uint16_t last_buttons = 0xffff; while (1) { FntPrint(-1, "PLAYING SPU STREAM\n\n"); + FntPrint(-1, "BUFFER: %d\n", str_ctx.db_active); + FntPrint(-1, "STATUS: %s\n\n", str_ctx.state ? "BUFFERING" : "IDLE"); - FntPrint(-1, "BUFFER: %d\nSTATUS: ", str_ctx.db_active); - if (str_ctx.spu_pos >= CHUNK_SIZE) - FntPrint(-1, "IDLE\n\n"); - else if (str_ctx.spu_pos) - FntPrint(-1, "BUFFERING\n\n"); - else - FntPrint(-1, "SEEKING\n\n"); - - FntPrint(-1, "POSITION: %5d/%5d\n", str_ctx.pos, str_ctx.length); - FntPrint(-1, "BUFFERED: %5d/%5d\n", str_ctx.spu_pos, CHUNK_SIZE); + FntPrint(-1, "POSITION: %d/%d\n", str_ctx.next_chunk, str_ctx.num_chunks); FntPrint(-1, "SMP RATE: %5d HZ\n\n", (sample_rate * 44100) >> 12); FntPrint(-1, "[START] %s\n", paused ? "RESUME" : "PAUSE"); @@ -411,7 +319,11 @@ int main(int argc, const char* argv[]) { PADTYPE *pad = (PADTYPE *) pad_buff[0]; if (pad->stat) continue; - if ((pad->type != 4) && (pad->type != 5) && (pad->type != 7)) + if ( + (pad->type != PAD_ID_DIGITAL) && + (pad->type != PAD_ID_ANALOG_STICK) && + (pad->type != PAD_ID_ANALOG) + ) continue; if ((last_buttons & PAD_START) && !(pad->btn & PAD_START)) { @@ -422,21 +334,19 @@ int main(int argc, const char* argv[]) { start_stream(); } - // Seeking by an arbitrary number of sectors isn't a problem as - // spu_irq_handler() always realigns the counter. if (!(pad->btn & PAD_LEFT)) - str_ctx.pos -= 16; + str_ctx.next_chunk--; if (!(pad->btn & PAD_RIGHT)) - str_ctx.pos += 16; + str_ctx.next_chunk++; if ((last_buttons & PAD_CIRCLE) && !(pad->btn & PAD_CIRCLE)) - str_ctx.pos = 0; + str_ctx.next_chunk = -1; if (!(pad->btn & PAD_DOWN) && (sample_rate > 0x400)) sample_rate -= 0x40; if (!(pad->btn & PAD_UP) && (sample_rate < 0x2000)) sample_rate += 0x40; if ((last_buttons & PAD_CROSS) && !(pad->btn & PAD_CROSS)) - sample_rate = SAMPLE_RATE; + sample_rate = getSPUSampleRate(str_ctx.sample_rate); // Only set the sample rate registers if necessary. if (pad->btn != 0xffff) { diff --git a/examples/sound/spustream/stream.bin b/examples/sound/spustream/stream.bin deleted file mode 100644 index e53b726..0000000 Binary files a/examples/sound/spustream/stream.bin and /dev/null differ diff --git a/examples/sound/spustream/stream.vag b/examples/sound/spustream/stream.vag new file mode 100644 index 0000000..e1cb4f4 Binary files /dev/null and b/examples/sound/spustream/stream.vag differ diff --git a/examples/sound/spustream/system.cnf b/examples/sound/spustream/system.cnf deleted file mode 100644 index 0c4561a..0000000 --- a/examples/sound/spustream/system.cnf +++ /dev/null @@ -1,4 +0,0 @@ -BOOT=cdrom:\spustrm.exe;1 -TCB=4 -EVENT=10 -STACK=801FFFF0 diff --git a/examples/sound/vagsample/3dfx.vag b/examples/sound/vagsample/3dfx.vag index 9284a9a..3a006bc 100644 Binary files a/examples/sound/vagsample/3dfx.vag and b/examples/sound/vagsample/3dfx.vag differ diff --git a/examples/sound/vagsample/main.c b/examples/sound/vagsample/main.c index c79e68e..6a60c19 100644 --- a/examples/sound/vagsample/main.c +++ b/examples/sound/vagsample/main.c @@ -1,280 +1,215 @@ -/* - * LibPSn00b Example Programs +/* + * PSn00bSDK SPU .VAG playback example + * (C) 2021-2022 Lameguy64, spicyjpeg - MPL licensed * - * VAG Playback Example - * 2019-2021 Meido-Tek Productions / PSn00bSDK Project + * This example demonstrates basic usage of the SPU. Two mono audio samples (in + * the standard PS1 .VAG format) are uploaded from main memory to SPU RAM and + * played on one of the 24 channels by manipulating the SPU's registers. The + * .VAG header is parsed to obtain the sample rate and data size, while the + * actual audio data does not need any processing as it is already encoded in + * the ADPCM format expected by the SPU. * - * This example program demonstrates the basic use of the SPU; uploading sound - * clips to SPU RAM and playing it back on one of 24 SPU voices (and possibly - * leave you with ears ringing from the cacophony). - * - * The PS1 SPU only supports playing back of specially encoded ADPCM samples - * natively and can play them at sample rates of up to 44.1KHz, so sound files - * will have to be converted to 'VAG' format before it can be used on the PS1. - * While it is possible to play plain PCM samples on the SPU, this requires - * some special trickery that involves abusing the echo buffer and is not - * supported by the (half-baked) SPU library of PSn00bSDK. - * - * Additionally, the SPU can only play ADPCM samples from its own local memory - * called the SPU RAM, so sound samples will have to be uploaded to SPU RAM - * before it can be played by the SPU. - * - * The included sound clips are by HighTreason610 (0proyt) and - * Lameguy64 (threedeeffeggzz) respectively. - * - * Example by Lameguy64 - * - * - * Changelog: - * - * October 6, 2021 - Initial version + * Note that PSn00bSDK does not yet provide any tool for SPU ADPCM encoding, so + * you will have to use an external program to convert your samples to .VAG. * + * The included sound clips are by HighTreason610 (proyt.vag) and Lameguy64 + * (3dfx.vag) respectively. */ - -#include + #include -#include -#include #include -#include #include +#include #include #include -extern const unsigned char proyt[]; -extern const int proyt_size; -extern const unsigned char tdfx[]; -extern const int tdfx_size; - -// Define display/draw environments for double buffering -DISPENV disp[2]; -DRAWENV draw[2]; -int db; - -unsigned char pad_buff[2][34]; - -// SPU addresses of the uploaded sound clips -int proyt_addr; -int tdfx_addr; - -// Init function -void init(void) -{ - int addr_temp; - - // This not only resets the GPU but it also installs the library's - // ISR subsystem to the kernel +extern const uint8_t proyt[]; +extern const uint8_t tdfx[]; + +/* Display/GPU context utilities */ + +#define SCREEN_XRES 320 +#define SCREEN_YRES 240 + +#define BGCOLOR_R 48 +#define BGCOLOR_G 24 +#define BGCOLOR_B 0 + +typedef struct { + DISPENV disp; + DRAWENV draw; +} Framebuffer; + +typedef struct { + Framebuffer db[2]; + int db_active; +} RenderContext; + +void init_context(RenderContext *ctx) { + Framebuffer *db; + ResetGraph(0); - - // Define display environments, first on top and second on bottom - SetDefDispEnv(&disp[0], 0, 0, 320, 240); - SetDefDispEnv(&disp[1], 0, 240, 320, 240); - - // Define drawing environments, first on bottom and second on top - SetDefDrawEnv(&draw[0], 0, 240, 320, 240); - SetDefDrawEnv(&draw[1], 0, 0, 320, 240); - - // Set and enable clear color - setRGB0(&draw[0], 0, 96, 0); - setRGB0(&draw[1], 0, 96, 0); - draw[0].isbg = 1; - draw[1].isbg = 1; - - // Clear double buffer counter - db = 0; - - // Apply the GPU environments - PutDispEnv(&disp[db]); - PutDrawEnv(&draw[db]); - - // Load test font + ctx->db_active = 0; + + db = &(ctx->db[0]); + SetDefDispEnv(&(db->disp), 0, 0, SCREEN_XRES, SCREEN_YRES); + SetDefDrawEnv(&(db->draw), SCREEN_XRES, 0, SCREEN_XRES, SCREEN_YRES); + setRGB0(&(db->draw), BGCOLOR_R, BGCOLOR_G, BGCOLOR_B); + db->draw.isbg = 1; + db->draw.dtd = 1; + + db = &(ctx->db[1]); + SetDefDispEnv(&(db->disp), SCREEN_XRES, 0, SCREEN_XRES, SCREEN_YRES); + SetDefDrawEnv(&(db->draw), 0, 0, SCREEN_XRES, SCREEN_YRES); + setRGB0(&(db->draw), BGCOLOR_R, BGCOLOR_G, BGCOLOR_B); + db->draw.isbg = 1; + db->draw.dtd = 1; + + PutDrawEnv(&(db->draw)); + //PutDispEnv(&(db->disp)); + + // Create a text stream at the top of the screen. FntLoad(960, 0); - - // Open up a test font text stream of 100 characters - FntOpen(0, 8, 320, 224, 0, 100); - - // Initialize the SPU - SpuInit(); - - // Set SPU transfer mode to DMA (only mode currently supported) + FntOpen(8, 16, 304, 208, 2, 512); +} + +void display(RenderContext *ctx) { + Framebuffer *db; + + DrawSync(0); + VSync(0); + ctx->db_active ^= 1; + + db = &(ctx->db[ctx->db_active]); + PutDrawEnv(&(db->draw)); + PutDispEnv(&(db->disp)); + SetDispMask(1); +} + +/* .VAG header structure */ + +typedef struct { + uint32_t magic; // 0x70474156 ("VAGp") for mono files + uint32_t version; + uint32_t interleave; // Unused in mono files + uint32_t size; // Big-endian, in bytes + uint32_t sample_rate; // Big-endian, in Hertz + uint32_t _reserved[3]; + char name[16]; +} VAG_Header; + +#define SWAP_ENDIAN(x) ( \ + (((uint32_t) (x) & 0x000000ff) << 24) | \ + (((uint32_t) (x) & 0x0000ff00) << 8) | \ + (((uint32_t) (x) & 0x00ff0000) >> 8) | \ + (((uint32_t) (x) & 0xff000000) >> 24) \ +) + +/* Helper functions */ + +// The first 4 KB of SPU RAM are reserved for capture buffers and psxspu +// additionally uploads a dummy sample (16 bytes) at 0x1000 by default, so the +// samples must be placed after those. +#define ALLOC_START_ADDR 0x1010 + +static int next_channel = 0; +static int next_sample_addr = ALLOC_START_ADDR; + +int upload_sample(const void *data, int size) { + // Round the size up to the nearest multiple of 64, as SPU DMA transfers + // are done in 64-byte blocks. + int _addr = next_sample_addr; + int _size = (size + 63) & 0xffffffc0; + SpuSetTransferMode(SPU_TRANSFER_BY_DMA); - - // Set SPU transfer address (start address for sample upload) - addr_temp = 0x1000; - SpuSetTransferStartAddr(addr_temp); - - // Upload first sound clip and wait for transfer to finish - SpuWrite((const uint32_t *) &proyt[48], proyt_size-48); - SpuIsTransferCompleted(SPU_TRANSFER_WAIT); - - // Obtain the address of the sound and advance address for the next one - // Samples are addressed in 8-byte units, so it'll have to be divided by 8 - proyt_addr = addr_temp/8; - addr_temp += proyt_size-48; - - printf("proyt.vag\t= %02x\n", proyt_addr); - - // Upload second sound clip - SpuSetTransferStartAddr(addr_temp); - SpuWrite((const uint32_t *) &tdfx[48], tdfx_size-48); + SpuSetTransferStartAddr(_addr); + + SpuWrite((const uint32_t *) data, _size); SpuIsTransferCompleted(SPU_TRANSFER_WAIT); - - // Obtain the address of the second sound clip - tdfx_addr = addr_temp/8; - addr_temp += tdfx_size-48; - - printf("3dfx.vag\t= %02x\n", tdfx_addr); - - // Begin pad polling - InitPAD( pad_buff[0], 34, pad_buff[1], 34 ); + + next_sample_addr = _addr + _size; + return _addr; +} + +void play_sample(int addr, int sample_rate) { + int ch = next_channel; + + // Make sure the channel is stopped. + SpuSetKey(0, 1 << ch); + + // Set the channel's sample rate and start address. Note that the SPU + // expects the sample rate to be in 4.12 fixed point format (with + // 1.0 = 44100 Hz) and the address in 8-byte units; psxspu.h provides the + // getSPUSampleRate() and getSPUAddr() macros to convert values to these + // units. + SPU_CH_FREQ(ch) = getSPUSampleRate(sample_rate); + SPU_CH_ADDR(ch) = getSPUAddr(addr); + + // Set the channel's volume and ADSR parameters (0x80ff and 0x1fee are + // dummy values that disable the ADSR envelope entirely). + SPU_CH_VOL_L(ch) = 0x3fff; + SPU_CH_VOL_R(ch) = 0x3fff; + SPU_CH_ADSR1(ch) = 0x80ff; + SPU_CH_ADSR2(ch) = 0x1fee; + + // Start the channel. + SpuSetKey(1, 1 << ch); + + next_channel = (ch + 1) % 24; +} + +/* Main */ + +static RenderContext ctx; + +int main(int argc, const char* argv[]) { + init_context(&ctx); + SpuInit(); + + // Upload the samples to the SPU and parse their headers. + VAG_Header *proyt_vag = (VAG_Header *) proyt; + VAG_Header *tdfx_vag = (VAG_Header *) tdfx; + + int proyt_addr = upload_sample(&proyt_vag[1], SWAP_ENDIAN(proyt_vag->size)); + int tdfx_addr = upload_sample(&tdfx_vag[1], SWAP_ENDIAN(tdfx_vag->size)); + int proyt_sr = SWAP_ENDIAN(proyt_vag->sample_rate); + int tdfx_sr = SWAP_ENDIAN(tdfx_vag->sample_rate); + + // Set up controller polling. + uint8_t pad_buff[2][34]; + InitPAD(pad_buff[0], 34, pad_buff[1], 34); StartPAD(); ChangeClearPAD(0); -} /* init */ - -// Display function -void display(void) -{ - // Flip buffer index - db = !db; - - // Wait for all drawing to complete - DrawSync(0); - - // Wait for vertical sync to cap the logic to 60fps (or 50 in PAL mode) - // and prevent screen tearing - VSync(0); - // Switch pages - PutDispEnv(&disp[db]); - PutDrawEnv(&draw[db]); - - // Enable display output, ResetGraph() disables it by default - SetDispMask(1); - -} /* main */ - -// Main function, program entrypoint -int main(int argc, const char *argv[]) -{ - int counter,nextchan; - int cross_pressed; - int circle_pressed; - PADTYPE *pad; - - // Init stuff - init(); - - // Main loop - counter = 0; - nextchan = 0; - cross_pressed = 0; - circle_pressed = 0; - - while(1) - { - pad = (PADTYPE*)&pad_buff[0][0]; - - if( pad->stat == 0 ) - { - // For digital pad, dual-analog and dual-shock - if( ( pad->type == 0x4 ) || ( pad->type == 0x5 ) || ( pad->type == 0x7 ) ) - { - // Plays the first sound - if( !(pad->btn&PAD_CROSS) ) - { - if( !cross_pressed ) - { - // Voice frequency - // (800h = 22.05KHz) - SPU_CH_FREQ(nextchan) = 0x800; - // Voice start playback address - // (transfer address / 8) - SPU_CH_ADDR(nextchan) = proyt_addr; - // Voice loop address - // (transfer address / 8) - SPU_CH_LOOP_ADDR(nextchan) = proyt_addr; - // Voice volume and envelope - SPU_CH_VOL_L(nextchan) = 0x3fff; - SPU_CH_VOL_R(nextchan) = 0x3fff; - SPU_CH_ADSR(nextchan) = 0x1fee80ff; - - // Set voice to key-off to allow restart - SPU_KEY_OFF = 1 << nextchan; - // Set voice to key-on - SPU_KEY_ON = 1 << nextchan; - - // Advance to next voice - nextchan++; - if( nextchan > 23 ) - nextchan = 0; - - cross_pressed = 1; - } - } - else - { - cross_pressed = 0; - } - - // Plays the second sound - if( !(pad->btn&PAD_CIRCLE) ) - { - if( !circle_pressed ) - { - // Voice frequency - // (1000h = 44.1KHz) - SPU_CH_FREQ(nextchan) = 0x1000; - // Voice start playback address - // (transfer address / 8) - SPU_CH_ADDR(nextchan) = tdfx_addr; - // Voice loop address - // (transfer address / 8) - SPU_CH_LOOP_ADDR(nextchan) = tdfx_addr; - // Voice volume and envelope - SPU_CH_VOL_L(nextchan) = 0x3fff; - SPU_CH_VOL_R(nextchan) = 0x3fff; - SPU_CH_ADSR(nextchan) = 0x1fee80ff; - - // Set voice to key-off to allow restart - SPU_KEY_OFF = 1 << nextchan; - // Set voice to key-on - SPU_KEY_ON = 1 << nextchan; - - // Advance to next voice - nextchan++; - if( nextchan > 23 ) - nextchan = 0; - - circle_pressed = 1; - } - } - else - { - circle_pressed = 0; - } - } - } - else - { - cross_pressed = 0; - circle_pressed = 0; - } - - // Print the obligatory hello world and counter to show that the - // program isn't locking up to the last created text stream - FntPrint(-1, "VAG SAMPLE - PRESS X OR O TO PLAY\n"); - FntPrint(-1, "COUNTER=%d\n", counter); - - // Draw the last created text stream + uint16_t last_buttons = 0xffff; + + while (1) { + FntPrint(-1, "SPU SAMPLE PLAYBACK DEMO\n\n"); + FntPrint(-1, "[X] PLAY FIRST SAMPLE\n"); + FntPrint(-1, "[O] PLAY SECOND SAMPLE\n"); + FntFlush(-1); - - // Update display - display(); - - // Increment the counter - counter++; + display(&ctx); + + // Check if a compatible controller is connected and handle button + // presses. + PADTYPE *pad = (PADTYPE *) pad_buff[0]; + if (pad->stat) + continue; + if ( + (pad->type != PAD_ID_DIGITAL) && + (pad->type != PAD_ID_ANALOG_STICK) && + (pad->type != PAD_ID_ANALOG) + ) + continue; + + if ((last_buttons & PAD_CROSS) && !(pad->btn & PAD_CROSS)) + play_sample(proyt_addr, proyt_sr); + if ((last_buttons & PAD_CIRCLE) && !(pad->btn & PAD_CIRCLE)) + play_sample(tdfx_addr, tdfx_sr); + + last_buttons = pad->btn; } - + return 0; - -} /* main */ +} diff --git a/examples/sound/vagsample/proyt.vag b/examples/sound/vagsample/proyt.vag index 663828d..b8d68d6 100644 Binary files a/examples/sound/vagsample/proyt.vag and b/examples/sound/vagsample/proyt.vag differ diff --git a/libpsn00b/include/hwregs_a.inc b/libpsn00b/include/hwregs_a.inc index c78b41a..ca38542 100644 --- a/libpsn00b/include/hwregs_a.inc +++ b/libpsn00b/include/hwregs_a.inc @@ -32,12 +32,18 @@ .set SPU_MASTER_VOL_R, 0x1d82 .set SPU_REVERB_VOL_L, 0x1d84 .set SPU_REVERB_VOL_R, 0x1d86 -.set SPU_KEY_ON, 0x1d88 -.set SPU_KEY_OFF, 0x1d8c -.set SPU_FM_MODE, 0x1d90 -.set SPU_NOISE_MODE, 0x1d94 -.set SPU_REVERB_ON, 0x1d98 -.set SPU_CHAN_STATUS, 0x1d9c +.set SPU_KEY_ON1, 0x1d88 +.set SPU_KEY_ON1, 0x1d8a +.set SPU_KEY_OFF1, 0x1d8c +.set SPU_KEY_OFF2, 0x1d8e +.set SPU_FM_MODE1, 0x1d90 +.set SPU_FM_MODE2, 0x1d92 +.set SPU_NOISE_MODE1, 0x1d94 +.set SPU_NOISE_MODE2, 0x1d96 +.set SPU_REVERB_ON1, 0x1d98 +.set SPU_REVERB_ON2, 0x1d9a +.set SPU_CHAN_STATUS1, 0x1d9c +.set SPU_CHAN_STATUS2, 0x1d9e .set SPU_REVERB_ADDR, 0x1da2 .set SPU_IRQ_ADDR, 0x1da4 @@ -59,8 +65,8 @@ .set SPU_VOICE_VOL_R, 0x02 .set SPU_VOICE_FREQ, 0x04 .set SPU_VOICE_ADDR, 0x06 -.set SPU_VOICE_ADSR_L, 0x08 -.set SPU_VOICE_ADSR_H, 0x0a +.set SPU_VOICE_ADSR1, 0x08 +.set SPU_VOICE_ADSR2, 0x0a .set SPU_VOICE_LOOP, 0x0e ## MDEC diff --git a/libpsn00b/include/hwregs_c.h b/libpsn00b/include/hwregs_c.h index b205b87..0e21922 100644 --- a/libpsn00b/include/hwregs_c.h +++ b/libpsn00b/include/hwregs_c.h @@ -38,12 +38,18 @@ #define SPU_MASTER_VOL_R _MMIO16(IOBASE | 0x1d82) #define SPU_REVERB_VOL_L _MMIO16(IOBASE | 0x1d84) #define SPU_REVERB_VOL_R _MMIO16(IOBASE | 0x1d86) -#define SPU_KEY_ON _MMIO32(IOBASE | 0x1d88) -#define SPU_KEY_OFF _MMIO32(IOBASE | 0x1d8c) -#define SPU_FM_MODE _MMIO32(IOBASE | 0x1d90) -#define SPU_NOISE_MODE _MMIO32(IOBASE | 0x1d94) -#define SPU_REVERB_ON _MMIO32(IOBASE | 0x1d98) -#define SPU_CHAN_STATUS _MMIO32(IOBASE | 0x1d9c) +#define SPU_KEY_ON1 _MMIO16(IOBASE | 0x1d88) +#define SPU_KEY_ON2 _MMIO16(IOBASE | 0x1d8a) +#define SPU_KEY_OFF1 _MMIO16(IOBASE | 0x1d8c) +#define SPU_KEY_OFF2 _MMIO16(IOBASE | 0x1d8e) +#define SPU_FM_MODE1 _MMIO16(IOBASE | 0x1d90) +#define SPU_FM_MODE2 _MMIO16(IOBASE | 0x1d92) +#define SPU_NOISE_MODE1 _MMIO16(IOBASE | 0x1d94) +#define SPU_NOISE_MODE2 _MMIO16(IOBASE | 0x1d96) +#define SPU_REVERB_ON1 _MMIO16(IOBASE | 0x1d98) +#define SPU_REVERB_ON2 _MMIO16(IOBASE | 0x1d9a) +#define SPU_CHAN_STATUS1 _MMIO16(IOBASE | 0x1d9c) +#define SPU_CHAN_STATUS2 _MMIO16(IOBASE | 0x1d9e) #define SPU_REVERB_ADDR _MMIO16(IOBASE | 0x1da2) #define SPU_IRQ_ADDR _MMIO16(IOBASE | 0x1da4) @@ -67,7 +73,8 @@ #define SPU_CH_VOL_R(N) _MMIO16(IOBASE | 0x1c02 + 16 * (N)) #define SPU_CH_FREQ(N) _MMIO16(IOBASE | 0x1c04 + 16 * (N)) #define SPU_CH_ADDR(N) _MMIO16(IOBASE | 0x1c06 + 16 * (N)) -#define SPU_CH_ADSR(N) _MMIO32(IOBASE | 0x1c08 + 16 * (N)) +#define SPU_CH_ADSR1(N) _MMIO16(IOBASE | 0x1c08 + 16 * (N)) +#define SPU_CH_ADSR2(N) _MMIO16(IOBASE | 0x1c0a + 16 * (N)) #define SPU_CH_LOOP_ADDR(N) _MMIO16(IOBASE | 0x1c0e + 16 * (N)) /* MDEC */ diff --git a/libpsn00b/include/psxspu.h b/libpsn00b/include/psxspu.h index 05737f7..cdc3ac7 100644 --- a/libpsn00b/include/psxspu.h +++ b/libpsn00b/include/psxspu.h @@ -111,11 +111,19 @@ typedef struct _SpuCommonAttr { (SPU_CH_FREQ(ch) = (pitch)) #define SpuSetVoiceStartAddr(ch, addr) \ (SPU_CH_ADDR(ch) = getSPUAddr(addr)) -#define SpuSetVoiceADSR(ch, ar, dr, sr, rr, sl) \ - (SPU_CH_ADSR(ch) = getSPUADSR(ar, dr, sr, rr, sl)) +#define SpuSetVoiceADSR(ch, ar, dr, sr, rr, sl) ( \ + SPU_CH_ADSR1(ch) = (sl) | ((dr) << 4) | ((ar) << 8), \ + SPU_CH_ADSR2(ch) = (rr) | ((sr) << 6) | (1 << 14) \ +) #define SpuSetKey(enable, voice_bit) \ - ((enable) ? (SPU_KEY_ON = (voice_bit)) : (SPU_KEY_OFF = (voice_bit))) + ((enable) ? ( \ + SPU_KEY_ON1 = (uint16_t) (voice_bit), \ + SPU_KEY_ON2 = (uint16_t) ((voice_bit) >> 16) \ + ) : ( \ + SPU_KEY_OFF1 = (uint16_t) (voice_bit), \ + SPU_KEY_OFF2 = (uint16_t) ((voice_bit) >> 16) \ + )) /* Public API */ diff --git a/libpsn00b/psxspu/common.c b/libpsn00b/psxspu/common.c index ca4b201..d1dabfe 100644 --- a/libpsn00b/psxspu/common.c +++ b/libpsn00b/psxspu/common.c @@ -105,10 +105,14 @@ void SpuInit(void) { SPU_MASTER_VOL_R = 0; SPU_REVERB_VOL_L = 0; SPU_REVERB_VOL_R = 0; - SPU_KEY_OFF = 0x00ffffff; - SPU_FM_MODE = 0; - SPU_NOISE_MODE = 0; - SPU_REVERB_ON = 0; + SPU_KEY_OFF1 = 0xffff; + SPU_KEY_OFF2 = 0x00ff; + SPU_FM_MODE1 = 0; + SPU_FM_MODE2 = 0; + SPU_NOISE_MODE1 = 0; + SPU_NOISE_MODE2 = 0; + SPU_REVERB_ON1 = 0; + SPU_REVERB_ON2 = 0; SPU_REVERB_ADDR = 0xfffe; SPU_CD_VOL_L = 0; SPU_CD_VOL_R = 0; @@ -140,7 +144,8 @@ void SpuInit(void) { // Sony's implementation leaves everything muted, however it makes sense to // turn up at least the master and CD audio volume by default. - SPU_KEY_ON = 0x00ffffff; + SPU_KEY_ON1 = 0xffff; + SPU_KEY_ON2 = 0x00ff; SPU_MASTER_VOL_L = 0x3fff; SPU_MASTER_VOL_R = 0x3fff; SPU_CD_VOL_L = 0x7fff; -- cgit v1.2.3 From 68daf6d338aba6e32e687d4151eaddc8735227b3 Mon Sep 17 00:00:00 2001 From: spicyjpeg Date: Sun, 30 Oct 2022 08:28:44 +0100 Subject: Refactor dynamic linker, misc. cleanups --- examples/sound/spustream/main.c | 4 +- examples/system/dynlink/main.c | 44 +++-- libpsn00b/include/dlfcn.h | 237 +++++++++++++----------- libpsn00b/include/stdlib.h | 12 +- libpsn00b/libc/memset.s | 6 +- libpsn00b/psxetc/dl.c | 399 +++++++++++++--------------------------- 6 files changed, 287 insertions(+), 415 deletions(-) (limited to 'examples') diff --git a/examples/sound/spustream/main.c b/examples/sound/spustream/main.c index 68cf9b0..d240433 100644 --- a/examples/sound/spustream/main.c +++ b/examples/sound/spustream/main.c @@ -19,8 +19,8 @@ * As the loop point doesn't necessarily have to be within the chunk itself, it * can be used to "queue" another chunk to be played immediately after the * current one. This allows for double buffering: two chunks are always kept in - * SPU RAM and one is overwritten with while the other is playing. Chunks are - * laid out in SPU RAM as follows: + * SPU RAM and one is overwritten with a new chunk while the other is playing. + * Chunks are laid out in SPU RAM as follows: * * ________________________________________________ * / __________________ \ diff --git a/examples/system/dynlink/main.c b/examples/system/dynlink/main.c index fcce5b1..d813c07 100644 --- a/examples/system/dynlink/main.c +++ b/examples/system/dynlink/main.c @@ -7,8 +7,8 @@ * symbol map file, which is generated at compile time by GCC's nm command and * included into the CD image. The symbol map lists all functions/variables in * the executable and their type, address and size. Currently only searching - * for a symbol's address by its name (DL_GetSymbolByName()) is supported, - * however this may be expanded in the future. + * for a symbol's address by its name (DL_GetMapSymbol()) is supported, however + * this may be expanded in the future. * * Being able to introspect local symbols at runtime, in turn, allows us to use * another set of APIs to load, link and execute code from an external file @@ -140,18 +140,18 @@ void display(RenderContext *ctx) { /* Symbol overriding example */ -static volatile uint32_t resolve_counter = 0; +static volatile int resolve_counter = 0; // This function will override printf(), i.e. DLLs will use this instead of the // "real" printf() present in the executable, thanks to the custom resolver -// defined below. We'll use this to redirect the DLL's output to the debug text -// window. +// defined below. We'll use this to redirect the DLL's output to be shown on +// screen. int dll_printf(const char *format, ...) { va_list args; va_start(args, format); - char buffer[256]; - int32_t return_value = vsprintf(buffer, format, args); + char buffer[256]; + int return_value = vsprintf(buffer, format, args); va_end(args); FntPrint(-1, "DLL: %s", buffer); @@ -163,7 +163,7 @@ int dll_printf(const char *format, ...) { // This function will be called by the linker for each undefined symbol // (function or variable) in the DLL, and should return the address of the // symbol so the dynamic linker can patch it in. The default resolver tries to -// find them in the currently loaded symbol map using DL_GetSymbolByName(). +// find them in the currently loaded symbol map using DL_GetMapSymbol(). void *custom_resolver(DLL *dll, const char *name) { if (!strcmp(name, "printf")) { printf("Resolving printf() -> dll_printf() (#%d)\n", resolve_counter++); @@ -173,7 +173,7 @@ void *custom_resolver(DLL *dll, const char *name) { printf("Resolving %s() (#%d)\n", name, resolve_counter++); // Custom resolvers should always fall back to the default behavior. - return DL_GetSymbolByName(name); + return DL_GetMapSymbol(name); } /* Global variables and structs */ @@ -187,7 +187,7 @@ typedef struct { void (*render)(RenderContext *, uint16_t buttons); } DLL_API; -static DLL *dll = 0; +static DLL dll; static DLL_API dll_api; static RenderContext ctx; @@ -225,20 +225,18 @@ size_t load_file(const char *filename, void **ptr) { } void load_dll(const char *filename) { - // As we're passing RTLD_FREE_ON_DESTROY to DL_CreateDLL(), calling + // As we're passing DL_FREE_ON_DESTROY to DL_CreateDLL(), calling // DL_DestroyDLL() will also deallocate the buffer the DLL was loaded into. - if (dll) - DL_DestroyDLL(dll); + DL_DestroyDLL(&dll); void *ptr; size_t len = load_file(filename, &ptr); - dll = DL_CreateDLL(ptr, len, RTLD_LAZY | RTLD_FREE_ON_DESTROY); - if (!dll) - SHOW_ERROR("FAILED TO PARSE %s\nERROR=%d\n", filename, (int32_t) DL_GetLastError()); + if (!DL_CreateDLL(&dll, ptr, len, DL_LAZY | DL_FREE_ON_DESTROY)) + SHOW_ERROR("FAILED TO PARSE %s\n", filename); - dll_api.init = DL_GetDLLSymbol(dll, "init"); - dll_api.render = DL_GetDLLSymbol(dll, "render"); + dll_api.init = DL_GetDLLSymbol(&dll, "init"); + dll_api.render = DL_GetDLLSymbol(&dll, "render"); printf("DLL init() @ %08x, render() @ %08x\n", dll_api.init, dll_api.render); @@ -266,14 +264,14 @@ int main(int argc, const char* argv[]) { size_t len = load_file("\\MAIN.MAP;1", &ptr); if (!DL_ParseSymbolMap(ptr, len)) - SHOW_ERROR("FAILED TO PARSE SYMBOL MAP\nERROR=%d\n", (int32_t) DL_GetLastError()); + SHOW_ERROR("FAILED TO PARSE SYMBOL MAP\n"); free(ptr); // Try to obtain a reference to a local function. - void (*_display)() = DL_GetSymbolByName("display"); + void (*_display)() = DL_GetMapSymbol("display"); if (!_display) - SHOW_ERROR("FAILED TO LOOK UP LOCAL FUNCTION\nERROR=%d\n", (int32_t) DL_GetLastError()); + SHOW_ERROR("FAILED TO LOOK UP LOCAL FUNCTION\n"); printf("Symbol map test, display() @ %08x\n", _display); @@ -295,7 +293,7 @@ int main(int argc, const char* argv[]) { DL_PRE_CALL(dll_api.render); dll_api.render(&ctx, last_buttons); - FntPrint(-1, "MAIN: DLL ADDR=%08x SIZE=%d\n", dll->ptr, dll->size); + FntPrint(-1, "MAIN: DLL ADDR=%08x SIZE=%d\n", dll.ptr, dll.size); FntPrint(-1, "MAIN: %d FUNCTIONS RESOLVED\n", resolve_counter); FntPrint(-1, "[START] LOAD NEXT DLL\n"); FntFlush(-1); @@ -320,7 +318,7 @@ int main(int argc, const char* argv[]) { last_buttons = pad->btn; } - //DL_DestroyDLL(dll); + //DL_DestroyDLL(&dll); //DL_UnloadSymbolMap(); return 0; } diff --git a/libpsn00b/include/dlfcn.h b/libpsn00b/include/dlfcn.h index 5848a95..3c5260d 100644 --- a/libpsn00b/include/dlfcn.h +++ b/libpsn00b/include/dlfcn.h @@ -7,38 +7,31 @@ #define __DLFCN_H #include +#include #include -/* Helper macro for setting $t9 before calling a function */ +/* Macros */ -#define DL_PRE_CALL(func) { \ - __asm__ volatile("move $t9, %0;" :: "r"(func) : "$t9"); \ -} +/** + * @brief Prepares for a DLL function call. + * + * @details Sets the $t9 register to the specified value (which should be a + * pointer to a DLL function obtained using DL_GetDLLSymbol()). This must be + * done prior to calling a DLL function from the main executable to ensure the + * DLL can correctly invoke the symbol resolver if necessary. + * + * This macro is not required when calling a DLL function from another DLL, as + * GCC will generate code to set $t9 appropriately. + */ +#define DL_PRE_CALL(func) \ + __asm__ volatile("move $t9, %0;" :: "r"(func) : "$t9"); -/* Types */ - -#define RTLD_DEFAULT ((DLL *) 0) - -typedef enum _DL_Error { - RTLD_E_NONE = 0, // No error - RTLD_E_FILE_OPEN = 1, // Unable to find or open file - RTLD_E_FILE_ALLOC = 2, // Unable to allocate buffer to load file into - RTLD_E_FILE_READ = 3, // Failed to read file - RTLD_E_NO_MAP = 4, // No symbol map has been loaded yet - RTLD_E_MAP_ALLOC = 5, // Unable to allocate symbol map structures - RTLD_E_NO_SYMBOLS = 6, // No symbols found in symbol map - RTLD_E_DLL_NULL = 7, // Unable to initialize DLL from null pointer - RTLD_E_DLL_ALLOC = 8, // Unable to allocate DLL metadata structures - RTLD_E_DLL_FORMAT = 9, // Unsupported DLL type or format - RTLD_E_MAP_SYMBOL = 10, // Symbol not found in symbol map - RTLD_E_DLL_SYMBOL = 11, // Symbol not found in DLL - RTLD_E_HASH_LOOKUP = 12 // Hash table lookup failed due to internal error -} DL_Error; +/* Structure and enum definitions */ typedef enum _DL_ResolveMode { - RTLD_LAZY = 1, // Resolve functions when they are first called (default) - RTLD_NOW = 2, // Resolve all symbols immediately on load - RTLD_FREE_ON_DESTROY = 4 // Automatically free DLL buffer when closing DLL + DL_LAZY = 1, // Resolve functions when they are first called (default) + DL_NOW = 2, // Resolve all symbols immediately on load + DL_FREE_ON_DESTROY = 4 // Automatically free DLL buffer when closing DLL } DL_ResolveMode; // Members of this struct should not be accessed directly in most cases, but @@ -55,151 +48,171 @@ typedef struct _DLL { uint16_t got_length; } DLL; -/* API */ +/* Public API */ #ifdef __cplusplus extern "C" { #endif /** - * @brief Reads the symbol table from the provided string buffer (which may or - * may not be null-terminated), parses it and stores the parsed entries into a - * private hash table; the buffer won't be further referenced and can be safely - * deallocated after parsing. Returns the number of entries successfully parsed - * or -1 if an error occurred. + * @brief Creates an empty symbol map in memory. * - * This function expects the string buffer to contain one more lines, each of - * which must follow this format: + * @details Initializes the internal symbol hash table to contain at most the + * given number of symbols. Once this function is called, symbols can be + * registered using DL_AddMapSymbol() and then looked up using + * DL_GetMapSymbol(). The default DLL resolver will search the hash table for + * external symbols required by DLLs. * - * [DEBUG_INFO...] + * This function is normally not required when loading a map file through + * DL_ParseSymbolMap(), but it can be used alongside DL_AddMapSymbol() to + * implement a custom symbol map parser. * - * The "nm" tool included in the GCC toolchain can be used to generate a map - * file in the appropriate format after building the executable, by using this - * command: + * @param num_entries + * @return 0 or -1 in case of error * - * mipsel-none-elf-nm -f posix -l -n executable.elf >executable.map + * @see DL_AddMapSymbol(), DL_GetMapSymbol() + */ +int DL_InitSymbolMap(int num_entries); + +/** + * @brief Destroys the currently loaded symbol map. * - * @param ptr - * @param size - * @return -1 or number of entries parsed + * @details Frees the internal hash table allocated by DL_InitSymbolMap() or + * DL_ParseSymbolMap(), containing the currently loaded symbol map. Freeing the + * table manually before loading a new symbol map is normally unnecessary as it + * is done automatically, however this function can be useful to recover heap + * space once the map is no longer needed. */ -int32_t DL_ParseSymbolMap(const char *ptr, size_t size); +void DL_UnloadSymbolMap(void); /** - * @brief File wrapper around DL_ParseSymbolMap(). Allocates a temporary buffer - * then loads the specified map file into it (using BIOS APIs) and calls - * DL_ParseSymbolMap() to parse it. The buffer is deallocated immediately after - * parsing. + * @brief Adds a symbol to the currently loaded symbol map. * - * @param filename Must always contain device name, e.g. "cdrom:MODULE.DLL;1" - * @return -1 or number of entries parsed + * @details Registers a new symbol (function or variable) with the given name + * and address, and adds it to the internal hash table. The symbol can then be + * looked up using DL_GetMapSymbol(). The default DLL resolver will search the + * hash table for external symbols required by DLLs. + * + * This function shall only be called after DL_InitSymbolMap() or + * DL_ParseSymbolMap() is called. + * + * @param name + * @param ptr + * + * @see DL_GetMapSymbol() */ -//int32_t DL_LoadSymbolMapFromFile(const char *filename); +void DL_AddMapSymbol(const char *name, void *ptr); /** - * @brief Frees internal buffers containing the currently loaded symbol map. - * This is automatically done before loading a new symbol map so there is no - * need to call this function in most cases, however it can still be useful to - * free up space on the heap once the symbol map is no longer needed. + * @brief Creates a symbol map in memory from a map file in text format. + * + * @details Initializes the internal symbol hash table, then parses entries + * from the provided string buffer (which may or may not be null-terminated) + * and adds each one to the table. The string buffer won't be further + * referenced and can be safely deallocated after parsing. Returns the number + * of entries successfully parsed. + * + * The string buffer shall contain one or more lines, each of which must follow + * this format: + * + * [...] + * + * The "nm" tool included in the GCC toolchain can be used to generate a map + * file in the appropriate format after building the executable: + * + * mipsel-none-elf-nm -f posix -l -n executable.elf >executable.map + * + * @param ptr + * @param size + * @return Number of entries parsed, -1 in case of failure + * + * @see DL_UnloadSymbolMap(), DL_GetMapSymbol() */ -void DL_UnloadSymbolMap(void); +int DL_ParseSymbolMap(const char *ptr, size_t size); /** - * @brief Queries the currently loaded symbol map for the symbol with the given - * name and returns a pointer to it, which can then be used to directly access - * the symbol. If the symbol can't be found, null is returned instead. + * @brief Gets a pointer to a symbol in the currently loaded map by its name. + * + * @details Queries the currently loaded symbol map for the symbol with the + * given name and returns a pointer to it, which can then be used to directly + * access the symbol. If the symbol can't be found, a null pointer is returned. * * @param name - * @return NULL or pointer to symbol (any type) + * @return NULL or pointer to symbol */ -void *DL_GetSymbolByName(const char *name); +void *DL_GetMapSymbol(const char *name); /** - * @brief Sets a custom function to be called for resolving symbols in DLLs. + * @brief Sets a custom handler for resolving symbols in DLLs. + * + * @details Sets a custom function to be called for resolving symbols in DLLs. * The function will be given a pointer to the current DLL and the unresolved * symbol's name, and should return the address of the symbol in the executable * (the dynamic linker will lock up if it returns null). Passing null instead - * of a function resets the default behavior of calling DL_GetSymbolByName() to + * of a function resets the default behavior of calling DL_GetMapSymbol() to * find the symbol in the currently loaded symbol map. - * + * * @param callback NULL or pointer to callback function + * @return Previously set callback or NULL */ -void DL_SetResolveCallback(void *(*callback)(DLL *, const char *)); +void *DL_SetResolveCallback(void *(*callback)(DLL *, const char *)); /** - * @brief Initializes a buffer holding the contents of a dynamically-loaded + * @brief Initializes a DLL structure. + * + * @details Initializes a buffer holding the contents of a dynamically-loaded * library file (compiled with the dll.ld linker script and converted to a raw - * binary) *in-place*. A new DLL struct is allocated to store metadata but, + * binary) *in-place*. Metadata is written to the provided DLL struct but, * unlike DL_ParseSymbolMap(), the DLL's actual code, data and tables are * referenced directly from the provided buffer. The buffer must not be moved * or deallocated, at least not before calling DL_DestroyDLL() on the DLL * struct returned by this function. * * The third argument specifies when symbols in the DLL should be resolved. - * Setting it to RTLD_LAZY defers resolution of undefined functions to when - * they are first called, while RTLD_NOW forces all symbols to be resolved - * immediately. If a custom resolver has been set via DL_SetResolveCallback(), - * it will be called for each symbol to resolve. + * Setting it to DL_LAZY defers resolution of undefined functions to when they + * are first called, while DL_NOW forces all symbols to be resolved + * immediately. Either mode can be OR'd with DL_FREE_ON_DESTROY to + * automatically deallocate the provided buffer when DL_DestroyDLL() is called. * + * If a custom resolver has been set via DL_SetResolveCallback(), it will be + * called for each symbol to resolve. + * + * @param dll * @param ptr * @param size - * @param mode RTLD_LAZY or RTLD_NOW - * @return NULL or pointer to a new DLL struct - */ -DLL *DL_CreateDLL(void *ptr, size_t size, DL_ResolveMode mode); - -/** - * @brief File wrapper around dlinit(). Allocates a new buffer, loads the - * specified file into it (using BIOS APIs) and calls dlinit() on that. When - * calling dlclose() on a DLL loaded from a file, the file buffer is - * automatically destroyed. - * - * @param filename Must always contain device name, e.g. "cdrom:MODULE.DLL;1" - * @param mode RTLD_LAZY or RTLD_NOW + optionally RTLD_FREE_ON_DESTROY - * @return NULL or pointer to a new DLL struct + * @param mode DL_LAZY or DL_NOW, optionally with DL_FREE_ON_DESTROY + * @return Pointer to DLL structure or NULL in case of failure + * + * @see DL_DestroyDLL(), DL_GetDLLSymbol() */ -//DLL *DL_LoadDLLFromFile(const char *filename, DL_ResolveMode mode); +DLL *DL_CreateDLL(DLL *dll, void *ptr, size_t size, DL_ResolveMode mode); /** - * @brief Destroys a loaded DLL by calling its global destructors and freeing - * the buffer it's loaded in. Any pointer passed to DL_DestroyDLL() should no - * longer be used after the call. If the DLL was initialized in-place using - * DL_CreateDLL(), DL_DestroyDLL() will only free the buffer initially passed - * to DL_CreateDLL() if RTLD_FREE_ON_DESTROY was used. + * @brief Destroys a DLL structure. + * + * @details Destroys a loaded DLL by calling its global destructors. If the DLL + * was initialized with the DL_FREE_ON_DESTROY flag, the buffer associated with + * the DLL is also deallocated. Note that the DLL structure itself is *not* + * deallocated. * * @param dll */ void DL_DestroyDLL(DLL *dll); /** - * @brief Returns a pointer to the DLL symbol with the given name, or null if - * it can't be found. If null or RTLD_DEFAULT is passed as first argument, the - * executable itself is searched instead using the symbol map (behaving the - * same as DL_GetSymbolByName()). + * @brief Gets a pointer to a symbol in a DLL by its name. + * + * @details Returns a pointer to the DLL symbol with the given name, or null if + * it can't be found. If a null pointer is passed as first argument, the + * executable itself is searched instead using the symbol map (behaving + * identically to DL_GetMapSymbol()). * - * @param dll DLL struct or RTLD_DEFAULT + * @param dll Pointer to DLL structure or NULL * @param name * @return NULL or pointer to symbol (any type) */ void *DL_GetDLLSymbol(const DLL *dll, const char *name); -/** - * @brief Returns a code describing the last error that occurred, or DL_E_NONE - * if no error has occurred since the last call to dlerror() (i.e. calling this - * also resets the internal error flags). - * - * @return NULL or member of DL_Error enum - */ -DL_Error DL_GetLastError(void); - -/* POSIX "compatibility" macros */ - -#define dlinit(ptr, size, mode) DL_CreateDLL(ptr, size, mode) -//#define dlopen(filename, mode) DL_LoadDLLFromFile(filename, mode) -#define dlsym(dll, name) DL_GetDLLSymbol(dll, name) -#define dlclose(dll) DL_DestroyDLL(dll) -#define dlerror() DL_GetLastError() - #ifdef __cplusplus } #endif diff --git a/libpsn00b/include/stdlib.h b/libpsn00b/include/stdlib.h index f0753c1..049d067 100644 --- a/libpsn00b/include/stdlib.h +++ b/libpsn00b/include/stdlib.h @@ -31,17 +31,19 @@ extern "C" { extern int __argc; extern const char **__argv; +void abort(void); + int rand(void); -void srand(unsigned long seed); +void srand(int seed); int abs(int j); long labs(long i); -long long strtoll(const char *nptr, char **endptr, int base); -long strtol(const char *nptr, char **endptr, int base); -long double strtold(const char *nptr, char **endptr); -double strtod(const char *nptr, char **endptr); +long strtol(const char *nptr, char **endptr, int base); +long long strtoll(const char *nptr, char **endptr, int base); float strtof(const char *nptr, char **endptr); +double strtod(const char *nptr, char **endptr); +long double strtold(const char *nptr, char **endptr); void InitHeap(void *addr, size_t size); void *sbrk(ptrdiff_t incr); diff --git a/libpsn00b/libc/memset.s b/libpsn00b/libc/memset.s index 73f92bd..6ef84ec 100644 --- a/libpsn00b/libc/memset.s +++ b/libpsn00b/libc/memset.s @@ -9,7 +9,6 @@ memset: # If more than 16 bytes have to be written then take the "large" path, # otherwise use the code below. - blez $a2, .Lnull_count addiu $t0, $a2, -16 bgtz $t0, .Llarge_fill move $v0, $a0 # return_value = dest @@ -39,12 +38,9 @@ memset: sb $a1, 0xc($a0) sb $a1, 0xd($a0) sb $a1, 0xe($a0) - jr $ra sb $a1, 0xf($a0) - -.Lnull_count: jr $ra - move $v0, $a0 # return_value = dest + nop .Llarge_fill: # Initialize fast filling by repeating the fill byte 4 times, so it can be diff --git a/libpsn00b/psxetc/dl.c b/libpsn00b/psxetc/dl.c index fa5e74d..ccf7a7c 100644 --- a/libpsn00b/psxetc/dl.c +++ b/libpsn00b/psxetc/dl.c @@ -1,6 +1,6 @@ /* * PSn00bSDK dynamic linker - * (C) 2021 spicyjpeg - MPL licensed + * (C) 2021-2022 spicyjpeg - MPL licensed * * The bulk of this code is MIPS-specific but not PS1-specific, so the whole * dynamic linker could be ported to other MIPS platforms that do not have one @@ -27,8 +27,10 @@ #define SDK_LIBRARY_NAME "psxetc/dl" #include +#include #include #include +#include #include #include #include @@ -36,13 +38,6 @@ #include #include -/* Compile options */ - -// Comment before building to disable functions that rely on BIOS file APIs, -// i.e. DL_LoadSymbolMapFromFile() and DL_LoadDLLFromFile(). -// FIXME: those seem to be broken currently, and shouldn't be used anyway -//#define USE_FILE_API - /* Private types */ typedef struct { @@ -51,17 +46,15 @@ typedef struct { } MapEntry; typedef struct { - uint32_t nbucket; - uint32_t nchain; + int nbucket, nchain, index; MapEntry *entries; uint32_t *bucket; uint32_t *chain; } SymbolMap; -/* Data */ +/* Internal globals */ -static DL_Error _error_code = RTLD_E_NONE; static SymbolMap _symbol_map; // Accessed by _dl_resolve_helper, stores the pointer to the current resolver @@ -70,11 +63,6 @@ void *(*_dl_resolve_callback)(DLL *, const char *) = 0; /* Private utilities */ -#define _ERROR(code, ret) { \ - _error_code = code; \ - return ret; \ -} - void _dl_resolve_wrapper(void); // Called by _dl_resolve_wrapper() (which is in turn called by GCC stubs) to @@ -82,29 +70,28 @@ void _dl_resolve_wrapper(void); void *_dl_resolve_helper(DLL *dll, uint32_t index) { Elf32_Sym *sym = &(dll->symtab[index]); const char *_name = &(dll->strtab[sym->st_name]); - void *address; + void *addr; if (_dl_resolve_callback) - address = _dl_resolve_callback(dll, _name); + addr = _dl_resolve_callback(dll, _name); else - address = DL_GetSymbolByName(_name); + addr = DL_GetMapSymbol(_name); - if (!address) { - _sdk_log("FATAL! can't resolve %s, locking up\n", _name); - while (1) - __asm__ volatile("nop"); + if (!addr) { + _sdk_log("FATAL! can't resolve %s, aborting\n", _name); + abort(); } // Patch the GOT entry to "cache" the resolved address. This can probably // be implemented in a faster way, but this thing is already too complex. - for (uint32_t i = 0; i < dll->got_length; i++) { + for (int i = 0; i < dll->got_length; i++) { if (dll->got[2 + i] == (uint32_t) sym->st_value) { - dll->got[2 + i] = (uint32_t) address; + dll->got[2 + i] = (uint32_t) addr; break; } } - return address; + return addr; } // Implementation of the weird obscure hashing function used in the ELF .hash @@ -127,120 +114,109 @@ static uint32_t _elf_hash(const char *str) { return value; } -#ifdef USE_FILE_API -static uint8_t *_dl_load_file(const char *filename, size_t *size_output) { - int32_t fd = open(filename, 1); - if (fd < 0) { - _sdk_log("can't open %s, error = %d\n", filename, fd); - _ERROR(RTLD_E_FILE_OPEN, 0); - } +/* Symbol map loading/introspection API */ - // Extract file size from the file's associated control block. - // https://problemkaputt.de/psx-spx.htm#biosmemorymap - FCB *fcb = (FCB *) *((FCB **) 0x80000140); - size_t size = fcb[fd].filesize; +int DL_InitSymbolMap(int num_entries) { + if (_symbol_map.entries) + DL_UnloadSymbolMap(); - uint8_t *buffer = malloc(size); - if (!buffer) { - _sdk_log("unable to allocate %d bytes for %s\n", size, filename); - _ERROR(RTLD_E_FILE_ALLOC, 0); - } + // TODO: find a way to calculate the optimal number of hash table "buckets" + // in order to minimize hash table size + _symbol_map.nbucket = num_entries; + _symbol_map.nchain = num_entries; + _symbol_map.index = 0; + _sdk_log( + "allocating nbucket = %d, nchain = %d\n", + _symbol_map.nbucket, num_entries + ); - //_sdk_log("loading %s (%d bytes)..", filename, size); + _symbol_map.entries = malloc(sizeof(MapEntry) * num_entries); + _symbol_map.bucket = malloc(sizeof(uint32_t) * num_entries); + _symbol_map.chain = malloc(sizeof(uint32_t) * num_entries); - for (uint32_t offset = 0; offset < size; ) { - int32_t length = read(fd, &(buffer[offset]), 0x800); + if (!_symbol_map.entries || !_symbol_map.bucket || !_symbol_map.chain) { + _sdk_log("unable to allocate symbol map table\n"); + return -1; + } - if (length <= 0) { - close(fd); - free(buffer); + memset(_symbol_map.bucket, 0xff, sizeof(uint32_t) * num_entries); + memset(_symbol_map.chain, 0xff, sizeof(uint32_t) * num_entries); - _sdk_log("failed, error = %d\n", length); - _ERROR(RTLD_E_FILE_READ, 0); - } + return 0; +} - //_sdk_log("."); - offset += length; - } +void DL_UnloadSymbolMap(void) { + if (!_symbol_map.entries) + return; - close(fd); - _sdk_log(" done\n"); + free(_symbol_map.entries); + free(_symbol_map.bucket); + free(_symbol_map.chain); - if (size_output) - *size_output = size; - return buffer; + _symbol_map.entries = 0; + _symbol_map.bucket = 0; + _symbol_map.chain = 0; } -#endif -/* Symbol map loading/parsing API */ +void DL_AddMapSymbol(const char *name, void *ptr) { + uint32_t hash = _elf_hash(name); + int index = _symbol_map.index; + _symbol_map.index = index + 1; -int32_t DL_ParseSymbolMap(const char *ptr, size_t size) { - DL_UnloadSymbolMap(); + MapEntry *entry = &(_symbol_map.entries[index]); + entry->hash = hash; + entry->ptr = ptr; + + // Append a reference to the entry to the hash table's chain. + uint32_t *hash_entry = &(_symbol_map.bucket[hash % _symbol_map.nbucket]); + while (*hash_entry != 0xffffffff) + hash_entry = &(_symbol_map.chain[*hash_entry]); + + *hash_entry = index; +} + +int DL_ParseSymbolMap(const char *ptr, size_t size) { + int entries = 0; // Perform a quick scan over the entire map text and count the number of // newlines. This allows us to (over)estimate the number of entries and - // allocate a sufficiently large hash/entry table. - uint32_t entries = 0; - for (uint32_t pos = 0; pos < size; pos++) { + // allocate a sufficiently large hash table. + for (int pos = 0; pos < size; pos++) { if (ptr[pos] == '\n') entries++; } - // TODO: find a way to calculate the optimal number of hash table "buckets" - // in order to minimize hash table size - _symbol_map.nbucket = entries; - _symbol_map.nchain = entries; - _sdk_log( - "allocating nbucket = %d, nchain = %d\n", - _symbol_map.nbucket, - entries - ); - - // Allocate an entry table to store parsed symbols in, and an associated - // hash table (same format as .hash section, with 8-byte header). - _symbol_map.entries = malloc(sizeof(MapEntry) * entries); - _symbol_map.bucket = malloc(sizeof(uint32_t) * _symbol_map.nbucket); - _symbol_map.chain = malloc(sizeof(uint32_t) * entries); + int err = DL_InitSymbolMap(entries); + if (err) + return err; - if (!_symbol_map.entries || !_symbol_map.bucket || !_symbol_map.chain) { - _sdk_log("unable to allocate symbol map table\n"); - _ERROR(RTLD_E_MAP_ALLOC, -1); - } + // Go again through the symbol map and fill in the hash table by calling + // DL_AddMapSymbol() for each valid entry. + entries = 0; - for (uint32_t i = 0; i < _symbol_map.nbucket; i++) - _symbol_map.bucket[i] = 0xffffffff; - for (uint32_t i = 0; i < entries; i++) - _symbol_map.chain[i] = 0xffffffff; - - // Go again through the symbol map and fill in the hash table. - uint32_t index = 0; - for (uint32_t pos = 0; (pos < size) && ptr[pos]; pos++) { - char name[64]; - char type_string[2]; - uint64_t address64; + for (int pos = 0; (pos < size) && ptr[pos]; pos++) { + uint64_t full_addr; + char name[64], type_string[4]; size_t _size; // e.g. "main T ffffffff80000000 100 ...\n" - int32_t parsed = sscanf( + int parsed = sscanf( &(ptr[pos]), "%63s %1s %Lx %x", name, type_string, - &address64, + &full_addr, &_size // Optional, unused (yet) ); if (parsed >= 3) { // Drop the upper 32 bits of the address (for some reason MIPS nm - // insists on printing 64-bit addresses... wtf) and normalize the - // type letter to upper case, then check if the entry is valid and - // non-null. - void *address = (void *) ((uint32_t) address64); - char _type = toupper(type_string[0]); - uint32_t hash = _elf_hash(name); - uint32_t hash_mod = hash % _symbol_map.nbucket; - - if (address && ( + // insists on printing 64-bit addresses... wtf) and check if the + // entry is valid and non-null. + void *addr = (void *) ((uint32_t) full_addr); + char _type = toupper(type_string[0]); + + if (addr && ( (_type == 'T') || // .text (_type == 'R') || // .rodata (_type == 'D') || // .data @@ -248,21 +224,11 @@ int32_t DL_ParseSymbolMap(const char *ptr, size_t size) { )) { //_sdk_log( //"map sym: %08x,%08x [%c %s]\n", - //address, _size, _type, name + //addr, _size, _type, name //); - MapEntry *entry = &(_symbol_map.entries[index]); - entry->hash = hash; - entry->ptr = address; - - // Append a reference to the entry to the hash table's chain - // for the current hash_mod. I can't explain this properly. - uint32_t *hash_entry = &(_symbol_map.bucket[hash_mod]); - while (*hash_entry != 0xffffffff) - hash_entry = &(_symbol_map.chain[*hash_entry]); - - *hash_entry = index; - index++; + DL_AddMapSymbol(name, addr); + entries++; } } @@ -273,55 +239,27 @@ int32_t DL_ParseSymbolMap(const char *ptr, size_t size) { } _sdk_log("parsed %d symbols\n", entries); - if (!entries) - _ERROR(RTLD_E_NO_SYMBOLS, -1); - return entries; } -#ifdef USE_FILE_API -int32_t DL_LoadSymbolMapFromFile(const char *filename) { - size_t size; - char *ptr = _dl_load_file(filename, &size); - if (!ptr) - return -1; - - int32_t entries = DL_ParseSymbolMap(ptr, size); - free(ptr); - - return entries; -} -#endif - -void DL_UnloadSymbolMap(void) { - if (!_symbol_map.entries) - return; - - free(_symbol_map.entries); - free(_symbol_map.bucket); - free(_symbol_map.chain); - _symbol_map.entries = 0; -} - -void *DL_GetSymbolByName(const char *name) { +void *DL_GetMapSymbol(const char *name) { if (!_symbol_map.entries) { - _sdk_log("attempted lookup with no map loaded\n"); - _ERROR(RTLD_E_NO_MAP, 0); + _sdk_log("DL_GetMapSymbol() with no map loaded\n"); + return 0; } - // https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter6-48031.html - uint32_t hash = _elf_hash(name); - uint32_t hash_mod = hash % _symbol_map.nbucket; - // Go through the hash table's chain until the symbol hash matches the one // calculated. - for (uint32_t i = _symbol_map.bucket[hash_mod]; i != 0xffffffff;) { + // https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter6-48031.html + uint32_t hash = _elf_hash(name); + + for (int i = _symbol_map.bucket[hash % _symbol_map.nbucket]; i != 0xffffffff;) { if (i >= _symbol_map.nchain) { _sdk_log( - "GetSymbolByName() index out of bounds (%d >= %d)\n", + "DL_GetMapSymbol() index out of bounds (%d >= %d)\n", i, _symbol_map.nchain ); - _ERROR(RTLD_E_HASH_LOOKUP, 0); + return 0; } MapEntry *entry = &(_symbol_map.entries[i]); @@ -335,27 +273,24 @@ void *DL_GetSymbolByName(const char *name) { } _sdk_log("map lookup [%s not found]\n", name); - _ERROR(RTLD_E_MAP_SYMBOL, 0); + return 0; } -void DL_SetResolveCallback(void *(*callback)(DLL *, const char *)) { +void *DL_SetResolveCallback(void *(*callback)(DLL *, const char *)) { + void *old_callback = _dl_resolve_callback; _dl_resolve_callback = callback; + + return old_callback; } /* Library loading and linking API */ -DLL *DL_CreateDLL(void *ptr, size_t size, DL_ResolveMode mode) { - if (!ptr) - _ERROR(RTLD_E_DLL_NULL, 0); - - DLL *dll = malloc(sizeof(DLL)); - if (!dll) { - _sdk_log("unable to allocate DLL struct\n"); - _ERROR(RTLD_E_DLL_ALLOC, 0); - } +DLL *DL_CreateDLL(DLL *dll, void *ptr, size_t size, DL_ResolveMode mode) { + if (!dll || !ptr) + return 0; dll->ptr = ptr; - dll->malloc_ptr = (mode & RTLD_FREE_ON_DESTROY) ? ptr : 0; + dll->malloc_ptr = (mode & DL_FREE_ON_DESTROY) ? ptr : 0; dll->size = size; _sdk_log("initializing DLL at %08x\n", ptr); @@ -371,47 +306,30 @@ DLL *DL_CreateDLL(void *ptr, size_t size, DL_ResolveMode mode) { switch (dyn->d_tag) { // Offset of .got section case DT_PLTGOT: - //_sdk_log("[PLTGOT]\n"); - dll->got = (void *) (ptr + dyn->d_un.d_val); break; // Offset of .hash section case DT_HASH: - //_sdk_log("[HASH]\n"); - dll->hash = (void *) (ptr + dyn->d_un.d_val); break; // Offset of .dynstr (NOT .strtab) section case DT_STRTAB: - //_sdk_log("[STRTAB]\n"); - dll->strtab = (void *) (ptr + dyn->d_un.d_val); break; // Offset of .dynsym (NOT .symtab) section case DT_SYMTAB: - //_sdk_log("[SYMTAB]\n"); - dll->symtab = (void *) (ptr + dyn->d_un.d_val); break; - // Length of .dynstr section - //case DT_STRSZ: - //_sdk_log("[STRSZ]\n"); - //break; - // Length of each .dynsym entry case DT_SYMENT: - //_sdk_log("[SYMENT]\n"); - // Only 16-byte symbol table entries are supported. if (dyn->d_un.d_val != sizeof(Elf32_Sym)) { - free(dll); - _sdk_log("invalid DLL symtab entry size %d\n", dyn->d_un.d_val); - _ERROR(RTLD_E_DLL_FORMAT, 0); + return 0; } break; @@ -421,73 +339,44 @@ DLL *DL_CreateDLL(void *ptr, size_t size, DL_ResolveMode mode) { // Versions other than 1 are unsupported (do they even exist?). if (dyn->d_un.d_val != 1) { - free(dll); - _sdk_log("invalid DLL version %d\n", dyn->d_un.d_val); - _ERROR(RTLD_E_DLL_FORMAT, 0); + return 0; } break; // DLL/ABI flags case DT_MIPS_FLAGS: - //_sdk_log("[MIPS_FLAGS]\n"); - // Shortcut pointers (whatever they are) are not supported. if (dyn->d_un.d_val & RHF_QUICKSTART) { - free(dll); - _sdk_log("invalid DLL flags\n"); - _ERROR(RTLD_E_DLL_FORMAT, 0); + return 0; } break; // Number of local (not to resolve) GOT entries case DT_MIPS_LOCAL_GOTNO: - //_sdk_log("[MIPS_LOCAL_GOTNO]\n"); - local_got_len = dyn->d_un.d_val; break; // Base address DLL was compiled for case DT_MIPS_BASE_ADDRESS: - //_sdk_log("[MIPS_BASE_ADDRESS]\n"); - // Base addresses other than zero are not supported. It would // be easy enough to support them, but why? if (dyn->d_un.d_val) { - free(dll); - _sdk_log("invalid DLL base address %08x\n", dyn->d_un.d_val); - _ERROR(RTLD_E_DLL_FORMAT, 0); + return 0; } break; // Number of symbol table entries case DT_MIPS_SYMTABNO: - //_sdk_log("[MIPS_SYMTABNO]\n"); - dll->symbol_count = dyn->d_un.d_val; break; - // Index of first unresolved symbol table entry - //case DT_MIPS_UNREFEXTNO: - //_sdk_log("[MIPS_UNREFEXTNO]\n"); - //break; - // Index of first symbol table entry which has a matching GOT entry case DT_MIPS_GOTSYM: - //_sdk_log("[MIPS_GOTSYM]\n"); - first_got_sym = dyn->d_un.d_val; break; - - // Number of pages the GOT is split into (does not apply to PS1) - //case DT_MIPS_HIPAGENO: - //_sdk_log("[MIPS_HIPAGENO]\n"); - //break; - - //default: - //_sdk_log("[ignored]\n"); } } @@ -513,14 +402,14 @@ DLL *DL_CreateDLL(void *ptr, size_t size, DL_ResolveMode mode) { dll->got[0] = (uint32_t) &_dl_resolve_wrapper; dll->got[1] = (uint32_t) dll; - for (uint32_t i = 0; i < dll->got_length; i++) + for (int i = 0; i < dll->got_length; i++) dll->got[2 + i] += (uint32_t) ptr; // Fix addresses in the symbol table. // TODO: clean this shit up uint32_t got_offset = first_got_sym; - for (uint32_t i = 0; i < dll->symbol_count; i++) { + for (int i = 0; i < dll->symbol_count; i++) { Elf32_Sym *sym = &(dll->symtab[i]); const char *_name = &(dll->strtab[sym->st_name]); @@ -533,12 +422,12 @@ DLL *DL_CreateDLL(void *ptr, size_t size, DL_ResolveMode mode) { //sym->st_value, sym->st_size, _name //); - // If RTLD_NOW was passed, resolve GOT entries ahead of time by + // If DL_NOW was passed, resolve GOT entries ahead of time by // cross-referencing them with the symbol table. - if (!(mode & RTLD_NOW)) + if (!(mode & DL_NOW)) continue; - for (uint32_t j = got_offset; j < dll->got_length; j++) { + for (int j = got_offset; j < dll->got_length; j++) { if (dll->got[2 + j] != (uint32_t) sym->st_value) continue; @@ -553,10 +442,8 @@ DLL *DL_CreateDLL(void *ptr, size_t size, DL_ResolveMode mode) { )) { dll->got[2 + j] = (uint32_t) _dl_resolve_callback(dll, _name); - if (!dll->got[2 + j]) { - free(dll); - _ERROR(RTLD_E_MAP_SYMBOL, 0); - } + if (!dll->got[2 + j]) + return 0; } break; @@ -573,7 +460,7 @@ DLL *DL_CreateDLL(void *ptr, size_t size, DL_ResolveMode mode) { // DLL itself. const uint32_t *ctor_list = DL_GetDLLSymbol(dll, "__CTOR_LIST__"); if (ctor_list) { - for (uint32_t i = ((uint32_t) ctor_list[0]); i >= 1; i--) { + for (int i = ((int) ctor_list[0]); i >= 1; i--) { void (*ctor)(void) = (void (*)(void)) ctor_list[i]; DL_PRE_CALL(ctor); ctor(); @@ -583,64 +470,47 @@ DLL *DL_CreateDLL(void *ptr, size_t size, DL_ResolveMode mode) { return dll; } -#ifdef USE_FILE_API -DLL *DL_LoadDLLFromFile(const char *filename, DL_ResolveMode mode) { - size_t size; - char *ptr = _dl_load_file(filename, &size); - if (!ptr) - return 0; - - DLL *dll = DL_CreateDLL(ptr, size, mode | RTLD_FREE_ON_DESTROY); - if (!dll) - free(ptr); - - return dll; -} -#endif - void DL_DestroyDLL(DLL *dll) { - if (dll == RTLD_DEFAULT) + if (!dll) return; if (dll->ptr) { // Call the DLL's global destructors. const uint32_t *dtor_list = DL_GetDLLSymbol(dll, "__DTOR_LIST__"); if (dtor_list) { - for (uint32_t i = 0; i < ((uint32_t) dtor_list[0]); i++) { + for (int i = 0; i < ((int) dtor_list[0]); i++) { void (*dtor)(void) = (void (*)(void)) dtor_list[i + 1]; DL_PRE_CALL(dtor); dtor(); } } + + dll->ptr = 0; } - // If the DLL is associated to a buffer allocated by DL_LoadDLLFromFile(), - // free that buffer. - if (dll->malloc_ptr) + // If the DLL is associated to a buffer, free that buffer. + if (dll->malloc_ptr) { free(dll->malloc_ptr); - - free(dll); + dll->malloc_ptr = 0; + } } void *DL_GetDLLSymbol(const DLL *dll, const char *name) { - if (dll == RTLD_DEFAULT) - return DL_GetSymbolByName(name); - //return _dl_resolve_callback(RTLD_DEFAULT, name); + if (!dll) + return DL_GetMapSymbol(name); + //return _dl_resolve_callback(0, name); - // https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter6-48031.html uint32_t nbucket = dll->hash[0]; uint32_t nchain = dll->hash[1]; const uint32_t *bucket = &(dll->hash[2]); const uint32_t *chain = &(dll->hash[2 + nbucket]); - uint32_t hash_mod = _elf_hash(name) % nbucket; - // Go through the hash table's chain until the symbol name matches the one // provided. - for (uint32_t i = bucket[hash_mod]; i != 0xffffffff;) { + for (int i = bucket[_elf_hash(name) % nbucket]; i != 0xffffffff;) { if (i >= nchain) { _sdk_log("DL_GetDLLSymbol() index out of bounds (%d >= %d)\n", i, nchain); - _ERROR(RTLD_E_HASH_LOOKUP, 0); + return 0; } Elf32_Sym *sym = &(dll->symtab[i]); @@ -655,12 +525,5 @@ void *DL_GetDLLSymbol(const DLL *dll, const char *name) { } _sdk_log("DLL lookup [%s not found]\n", name); - _ERROR(RTLD_E_DLL_SYMBOL, 0); -} - -DL_Error DL_GetLastError(void) { - DL_Error last = _error_code; - _error_code = RTLD_E_NONE; - - return last; + return 0; } -- cgit v1.2.3