diff options
| author | John "Lameguy" Wilbert Villamor <lameguy64@gmail.com> | 2022-01-18 08:31:14 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-01-18 08:31:14 +0800 |
| commit | 05d44488bd5587786f4bd0286fc0f555c79aa46a (patch) | |
| tree | 5740f396d10a9580c3a39ca536544436898ff1b6 /examples | |
| parent | 08de895e8582dbc70b639ae5f511ab9ebfb4d68a (diff) | |
| parent | e9475e283a82665fe6c19bebc3318b5084f15a2e (diff) | |
| download | psn00bsdk-05d44488bd5587786f4bd0286fc0f555c79aa46a.tar.gz | |
Merge pull request #44 from spicyjpeg/actions
GitHub Actions CI, psxcd and libc fixes, new examples
Diffstat (limited to 'examples')
40 files changed, 1358 insertions, 343 deletions
diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..dfea465 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,82 @@ + +# PSn00bSDK Example Programs + +## Examples summary + +The following list is a brief summary of all the example programs included. +Additional information may be found in the source code of each example. + +| Path | Description | Type | Notes | +| :--------------------------------------------- | :---------------------------------------------------- | :--: | :---: | +| [`beginner/cppdemo`](./beginner/cppdemo) | Simple demonstration of (dynamic) C++ classes | EXE | | +| [`beginner/hello`](./beginner/hello) | The obligatory "Hello World" example program | EXE | | +| [`cdrom/cdbrowse`](./cdrom/cdbrowse) | File browser using libpsxcd's directory functions | CD | | +| [`cdrom/cdxa`](./cdrom/cdxa) | CD-XA ADPCM audio player | CD | 1 | +| [`demos/n00bdemo`](./demos/n00bdemo) | The premiere demonstration program of PSn00bSDK | EXE | | +| [`graphics/balls`](./graphics/balls) | Draws colored balls bouncing around the screen | EXE | | +| [`graphics/billboard`](./graphics/billboard) | Demonstrates how to draw 2D sprites in a 3D space | EXE | | +| [`graphics/fpscam`](./graphics/fpscam) | First-person perspective camera with look-at | EXE | | +| [`graphics/gte`](./graphics/gte) | Displays a rotating cube using GTE macros | EXE | | +| [`graphics/hdtv`](./graphics/hdtv) | Demonstrates anamorphic widescreen at 704x480 | EXE | | +| [`graphics/render2tex`](./graphics/render2tex) | Procedural texture effects using off-screen drawing | EXE | | +| [`graphics/rgb24`](./graphics/rgb24) | Displays a 640x480 24-bit RGB image | EXE | | +| [`graphics/tilesasm`](./graphics/tilesasm) | Drawing a tile-map with assembly language | EXE | | +| [`io/pads`](./io/pads) | Demonstrates reading controllers via low-level access | EXE | | +| [`io/system573`](./io/system573) | Konami System 573 (PS1-based arcade board) example | CD | | +| [`lowlevel/cartrom`](./lowlevel/cartrom) | ROM firmware for cheat devices written using GNU GAS | ROM | 2 | +| [`sound/spustream`](./sound/spustream) | Custom (non XA) CD-ROM audio streaming using the SPU | CD | 1 | +| [`sound/vagsample`](./sound/vagsample) | Demonstrates playing VAG sound files with 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 | | +| [`system/timer`](./system/timer) | Demonstrates using hardware timers with interrupts | EXE | | +| [`system/tty`](./system/tty) | Using TTY as a remote text console interface | EXE | | + +Notes: + +1. `cdrom/cdxa` and `sound/spustream` do not come with example audio files. In + order to run these examples you'll have to provide your own files (and, in + the case of `spustream`, convert them using the included Python script) and + build the CD image manually. +2. The `lowlevel/cartrom` example is outdated and does not use SDK libraries. + It is kept for reference purposes only. + +## Building the examples + +The instructions below assume that PSn00bSDK, CMake 3.20+ and a GCC toolchain +are already installed. Refer to the [installation guide](../doc/installation.md) +for details. + +**NOTE**: all examples are compiled by default when building the PSn00bSDK +libraries and tools (check the `build/examples` directory). These instructions +are for rebuilding the examples *after* the SDK has been installed. + +1. Copy the contents of this directory (`share/psn00bsdk/examples` within the + PSn00bSDK installation directory) to your home directory or to another + folder you have write access to. + +2. Configure and build the examples by running: + + ```bash + cmake -S . -B ./build -G "Ninja" -DCMAKE_TOOLCHAIN_FILE=<INSTALL_PATH>/lib/libpsn00b/cmake/sdk.cmake + cmake --build ./build + ``` + + Replace `<INSTALL_PATH>` with the installation prefix you chose when + installing the SDK (usually `C:\Program Files\PSn00bSDK` or `/usr/local`, + so the full path to `sdk.cmake` would be + `C:\Program Files\PSn00bSDK\lib\libpsn00b\cmake\sdk.cmake` or + `/usr/local/lib/libpsn00b/cmake/sdk.cmake` respectively). + + Add `-DPSN00BSDK_TARGET=mipsel-unknown-elf` to the first command if your + toolchain targets `mipsel-unknown-elf` rather than `mipsel-none-elf`. If you + can't get Ninja to work or don't have it installed, you can also replace + `-G "Ninja"` with `-G "Unix Makefiles"` (`-G "MSYS Makefiles"` on Windows) + to build using `make` instead. + + This should create a `build` directory whose structure mirrors the one of + the parent directory, with each subfolder containing built executables and + CD images for each example. + +----------------------------------------- +_Last updated on 2022-01-17 by spicyjpeg_ diff --git a/examples/beginner/cppdemo/CMakeLists.txt b/examples/beginner/cppdemo/CMakeLists.txt index becf464..c43d4a1 100644 --- a/examples/beginner/cppdemo/CMakeLists.txt +++ b/examples/beginner/cppdemo/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( cppdemo LANGUAGES CXX diff --git a/examples/beginner/hello/CMakeLists.txt b/examples/beginner/hello/CMakeLists.txt index 7fb7c22..d8297c5 100644 --- a/examples/beginner/hello/CMakeLists.txt +++ b/examples/beginner/hello/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( hello LANGUAGES C diff --git a/examples/cdrom/cdbrowse/CMakeLists.txt b/examples/cdrom/cdbrowse/CMakeLists.txt index e5ec759..e36407d 100644 --- a/examples/cdrom/cdbrowse/CMakeLists.txt +++ b/examples/cdrom/cdbrowse/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( cdbrowse LANGUAGES C diff --git a/examples/cdrom/cdxa/CMakeLists.txt b/examples/cdrom/cdxa/CMakeLists.txt index 18dcc69..7b90f59 100644 --- a/examples/cdrom/cdxa/CMakeLists.txt +++ b/examples/cdrom/cdxa/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( cdxa LANGUAGES C diff --git a/examples/cdrom/cdxa/main.c b/examples/cdrom/cdxa/main.c index 5f11d8d..284b92f 100644 --- a/examples/cdrom/cdxa/main.c +++ b/examples/cdrom/cdxa/main.c @@ -199,7 +199,7 @@ void xa_callback(int intr, unsigned char *result) if (intr == CdlDataReady) { /* Fetch data sector */ - CdGetSector((u_long*)&xa_sector_buff, 2048); + CdGetSector((u_long*)&xa_sector_buff, 512); /* Quirk: This CdGetSector() implementation must fetch 2048 bytes */ /* or more otherwise the following sectors will be read in an */ diff --git a/examples/demos/n00bdemo/CMakeLists.txt b/examples/demos/n00bdemo/CMakeLists.txt index c62c4ef..1c211b3 100644 --- a/examples/demos/n00bdemo/CMakeLists.txt +++ b/examples/demos/n00bdemo/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( n00bdemo LANGUAGES C ASM diff --git a/examples/graphics/balls/CMakeLists.txt b/examples/graphics/balls/CMakeLists.txt index 5886484..f5297c3 100644 --- a/examples/graphics/balls/CMakeLists.txt +++ b/examples/graphics/balls/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( balls LANGUAGES C diff --git a/examples/graphics/billboard/CMakeLists.txt b/examples/graphics/billboard/CMakeLists.txt index 8cd31a9..1b417d2 100644 --- a/examples/graphics/billboard/CMakeLists.txt +++ b/examples/graphics/billboard/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( billboard LANGUAGES C ASM diff --git a/examples/graphics/fpscam/CMakeLists.txt b/examples/graphics/fpscam/CMakeLists.txt index 791f6c2..cb0c086 100644 --- a/examples/graphics/fpscam/CMakeLists.txt +++ b/examples/graphics/fpscam/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( fpscam LANGUAGES C diff --git a/examples/graphics/gte/CMakeLists.txt b/examples/graphics/gte/CMakeLists.txt index 85b2942..f95c5ff 100644 --- a/examples/graphics/gte/CMakeLists.txt +++ b/examples/graphics/gte/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( gte LANGUAGES C diff --git a/examples/graphics/hdtv/CMakeLists.txt b/examples/graphics/hdtv/CMakeLists.txt index f92faeb..804b096 100644 --- a/examples/graphics/hdtv/CMakeLists.txt +++ b/examples/graphics/hdtv/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( hdtv LANGUAGES C diff --git a/examples/graphics/render2tex/CMakeLists.txt b/examples/graphics/render2tex/CMakeLists.txt index 360840d..70e489e 100644 --- a/examples/graphics/render2tex/CMakeLists.txt +++ b/examples/graphics/render2tex/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( render2tex LANGUAGES C ASM diff --git a/examples/graphics/rgb24/CMakeLists.txt b/examples/graphics/rgb24/CMakeLists.txt index bf8a8fa..449981a 100644 --- a/examples/graphics/rgb24/CMakeLists.txt +++ b/examples/graphics/rgb24/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( rgb24 LANGUAGES C ASM diff --git a/examples/graphics/tilesasm/CMakeLists.txt b/examples/graphics/tilesasm/CMakeLists.txt index 59ef665..3384875 100644 --- a/examples/graphics/tilesasm/CMakeLists.txt +++ b/examples/graphics/tilesasm/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( tilesasm LANGUAGES C ASM @@ -15,8 +11,14 @@ project( HOMEPAGE_URL "http://lameguy64.net/?page=psn00bsdk" ) +configure_file(data.s.template data.s) + file(GLOB _sources *.s *.c) -psn00bsdk_add_executable(tilesasm STATIC ${_sources}) +psn00bsdk_add_executable( + tilesasm STATIC + ${_sources} + ${PROJECT_BINARY_DIR}/data.s +) #psn00bsdk_add_cd_image(tilesasm_iso tilesasm iso.xml DEPENDS tilesasm) install(FILES ${PROJECT_BINARY_DIR}/tilesasm.exe TYPE BIN) diff --git a/examples/graphics/tilesasm/data.s b/examples/graphics/tilesasm/data.s.template index c64ebbc..1c4b01e 100644 --- a/examples/graphics/tilesasm/data.s +++ b/examples/graphics/tilesasm/data.s.template @@ -1,35 +1,34 @@ -#
-# LibPSn00b Example Programs
-#
-# Drawing Tile-maps with Assembler Routines
-# 2022 Meido-Tek Productions / PSn00bSDK Project
-#
-# Example by John "Lameguy" Wilbert Villamor (Lameguy64)
-#
-# This assembler file is used to include the file tiles.tim as an array named
-# 'tim_tileset' for use in this example program. Note how the variable name
-# itself is leading with an underscore (_) in this file. This is because
-# GNU C requires leading underscores for global variables, perhaps to prevent
-# function names and variable names from mixing up during the linking stage.
-
-# Tell assembler that the contents that follow must be in the .data section
-.section .data
-
-# This directive define the 'tim_tileset' label as a global symbol so that
-# main.c and other program modules can see this symbol during linking
-.global tim_tileset
-
-# This directive is not really required, but its best to define symbols
-# not pointing to program code as an object to help identify it as a
-# variable in debuggers
-.type tim_tileset, @object
-
-# The following line defines the variable 'tim_tileset' itself filled with the
-# contents of the file 'tiles.tim' by using the .incbin directive
-#
-# Remember the variable type of a symbol is always governed by how it is
-# declared in the C code
-#
-tim_tileset:
- .incbin "../tiles_256.tim"
-
\ No newline at end of file +# +# LibPSn00b Example Programs +# +# Drawing Tile-maps with Assembler Routines +# 2022 Meido-Tek Productions / PSn00bSDK Project +# +# Example by John "Lameguy" Wilbert Villamor (Lameguy64) +# +# This assembler file is used to include the file tiles.tim as an array named +# 'tim_tileset' for use in this example program. Note how the variable name +# itself is leading with an underscore (_) in this file. This is because +# GNU C requires leading underscores for global variables, perhaps to prevent +# function names and variable names from mixing up during the linking stage. + +# Tell assembler that the contents that follow must be in the .data section +.section .data + +# This directive define the 'tim_tileset' label as a global symbol so that +# main.c and other program modules can see this symbol during linking +.global tim_tileset + +# This directive is not really required, but its best to define symbols +# not pointing to program code as an object to help identify it as a +# variable in debuggers +.type tim_tileset, @object + +# The following line defines the variable 'tim_tileset' itself filled with the +# contents of the file 'tiles.tim' by using the .incbin directive +# +# Remember the variable type of a symbol is always governed by how it is +# declared in the C code +# +tim_tileset: + .incbin "${PROJECT_SOURCE_DIR}/tiles_256.tim" diff --git a/examples/io/pads/CMakeLists.txt b/examples/io/pads/CMakeLists.txt index 5bd7f5d..cf5f817 100644 --- a/examples/io/pads/CMakeLists.txt +++ b/examples/io/pads/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( pads LANGUAGES C ASM diff --git a/examples/io/pads/main.c b/examples/io/pads/main.c index 92beb1c..d100482 100644 --- a/examples/io/pads/main.c +++ b/examples/io/pads/main.c @@ -118,16 +118,16 @@ static volatile uint8_t pad_buff[2][34]; static volatile size_t pad_buff_len[2]; static volatile uint32_t pad_digital_only[2] = { 0, 0 }; -// Just a wrapper around spi_new_request(). This does not send the command +// Just a wrapper around SPI_CreateRequest(). This does not send the command // immediately but adds it to the driver's request queue. void send_pad_cmd( - uint32_t port, - PAD_COMMAND cmd, - uint8_t arg1, - uint8_t arg2, - SPICALLBACK callback + uint32_t port, + PadCommand cmd, + uint8_t arg1, + uint8_t arg2, + SPI_Callback callback ) { - SPIREQUEST *req = spi_new_request(); + SPI_Request *req = SPI_CreateRequest(); req->len = 9; req->port = port; @@ -150,12 +150,12 @@ void send_pad_cmd( // actually a DualShock in digital mode by checking if it started identifying // as CONFIG_MODE after receiving a configuration command. void dualshock_init_cb(uint32_t port, const volatile uint8_t *buff, size_t rx_len) { - PADTYPE *pad = (PADTYPE *) buff; + PadResponse *pad = (PadResponse *) buff; if ( (rx_len < 2) || - (pad->raw.prefix != 0x5a) || - (pad->raw.type != PAD_ID_CONFIG_MODE) + (pad->prefix != 0x5a) || + (pad->type != PAD_ID_CONFIG_MODE) ) { printf("no, pad is digital-only (len = %d)\n", rx_len); @@ -187,7 +187,7 @@ void poll_cb(uint32_t port, const volatile uint8_t *buff, size_t rx_len) { if (rx_len) memcpy((void *) pad_buff[port], (void *) buff, rx_len); - PADTYPE *pad = (PADTYPE *) buff; + PadResponse *pad = (PadResponse *) buff; // If this pad identifies as a digital pad and hasn't been flagged as a // digital-only pad already, attempt to put it into analog mode by entering @@ -196,8 +196,8 @@ void poll_cb(uint32_t port, const volatile uint8_t *buff, size_t rx_len) { // returning digital pad responses. if ( rx_len && - (pad->raw.prefix == 0x5a) && - (pad->raw.type == PAD_ID_DIGITAL) + (pad->prefix == 0x5a) && + (pad->type == PAD_ID_DIGITAL) ) { if (!pad_digital_only[port]) { printf("Detecting if pad %d supports config mode... ", port + 1); @@ -221,7 +221,7 @@ static CONTEXT ctx; int main(int argc, const char* argv[]) { init_context(&ctx); - spi_init(&poll_cb); + SPI_Init(&poll_cb); uint32_t counter = 0; @@ -238,15 +238,14 @@ int main(int argc, const char* argv[]) { continue; } - PADTYPE *pad = (PADTYPE *) pad_buff[port]; - PAD_TYPEID type = pad->raw.type; + PadResponse *pad = (PadResponse *) pad_buff[port]; // According to nocash docs, there is a hardware bug in DualShock // controllers that causes the prefix byte (normally 0x5a) to turn // into 0x00 if the analog button is pressed after configuration // commands have been used. Thus making sure the prefix is 0x5a // isn't enough to reliably detect pads. - /*if ((pad->raw.prefix != 0x5a) && (type != PAD_ID_ANALOG)) { + /*if ((pad->prefix != 0x5a) && (pad->type != PAD_ID_ANALOG)) { FntPrint(-1, "\n\nPORT %d: INVALID RESPONSE\n", port + 1); if ((counter % 64) < 32) FntPrint(-1, " CHECK CONNECTION..."); @@ -258,8 +257,8 @@ int main(int argc, const char* argv[]) { -1, "\n\nPORT %d: %s (TYPE=%d)\n", port + 1, - PAD_TYPEIDS[type], - type + PAD_TYPEIDS[pad->type], + pad->type ); // Print a hexdump of the payload returned by the pad. diff --git a/examples/io/pads/spi.c b/examples/io/pads/spi.c index e01b3f6..ef75ffc 100644 --- a/examples/io/pads/spi.c +++ b/examples/io/pads/spi.c @@ -54,21 +54,21 @@ /* Internal structures and globals */ -typedef struct _SPICONTEXT { - uint8_t tx_buff[SPI_BUFF_LEN]; - uint8_t rx_buff[SPI_BUFF_LEN]; - uint32_t tx_len, rx_len, port; - SPICALLBACK callback; -} SPICONTEXT; +typedef struct _SPI_CONTEXT { + uint8_t tx_buff[SPI_BUFF_LEN]; + uint8_t rx_buff[SPI_BUFF_LEN]; + uint32_t tx_len, rx_len, port; + SPI_Callback callback; +} SPI_Context; -static volatile SPICONTEXT ctx; -static volatile SPIREQUEST volatile *current_req; -static SPICALLBACK default_cb; +static volatile SPI_Context ctx; +static volatile SPI_Request volatile *current_req; +static SPI_Callback default_cb; /* Request queue management */ -static void prepare_poll_req(void) { - PADREQUEST *req = (PADREQUEST *) ctx.tx_buff; +static void _spi_create_poll_req(void) { + PadRequest *req = (PadRequest *) ctx.tx_buff; req->addr = 0x01; req->cmd = PAD_CMD_READ; @@ -82,7 +82,7 @@ static void prepare_poll_req(void) { ctx.callback = default_cb; } -static void prepare_next_req(void) { +static void _spi_next_req(void) { // Copy the contents of the first request in the queue into the TX buffer. memcpy((void *) ctx.tx_buff, (void *) current_req->data, current_req->len); @@ -93,7 +93,7 @@ static void prepare_next_req(void) { // Pop the first request from the queue by deallocating it and adjusting // the pointer to the first queue item. - SPIREQUEST *next = current_req->next; + SPI_Request *next = current_req->next; free((void *) current_req); current_req = next; @@ -101,7 +101,7 @@ static void prepare_next_req(void) { /* Interrupt handlers */ -static void poll_timer_tick(void) { +static void _spi_poll_irq_handler(void) { // Fetch the last response byte, which wasn't followed by a pulse on /ACK, // from the RX FIFO. if (JOY_STAT & 0x0002) @@ -112,9 +112,9 @@ static void poll_timer_tick(void) { // If the request queue is empty, create a pad polling request. if (current_req) - prepare_next_req(); + _spi_next_req(); else - prepare_poll_req(); + _spi_create_poll_req(); // Prepare the SPI port by clearing any pending IRQ, pulling /CS high and // enabling the /ACK IRQ. In order to communicate with controllers, /CS has @@ -132,7 +132,7 @@ static void poll_timer_tick(void) { JOY_TXRX = ctx.tx_buff[0]; } -static void spi_ack_handler(void) { +static void _spi_ack_irq_handler(void) { // Wait until /ACK is pulled up by the controller before sending the next // byte. According to nocash docs, this has to be done before resetting the // IRQ. @@ -166,8 +166,8 @@ static void spi_ack_handler(void) { /* Public API */ -SPIREQUEST *spi_new_request(void) { - SPIREQUEST *req = malloc(sizeof(SPIREQUEST)); +SPI_Request *SPI_CreateRequest(void) { + SPI_Request *req = malloc(sizeof(SPI_Request)); req->len = 0; req->port = 0; @@ -179,7 +179,7 @@ SPIREQUEST *spi_new_request(void) { if (!current_req) { current_req = req; } else { - volatile SPIREQUEST *volatile last = current_req; + volatile SPI_Request *volatile last = current_req; while (last->next) last = last->next; @@ -189,7 +189,7 @@ SPIREQUEST *spi_new_request(void) { return req; } -void spi_set_poll_rate(uint32_t value) { +void SPI_SetPollRate(uint32_t value) { TIM_CTRL(2) = 0x0258; // CLK/8 input, IRQ on reload, disable one-shot IRQ if (value < 65) @@ -198,21 +198,21 @@ void spi_set_poll_rate(uint32_t value) { TIM_RELOAD(2) = (F_CPU / 8) / value; } -void spi_init(SPICALLBACK callback) { +void SPI_Init(SPI_Callback callback) { // Disable the BIOS timer handler (which for some stupid reason is enabled // by default, even though it does nothing) and set up custom interrupt // handlers. EnterCriticalSection(); ChangeClearRCnt(2, 0); - InterruptCallback(6, &poll_timer_tick); - InterruptCallback(7, &spi_ack_handler); + InterruptCallback(6, &_spi_poll_irq_handler); + InterruptCallback(7, &_spi_ack_irq_handler); ExitCriticalSection(); JOY_CTRL = 0x0040; // Reset all registers JOY_MODE = 0x000d; // 1x multiplier, 8 data bits, no parity JOY_BAUD = 0x0088; // 250000 bps - spi_set_poll_rate(250); + SPI_SetPollRate(250); current_req = 0; default_cb = callback; } diff --git a/examples/io/pads/spi.h b/examples/io/pads/spi.h index 1c473cd..c50e065 100644 --- a/examples/io/pads/spi.h +++ b/examples/io/pads/spi.h @@ -9,23 +9,24 @@ #include <stdint.h> #include <psxpad.h> +// Maximum request/response length (34 bytes for pads, 140 for memory cards) //#define SPI_BUFF_LEN 34 #define SPI_BUFF_LEN 140 /* Request structures */ -typedef void (*SPICALLBACK)(uint32_t port, const volatile uint8_t *buff, size_t rx_len); +typedef void (*SPI_Callback)(uint32_t port, const volatile uint8_t *buff, size_t rx_len); -typedef struct _SPIREQUEST { +typedef struct _SPI_Request { union { - uint8_t data[SPI_BUFF_LEN]; - PADREQUEST pad_req; - MCDREQUEST mcd_req; + uint8_t data[SPI_BUFF_LEN]; + PadRequest pad_req; + MemCardRequest mcd_req; }; - uint32_t len, port; - SPICALLBACK callback; - struct _SPIREQUEST *next; -} SPIREQUEST; + uint32_t len, port; + SPI_Callback callback; + struct _SPI_Request *next; +} SPI_Request; /* Public API */ @@ -34,7 +35,7 @@ typedef struct _SPIREQUEST { * object must be populated afterwards by setting the length, callback and * filling in the TX data buffer. */ -SPIREQUEST *spi_new_request(void); +SPI_Request *SPI_CreateRequest(void); /** * @brief Changes the controller polling rate. The lowest supported rate is 65 @@ -43,7 +44,7 @@ SPIREQUEST *spi_new_request(void); * * @param value */ -void spi_set_poll_rate(uint32_t value); +void SPI_SetPollRate(uint32_t value); /** * @brief Installs the SPI and timer 2 interrupt handlers and starts the poll @@ -56,6 +57,6 @@ void spi_set_poll_rate(uint32_t value); * * @param callback */ -void spi_init(SPICALLBACK callback); +void SPI_Init(SPI_Callback callback); #endif diff --git a/examples/io/system573/CMakeLists.txt b/examples/io/system573/CMakeLists.txt new file mode 100644 index 0000000..1c74347 --- /dev/null +++ b/examples/io/system573/CMakeLists.txt @@ -0,0 +1,23 @@ +# PSn00bSDK example CMake script +# (C) 2021 spicyjpeg - MPL licensed + +cmake_minimum_required(VERSION 3.21) + +project( + system573 + LANGUAGES C ASM + VERSION 1.0.0 + DESCRIPTION "PSn00bSDK Konami System 573 example" + HOMEPAGE_URL "http://lameguy64.net/?page=psn00bsdk" +) + +file(GLOB _sources *.c *.s) +psn00bsdk_add_executable(system573 STATIC ${_sources}) +psn00bsdk_add_cd_image(system573_iso system573 iso.xml DEPENDS system573) + +install( + FILES + ${PROJECT_BINARY_DIR}/system573.bin + ${PROJECT_BINARY_DIR}/system573.cue + TYPE BIN +) diff --git a/examples/io/system573/iso.xml b/examples/io/system573/iso.xml new file mode 100644 index 0000000..09b4d85 --- /dev/null +++ b/examples/io/system573/iso.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<iso_project + image_name="${CD_IMAGE_NAME}.bin" + cue_sheet="${CD_IMAGE_NAME}.cue" +> + <track type="data"> + <identifiers + system ="PLAYSTATION" + volume ="SYSTEM573" + volume_set ="SYSTEM573" + publisher ="MEIDOTEK" + data_preparer ="PSN00BSDK ${PSN00BSDK_VERSION}" + application ="PLAYSTATION" + copyright ="README.TXT;1" + /> + + <directory_tree> + <!-- + The System 573 BIOS does not parse SYSTEM.CNF, it always looks + for an executable named PSX.EXE. Note that this behavior can be + abused to make multi-system CDs with different executables for + PS1 and 573 (i.e. have both PSX.EXE and SYSTEM.CNF pointing to + a different executable). + --> + <!--<file name="SYSTEM.CNF" type="data" source="${PROJECT_SOURCE_DIR}/system.cnf" />--> + <file name="PSX.EXE" type="data" source="system573.exe" /> + <file name="PSX.MAP" type="data" source="system573.map" /> + + <dummy sectors="1024"/> + </directory_tree> + </track> + + <!--<track type="audio" source="track2.wav" />--> +</iso_project> diff --git a/examples/io/system573/main.c b/examples/io/system573/main.c new file mode 100644 index 0000000..a06c4e5 --- /dev/null +++ b/examples/io/system573/main.c @@ -0,0 +1,371 @@ +/* + * PSn00bSDK Konami System 573 example + * (C) 2021 spicyjpeg - MPL licensed + * + * This is a minimal example demonstrating how to target the Konami System 573 + * using PSn00bSDK. The System 573 is a PS1-based arcade motherboard that + * powered various Konami arcade games throughout the late 1990s, most notably + * Dance Dance Revolution and other Bemani rhythm games. It came in several + * configurations, with slightly different I/O connectors depending on the game + * and two optional add-on modules (known as the "analog I/O" and "digital I/O" + * boards respectively) providing light control outputs and, in the case of the + * digital I/O board, MP3 audio playback. + * + * Unlike other arcade systems based on PS1 hardware, the 573 is mostly + * identical to a regular PS1, with almost all custom extensions mapped into + * the expansion port region at 0x1f000000. The major differences are: + * + * - RAM is 4 MB instead of 2, and VRAM is 2 MB instead of 1. It is recommended + * *not* to use the additional memory to preserve PS1 compatibility. + * + * - The CD drive is replaced by a standard IDE/ATAPI drive (which most of the + * time is going to be an aftermarket DVD drive, as the original drives the + * system shipped with were prone to failure and couldn't read CD-Rs). This + * also means the 573 has no support at all for XA audio playback, as XA is + * not part of the CD-ROM specification implemented by IDE drives. CD audio + * is supported by most IDE drives, but 573 units with the digital I/O board + * installed have the 4-pin audio cable plugged into that instead of the + * drive. The IDE bus is connected to IRQ10 and DMA5 (expansion port) instead + * of IRQ2 and DMA3, which go unused. + * + * - The BIOS seems to have most file I/O APIs removed and exposes no functions + * whatsoever for accessing the IDE drive or the filesystem on the disc. The + * launcher/shell is completely different from Sony's shell and is capable of + * loading an executable from the CD drive, a PCMCIA memory-mapped flash card + * or the internal 16 MB flash memory. + * + * - The SPI controller bus seems to be left unconnected. Inputs are routed to + * a JAMMA PCB edge connector and handled through two custom/relabeled chips, + * which expose the inputs as memory-mapped registers. There is also a JVS + * port (i.e. RS-485 serial bus wired to a USB-A connector, commonly used for + * daisy-chaining peripherals in arcade cabinets) managed by a + * microcontroller. + * + * - There is a "security cartridge" slot, which breaks out the serial port as + * well as several GPIO pins. All security cartridge communication and DRM is + * handled by games rather than by the BIOS, so a security cartridge is *not* + * required to boot homebrew. Each game came with a different cartridge type; + * many of them expose the serial port or provide additional game-specific + * I/O connectors. + * + * Currently the only publicly available documentation for the custom registers + * is the System 573 MAME driver. Also keep in mind that the psxcd library does + * not yet support IDE drives, so the 573's drive can only be accessed by + * writing a custom ATAPI driver and ISO9660 parser (which is out of the scope + * of this example). + * + * https://github.com/mamedev/mame/blob/master/src/mame/drivers/ksys573.cpp + * https://github.com/mamedev/mame/blob/master/src/mame/machine/k573dio.cpp + */ + +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <psxetc.h> +#include <psxapi.h> +#include <psxgpu.h> +#include <psxpad.h> + +/* Register definitions */ + +#define EXP1_ADDR *((volatile uint32_t *) 0x1f801000) +#define EXP1_CTRL *((volatile uint32_t *) 0x1f801008) + +#define K573_IN0 *((volatile uint16_t *) 0x1f400000) +#define K573_IN1_L *((volatile uint16_t *) 0x1f400004) +#define K573_IN1_H *((volatile uint16_t *) 0x1f400006) +#define K573_IN2 *((volatile uint16_t *) 0x1f400008) +#define K573_IN3_L *((volatile uint16_t *) 0x1f40000c) +#define K573_IN3_H *((volatile uint16_t *) 0x1f40000e) +#define K573_BANK *((volatile uint16_t *) 0x1f500000) +#define K573_WATCHDOG *((volatile uint16_t *) 0x1f5c0000) + +#define K573_IDE_CS0 ((volatile uint16_t *) 0x1f480000) +#define K573_IDE_CS1 ((volatile uint16_t *) 0x1f4c0000) +#define K573_RTC ((volatile uint16_t *) 0x1f620000) +#define K573_IO_BOARD ((volatile uint16_t *) 0x1f640000) + +typedef enum { + ANALOG_IO_LIGHTS0 = 0x20, + ANALOG_IO_LIGHTS1 = 0x22, + ANALOG_IO_LIGHTS2 = 0x24, + ANALOG_IO_LIGHTS3 = 0x26, + + // The digital I/O board has a lot more registers than these, but there + // seems to be no DIGITAL_IO_LIGHTS6 register. WTF + DIGITAL_IO_LIGHTS1 = 0x70, + DIGITAL_IO_LIGHTS0 = 0x71, + DIGITAL_IO_LIGHTS3 = 0x72, + DIGITAL_IO_LIGHTS7 = 0x73, + DIGITAL_IO_LIGHTS4 = 0x7d, + DIGITAL_IO_LIGHTS5 = 0x7e, + DIGITAL_IO_LIGHTS2 = 0x7f +} IO_BOARD_REG; + +// The 573's real-time clock chip is an M48T58, which behaves like a standard +// 8 KB battery-backed SRAM with a bunch of special registers. Official games +// store highscores and settings in RTC RAM. +typedef enum { + RTC_CTRL = 0x1ff8, + RTC_SECONDS = 0x1ff9, + RTC_MINUTES = 0x1ffa, + RTC_HOURS = 0x1ffb, + RTC_DAY_OF_WEEK = 0x1ffc, + RTC_DAY_OF_MONTH = 0x1ffd, + RTC_MONTH = 0x1ffe, + RTC_YEAR = 0x1fff +} RTC_REG; + +#define btoi(x) ((((x) >> 4) & 0xf) * 10 + ((x) & 0xf)) + +/* 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; +} DB; + +typedef struct { + DB db[2]; + uint32_t db_active; +} CONTEXT; + +void init_context(CONTEXT *ctx) { + DB *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(CONTEXT *ctx) { + DB *db; + + DrawSync(0); + VSync(0); + ctx->db_active ^= 1; + + db = &(ctx->db[ctx->db_active]); + PutDrawEnv(&(db->draw)); + PutDispEnv(&(db->disp)); + SetDispMask(1); +} + +/* Input polling utilities */ + +typedef struct { + uint8_t p1_joy, p1_btn; + uint8_t p2_joy, p2_btn; + uint8_t coin, dip_sw; +} JAMMAInputs; + +void get_jamma_inputs(JAMMAInputs *output) { + uint16_t in1l = K573_IN1_L; + uint16_t in1h = K573_IN1_H; + uint16_t in2 = K573_IN2; + uint16_t in3l = K573_IN3_L; + uint16_t in3h = K573_IN3_H; + uint8_t p1_btn, p2_btn, coin; + + // Rearrange the bits read from the input register into something that's + // easier to parse and display. Refer to MAME for information on what each + // bit in the IN* registers does. + p1_btn = ((in2 >> 15) & 0x0001); // Bit 0 = start button + p1_btn |= ((in2 >> 8) & 0x0007) << 1; // Bit 1-3 = buttons 1-3 + p1_btn |= ((in3l >> 8) & 0x0003) << 4; // Bit 4-5 = buttons 4-5 + p1_btn |= ((in3l >> 11) & 0x0001) << 6; // Bit 6 = button 6 + p2_btn = ((in2 >> 7) & 0x0001); // Bit 0 = start button + p2_btn |= ((in2 >> 4) & 0x0007) << 1; // Bit 1-3 = buttons 1-3 + p2_btn |= ((in3h >> 8) & 0x0003) << 4; // Bit 4-5 = buttons 4-5 + p2_btn |= ((in3h >> 11) & 0x0001) << 6; // Bit 6 = button 6 + coin = ((in1h >> 8) & 0x0003); // Bit 0-1 = coin switches + coin |= ((in1h >> 12) & 0x0001) << 2; // Bit 2 = service button + coin |= ((in3l >> 10) & 0x0001) << 3; // Bit 3 = test button + coin |= ((in1h >> 10) & 0x0003) << 4; // Bit 4-5 = PCMCIA cards + + output->p1_joy = (in2 >> 8) & 0x000f; + output->p1_btn = p1_btn; + output->p2_joy = in2 & 0x000f; + output->p2_btn = p2_btn; + output->coin = coin; + output->dip_sw = in1l & 0x000f; +} + +/* I/O board (light control) utilities */ + +// This function controls light outputs on analog I/O boards. +void set_lights_analog(uint32_t lights) { + uint32_t bits; + + bits = (lights & 0x01010101) << 7; // Lamp n*8+0 -> bit n*8+7 + bits |= (lights & 0x02020202) << 5; // Lamp n*8+1 -> bit n*8+6 + bits |= (lights & 0x04040404) >> 1; // Lamp n*8+2 -> bit n*8+1 + bits |= (lights & 0x08080808) >> 3; // Lamp n*8+3 -> bit n*8+0 + bits |= (lights & 0x10101010) << 1; // Lamp n*8+4 -> bit n*8+5 + bits |= (lights & 0x20202020) >> 1; // Lamp n*8+5 -> bit n*8+4 + bits |= (lights & 0x40404040) >> 3; // Lamp n*8+6 -> bit n*8+3 + bits |= (lights & 0x80808080) >> 5; // Lamp n*8+7 -> bit n*8+2 + + K573_IO_BOARD[ANALOG_IO_LIGHTS0] = (bits) & 0xff; + K573_IO_BOARD[ANALOG_IO_LIGHTS1] = (bits >> 8) & 0xff; + K573_IO_BOARD[ANALOG_IO_LIGHTS2] = (bits >> 16) & 0xff; + K573_IO_BOARD[ANALOG_IO_LIGHTS3] = (bits >> 24) & 0xff; +} + +// This function controls light outputs on digital I/O boards (i.e. the ones +// that include MP3 playback hardware in addition to the light control). +// TODO: test this on real hardware -- it might not work if lights are handled +// by the board's FPGA, which requires a binary blob... +void set_lights_digital(uint32_t lights) { + uint32_t bits; + + bits = (lights & 0x11111111); // Lamp n*4+0 -> bit n*4+0 + bits |= (lights & 0x22222222) << 1; // Lamp n*4+1 -> bit n*4+2 + bits |= (lights & 0x44444444) << 1; // Lamp n*4+2 -> bit n*4+3 + bits |= (lights & 0x88888888) >> 2; // Lamp n*4+3 -> bit n*4+1 + + K573_IO_BOARD[DIGITAL_IO_LIGHTS0] = ((bits) & 0xf) << 12; + K573_IO_BOARD[DIGITAL_IO_LIGHTS1] = ((bits >> 4) & 0xf) << 12; + K573_IO_BOARD[DIGITAL_IO_LIGHTS2] = ((bits >> 8) & 0xf) << 12; + K573_IO_BOARD[DIGITAL_IO_LIGHTS3] = ((bits >> 12) & 0xf) << 12; + K573_IO_BOARD[DIGITAL_IO_LIGHTS4] = ((bits >> 16) & 0xf) << 12; + K573_IO_BOARD[DIGITAL_IO_LIGHTS5] = ((bits >> 20) & 0xf) << 12; + //K573_IO_BOARD[DIGITAL_IO_LIGHTS6] = ((bits >> 24) & 0xf) << 12; + K573_IO_BOARD[DIGITAL_IO_LIGHTS7] = ((bits >> 28) & 0xf) << 12; +} + +/* Main */ + +static CONTEXT 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[]) { + // Reinitialize the heap and relocate the stack to allow the 573's full 4 + // MB of RAM to be used. This isn't strictly required; executables designed + // for 2 MB of RAM will also run fine on the 573 (obviously). + // FIXME: this seems to be broken currently + //__asm__ volatile("li $sp, 0x803fffe0"); + //_mem_init(0x400000, 0x20000); + + EXP1_ADDR = 0x1f000000; + EXP1_CTRL = 0x24173f47; // 573 BIOS uses this value + K573_WATCHDOG = 0; + + init_context(&ctx); + + // Determine whether we are running on a 573 by fetching the version string + // from the BIOS. + const char *const version = (const char *const) GetSystemInfo(0x02); + //if (strncmp(version, "Konami OS", 9)) + //SHOW_ERROR("ERROR: NOT RUNNING ON A SYSTEM 573!\n\n[%s]\n", version); + + uint32_t counter = 0; + uint8_t last_joystick = 0xff; + uint8_t last_buttons = 0xff; + uint32_t current_light = 0; + uint32_t is_digital = 0; + + while (1) { + FntPrint(-1, "COUNTER=%d\n", counter++); + + JAMMAInputs inputs; + get_jamma_inputs(&inputs); + + FntPrint(-1, "\nJAMMA INPUTS:\n"); + FntPrint(-1, " P1 JOYSTICK =%04@\n", inputs.p1_joy); + FntPrint(-1, " P1 BUTTONS =%07@\n", inputs.p1_btn); + FntPrint(-1, " P2 JOYSTICK =%04@\n", inputs.p2_joy); + FntPrint(-1, " P2 BUTTONS =%07@\n", inputs.p2_btn); + FntPrint(-1, " COIN/SERVICE=%04@\n", inputs.coin & 0xf); + FntPrint(-1, " DIP SWITCHES=%04@\n", inputs.dip_sw); + + FntPrint(-1, "\nCABINET LIGHTS:\n"); + FntPrint(-1, " BOARD=%s I/O\n", is_digital ? "DIGITAL" : "ANALOG"); + FntPrint(-1, " LIGHT=%d\n\n", current_light); + FntPrint(-1, " [START] CHANGE BOARD TYPE\n"); + FntPrint(-1, " [LEFT/RIGHT] SELECT LIGHT TO TEST\n"); + + // Request the current date/time from the RTC and display it. + K573_RTC[RTC_CTRL] |= 0x40; + FntPrint(-1, "\nRTC:\n"); + FntPrint( + -1, + " %02d-%02d-%02d %02d:%02d:%02d\n", + btoi(K573_RTC[RTC_YEAR]), + btoi(K573_RTC[RTC_MONTH]), + btoi(K573_RTC[RTC_DAY_OF_MONTH] & 0x3f), + btoi(K573_RTC[RTC_HOURS]), + btoi(K573_RTC[RTC_MINUTES]), + btoi(K573_RTC[RTC_SECONDS] & 0x7f) + ); + + FntPrint(-1, "\nSYSTEM:\n"); + FntPrint(-1, " KERNEL=%s\n", version); + FntPrint(-1, " PCMCIA=%02@\n", inputs.coin >> 4); + + FntFlush(-1); + display(&ctx); + + // Reset the watchdog. This must be done at least once per frame to + // prevent the 573 from rebooting. + K573_WATCHDOG = 0; + + if (is_digital) + set_lights_digital(1 << current_light); + else + set_lights_analog(1 << current_light); + + // Handle inputs. + if ((last_joystick & 0x01) && !(inputs.p1_joy & 0x01)) // Left + current_light--; + if ((last_joystick & 0x02) && !(inputs.p1_joy & 0x02)) // Right + current_light++; + if ((last_buttons & 0x02) && !(inputs.p1_btn & 0x02)) // Button 1 + current_light--; + if ((last_buttons & 0x04) && !(inputs.p1_btn & 0x04)) // Button 2 + current_light++; + if ((last_buttons & 0x01) && !(inputs.p1_btn & 0x01)) { // Start + is_digital = !is_digital; + if (is_digital) + set_lights_analog(0); + else + set_lights_digital(0); + } + + current_light %= 32; + last_joystick = inputs.p1_joy; + last_buttons = inputs.p1_btn; + } + + return 0; +} diff --git a/examples/lowlevel/cartrom/CMakeLists.txt b/examples/lowlevel/cartrom/CMakeLists.txt index 3e807a3..107cc3d 100644 --- a/examples/lowlevel/cartrom/CMakeLists.txt +++ b/examples/lowlevel/cartrom/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.21) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( cartrom LANGUAGES C ASM diff --git a/examples/readme.txt b/examples/readme.txt deleted file mode 100644 index 80747de..0000000 --- a/examples/readme.txt +++ /dev/null @@ -1,64 +0,0 @@ -PSn00bSDK Example Programs
-2019 - 2022 Meido-Tek Productions / PSn00bSDK Project
-
-## Building the examples ##
-
-The instructions below assumes that a mipsel-unknown-elf or mipsel-none-elf GNU
-toolchain is installed, CMake and Ninja or make is installed and the PSn00bSDK
-libraries and tools are compiled and installed. CMake version must be at least
-version 3.20 or newer.
-
-1. If the examples are in /usr/local/share, copy the directory into your home
- directory.
-
-2. Configure the examples with CMake by running:
-
- cmake -S . -B ./build -DCMAKE_TOOLCHAIN_FILE=<path to sdk.cmake>
-
- <path to sdk.cmake> must point to the sdk.cmake file provided by the SDK.
- Unless you've installed the SDK with a custom path, normally this file is
- located in /usr/local/lib/libpsn00b/cmake on *nix style systems or
- C:\Program Files\PSn00bSDK\lib\libpsn00b\cmake in Windows.
-
- If the mipsel toolchain has a different prefix (ie. mipsel-none-elf), specify
- -DPSN00BSDK_TARGET=<prefix> to override the default toolchain prefix.
-
- If Ninja does not work for you or don't have it installed, pass
- -G "Unix Makefiles" (or -G "MSys Makefiles" on Windows) to build using make
- instead.
-
-3. Build the example programs by running:
-
- cmake --build ./build
-
- This should create a build directory with a directory structure that mirrors
- the parent directory. The directories should contain compiled versions of the
- example programs as a PS-EXE or ISO file.
-
-
-## Examples summary ##
-
-The following list is a brief summary of all the example programs included.
-Additional information may be found in the source code of each example.
-
- beginner/cppdemo Simple demonstration of (dynamic) C++ classes
- beginner/hello The obligatory "Hello World" example program
- cdrom/cdbrowse File browser using libpsxcd's directory functions
- cdrom/cdxa Plays CD-XA audio (XA audio not included)
- demos/n00bdemo The premiere demonstration program of PSn00bSDK
- graphics/balls Draws colored balls bouncing around the screen
- graphics/billboard Demonstrates how to draw 2D sprites in a 3D space
- graphics/fpscam First-person perspective camera with look-at
- graphics/gte Displays a rotating cube using GTE macros
- graphics/hdtv Demonstrates anamorphic widescreen at 704x480
- graphics/render2tex Procedural texture effects using off-screen drawing
- graphics/rgb24 Displays a 640x480 24-bit RGB image
- graphics/tilesasm Drawing a tile-map with assembly language
- io/pads Demonstrates reading controllers via low-level access
- lowlevel/cartrom ROM firmware for cheat devices written using GNU GAS
- sound/vagsample Demonstrates playing VAG sound files with the SPU
- system/childexec Loading a child program and returning to parent
- system/console TTY based text console that interrupts gameplay
- system/dynlink Demonstrates dynamically linked libraries
- system/timer Demonstrates using hardware timers with interrupts
- system/tty Using TTY as a remote text console interface
\ No newline at end of file diff --git a/examples/sound/spustream/CMakeLists.txt b/examples/sound/spustream/CMakeLists.txt new file mode 100644 index 0000000..9e84fa3 --- /dev/null +++ b/examples/sound/spustream/CMakeLists.txt @@ -0,0 +1,25 @@ +# PSn00bSDK example CMake script +# (C) 2021 spicyjpeg - MPL licensed + +cmake_minimum_required(VERSION 3.20) + +project( + spustream + LANGUAGES C + VERSION 1.0.0 + DESCRIPTION "PSn00bSDK SPU custom 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 STATIC ${_sources}) +#psn00bsdk_add_cd_image(spustream_iso spustream iso.xml DEPENDS spustream) + +install( + FILES + #${PROJECT_BINARY_DIR}/spustream.bin + #${PROJECT_BINARY_DIR}/spustream.cue + ${PROJECT_BINARY_DIR}/spustream.exe + TYPE BIN +) diff --git a/examples/sound/spustream/convert_stream.py b/examples/sound/spustream/convert_stream.py new file mode 100644 index 0000000..1b1696f --- /dev/null +++ b/examples/sound/spustream/convert_stream.py @@ -0,0 +1,112 @@ +#!/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/iso.xml b/examples/sound/spustream/iso.xml new file mode 100644 index 0000000..3807046 --- /dev/null +++ b/examples/sound/spustream/iso.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<iso_project + image_name="${CD_IMAGE_NAME}.bin" + cue_sheet="${CD_IMAGE_NAME}.cue" +> + <track type="data"> + <identifiers + system ="PLAYSTATION" + volume ="SPUSTREAM" + volume_set ="SPUSTREAM" + publisher ="MEIDOTEK" + data_preparer ="PSN00BSDK ${PSN00BSDK_VERSION}" + application ="PLAYSTATION" + copyright ="README.TXT;1" + /> + + <directory_tree> + <file name="SYSTEM.CNF" type="data" source="${PROJECT_SOURCE_DIR}/system.cnf" /> + <file name="SPUSTRM.EXE" type="data" source="spustream.exe" /> + <file name="SPUSTRM.MAP" type="data" source="spustream.map" /> + + <file name="STREAM.BIN" type="data" source="${PROJECT_SOURCE_DIR}/stream.bin" /> + + <dummy sectors="1024"/> + </directory_tree> + </track> + + <!--<track type="audio" source="track2.wav" />--> +</iso_project> diff --git a/examples/sound/spustream/main.c b/examples/sound/spustream/main.c new file mode 100644 index 0000000..be095cb --- /dev/null +++ b/examples/sound/spustream/main.c @@ -0,0 +1,456 @@ +/* + * PSn00bSDK SPU audio streaming example + * (C) 2021 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. + * + * 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_CHANNELS[n].loop_addr) once 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: + * + * /================================================\ + * | /==================\ | + * v Loop point | v Loop point | + * +-------+----------------+----------------+----------------+----------------+ + * | 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. + * + * 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________________________/ + * + * Such file isn't provided as PSn00bSDK doesn't yet have a tool for audio + * transcoding. 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. + */ + +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <psxetc.h> +#include <psxapi.h> +#include <psxgpu.h> +#include <psxpad.h> +#include <psxspu.h> +#include <psxcd.h> + +// 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 26624 // (26624 / 16 * 28) / 44100 = 1.05 seconds + +#define NUM_CHANNELS 2 +#define CHANNEL_MASK 0x03 + +/* Register definitions */ + +// For some reason SpuVoiceRaw doesn't actually match the layout of SPU +// registers, so here we go. +typedef struct { + uint16_t vol_left; + uint16_t vol_right; + uint16_t freq; + uint16_t addr; + uint32_t adsr_param; + uint16_t _reserved; + uint16_t loop_addr; +} SPUChannel; + +#define SPU_CTRL *((volatile uint16_t *) 0x1f801daa) +#define SPU_IRQ_ADDR *((volatile uint16_t *) 0x1f801da4) +#define SPU_KEY_ON *((volatile uint32_t *) 0x1f801d88) +#define SPU_KEY_OFF *((volatile uint32_t *) 0x1f801d8c) + +// SPU RAM is addressed in 8-byte units, using 16-bit pointers. +#define SPU_CHANNELS ((volatile SPUChannel *) 0x1f801c00) +#define SPU_RAM_ADDR(x) ((uint16_t) (((uint32_t) (x)) >> 3)) + +/* 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; +} DB; + +typedef struct { + DB db[2]; + uint32_t db_active; +} CONTEXT; + +void init_context(CONTEXT *ctx) { + DB *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(CONTEXT *ctx) { + DB *db; + + DrawSync(0); + VSync(0); + ctx->db_active ^= 1; + + db = &(ctx->db[ctx->db_active]); + PutDrawEnv(&(db->draw)); + PutDispEnv(&(db->disp)); + SetDispMask(1); +} + +/* Stream interrupt handlers */ + +// This is a silent looping sample used 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"). It is 64 bytes as that is the minimum size for SPU DMA +// transfers, however only the first 16 bytes are kept. The rest is going to be +// overwritten by chunks. +// https://problemkaputt.de/psx-spx.htm#spuinterrupt +const uint8_t SPU_DUMMY_BLOCK[] = { + 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +}; + +// The first 4 KB of SPU RAM are reserved for capture buffers, so we have to +// place stream buffers after those. Sony's SPU library additionally places a +// dummy sample at 0x1000; we are going to do the same with the block above. +#define DUMMY_BLOCK_ADDR 0x1000 +#define BUFFER_START_ADDR 0x1010 +#define CHUNK_SIZE (BUFFER_SIZE * NUM_CHANNELS) + +typedef struct { + uint32_t lba; + uint32_t length; + uint32_t pos; + + uint32_t spu_addr; + uint32_t spu_pos; + uint32_t db_active; +} StreamContext; + +static volatile 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 uint8_t sector_buffer[2048]; + +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. + SPU_CTRL &= 0xffbf; + + str_ctx.db_active ^= 1; + str_ctx.spu_pos = 0; + + // 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); + + // Configure to SPU to trigger an IRQ once the buffer 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); + + for (uint32_t i = 0; i < NUM_CHANNELS; i++) + SPU_CHANNELS[i].loop_addr = SPU_RAM_ADDR(str_ctx.spu_addr + 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); +} + +void cd_event_handler(int32_t 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 (uint32_t i = 0; i < NUM_CHANNELS; i++) { + if ( + str_ctx.spu_pos >= (BUFFER_SIZE * i - 2048) && + str_ctx.spu_pos < (BUFFER_SIZE * i) + ) + sector[(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. + uint32_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. + // TODO TODO: preload first sector + if (str_ctx.spu_pos >= CHUNK_SIZE) { + CdControlF(CdlPause, 0); + SPU_CTRL |= 0x0040; + } +} + +/* Stream helpers */ + +void init_spu_channels(void) { + // Upload the dummy block to the SPU and play it on all channels, locking + // them up and stopping them from messing with the SPU interrupt. + // TODO: is this really necessary? (needs testing on real hardware) + SpuSetTransferStartAddr(DUMMY_BLOCK_ADDR); + SpuWrite(SPU_DUMMY_BLOCK, 64); + + SPU_KEY_OFF = 0x00ffffff; + + for (uint32_t i = 0; i < 24; i++) + SPU_CHANNELS[i].addr = SPU_RAM_ADDR(DUMMY_BLOCK_ADDR); + + SPU_KEY_ON = 0x00ffffff; +} + +void init_stream(CdlFILE *file) { + EnterCriticalSection(); + InterruptCallback(9, &spu_irq_handler); + CdReadyCallback(&cd_event_handler); + ExitCriticalSection(); + + // 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; + + // Ensure at least one chunk is in SPU RAM by invoking the SPU 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) + __asm__("nop"); +} + +void start_stream(void) { + SPU_KEY_OFF = CHANNEL_MASK; + + for (uint32_t i = 0; i < NUM_CHANNELS; i++) { + SPU_CHANNELS[i].addr = SPU_RAM_ADDR(BUFFER_START_ADDR + BUFFER_SIZE * i); + SPU_CHANNELS[i].freq = SAMPLE_RATE; + SPU_CHANNELS[i].adsr_param = 0x1fee80ff; // or 0x9fc080ff, 0xdff18087 + } + + // 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_CHANNELS[0].vol_left = 0x3fff; + SPU_CHANNELS[0].vol_right = 0x0000; + SPU_CHANNELS[1].vol_left = 0x0000; + SPU_CHANNELS[1].vol_right = 0x3fff; + + SPU_KEY_ON = CHANNEL_MASK; + spu_irq_handler(); +} + +/* Main */ + +static CONTEXT 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(); + init_spu_channels(); + + SHOW_STATUS("LOCATING 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); + start_stream(); + + // Set up controller polling. + uint8_t pad_buff[2][34]; + InitPAD(pad_buff[0], 34, pad_buff[1], 34); + StartPAD(); + ChangeClearPAD(0); + + uint16_t sample_rate = SAMPLE_RATE; + uint16_t last_buttons = 0xffff; + + while (1) { + FntPrint(-1, "PLAYING SPU STREAM\n"); + if (str_ctx.spu_pos >= CHUNK_SIZE) + FntPrint(-1, "STATUS: IDLE\n\n"); + else if (!str_ctx.spu_pos) + FntPrint(-1, "STATUS: SEEKING\n\n"); + else + FntPrint(-1, "STATUS: BUFFERING\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, "SMP RATE=%5d HZ\n\n", (sample_rate * 44100) >> 12); + + 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 != 4) && (pad->type != 5) && (pad->type != 7)) + continue; + + // 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; + if (!(pad->btn & PAD_RIGHT)) + str_ctx.pos += 16; + if ((last_buttons & PAD_CIRCLE) && !(pad->btn & PAD_CIRCLE)) + str_ctx.pos = 0; + + 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; + + // Only set the sample rate registers if necessary. + if (pad->btn != 0xffff) { + for (uint32_t i = 0; i < NUM_CHANNELS; i++) + SPU_CHANNELS[i].freq = sample_rate; + } + + last_buttons = pad->btn; + } + + return 0; +} diff --git a/examples/sound/spustream/system.cnf b/examples/sound/spustream/system.cnf new file mode 100644 index 0000000..0c4561a --- /dev/null +++ b/examples/sound/spustream/system.cnf @@ -0,0 +1,4 @@ +BOOT=cdrom:\spustrm.exe;1 +TCB=4 +EVENT=10 +STACK=801FFFF0 diff --git a/examples/sound/vagsample/0proyt.h b/examples/sound/vagsample/0proyt.h index 4402b9a..73629f9 100644 --- a/examples/sound/vagsample/0proyt.h +++ b/examples/sound/vagsample/0proyt.h @@ -12616,6 +12616,6 @@ unsigned char proyt[] = { 0xf2,0xf0,0xf2,0x15,0xc2,0x1b,0x00,0x00,0x01,0xe3,0x31,0x13,0xf3,0x1f,0xe2, 0x2f,0x13,0xd6,0x20,0x6e,0x2d,0x1b,0x00,0x03,0x42,0xc3,0x14,0x30,0x20,0x21, 0x21,0x32,0x0e,0x32,0x12,0x42,0xf0,0x39,0x01,0x13,0x12,0x04,0xb3,0x06,0x1f, -0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x00,0x77,0x77,0x77,0x77,0x77, +0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0x05,0x77,0x77,0x77,0x77,0x77, 0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77 }; diff --git a/examples/sound/vagsample/CMakeLists.txt b/examples/sound/vagsample/CMakeLists.txt index 1a15d9c..f37be97 100644 --- a/examples/sound/vagsample/CMakeLists.txt +++ b/examples/sound/vagsample/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( vagsample LANGUAGES C diff --git a/examples/sound/vagsample/threedeeffeggzz.h b/examples/sound/vagsample/threedeeffeggzz.h index ed0e098..f0815aa 100644 --- a/examples/sound/vagsample/threedeeffeggzz.h +++ b/examples/sound/vagsample/threedeeffeggzz.h @@ -15194,6 +15194,6 @@ unsigned char tdfx[] = { 0x0b,0x1e,0x21,0xa0,0x4f,0xe2,0x2d,0x17,0x00,0xd2,0x02,0x1d,0xd1,0xf0,0x21, 0xfa,0x2e,0xc4,0x1e,0xfe,0xf2,0x2f,0x24,0x17,0x00,0xef,0xee,0x1f,0xf2,0xce, 0x0d,0x25,0x01,0x3e,0xcb,0xe1,0x30,0xeb,0xc4,0x17,0x01,0x1f,0x1f,0xaf,0x22, -0xfd,0xe0,0xf3,0x0b,0x2e,0xdd,0xc3,0xde,0x12,0x2d,0x07,0x00,0x77,0x77,0x77, +0xfd,0xe0,0xf3,0x0b,0x2e,0xdd,0xc3,0xde,0x12,0x2d,0x07,0x05,0x77,0x77,0x77, 0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77,0x77 }; diff --git a/examples/system/childexec/CMakeLists.txt b/examples/system/childexec/CMakeLists.txt index 88168e0..ca0c110 100644 --- a/examples/system/childexec/CMakeLists.txt +++ b/examples/system/childexec/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( childexec LANGUAGES C ASM diff --git a/examples/system/console/CMakeLists.txt b/examples/system/console/CMakeLists.txt index 6dc6154..d58f212 100644 --- a/examples/system/console/CMakeLists.txt +++ b/examples/system/console/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( console LANGUAGES C diff --git a/examples/system/dynlink/CMakeLists.txt b/examples/system/dynlink/CMakeLists.txt index 5834647..aae3bb3 100644 --- a/examples/system/dynlink/CMakeLists.txt +++ b/examples/system/dynlink/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( dynlink LANGUAGES C diff --git a/examples/system/dynlink/display.c b/examples/system/dynlink/display.c deleted file mode 100644 index 573f17c..0000000 --- a/examples/system/dynlink/display.c +++ /dev/null @@ -1,74 +0,0 @@ -/* - * PSn00bSDK dynamic linker example (utilities) - * (C) 2021 spicyjpeg - MPL licensed - */ - -#include <psxgpu.h> - -#include "library/dll_common.h" - -#define SCREEN_XRES 320 -#define SCREEN_YRES 240 - -#define BGCOLOR_R 48 -#define BGCOLOR_G 24 -#define BGCOLOR_B 0 - -/* Display/GPU context utilities */ - -void init_context(CONTEXT *ctx) { - DB *db; - - ResetGraph(0); - ctx->xres = SCREEN_XRES; - ctx->yres = SCREEN_YRES; - 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; - - // Set up the ordering tables and primitive buffers. - db = &(ctx->db[0]); - ctx->db_nextpri = db->p; - ClearOTagR((u_long *) db->ot, OT_LEN); - - PutDrawEnv(&(db->draw)); - //PutDispEnv(&(db->disp)); - - db = &(ctx->db[1]); - ClearOTagR((u_long *) db->ot, OT_LEN); - - // Create a text stream at the top of the screen. - FntLoad(960, 0); - FntOpen(4, 12, 312, 32, 2, 256); -} - -void display(CONTEXT *ctx) { - DB *db; - - DrawSync(0); - VSync(0); - ctx->db_active ^= 1; - - db = &(ctx->db[ctx->db_active]); - ctx->db_nextpri = db->p; - ClearOTagR((u_long *) db->ot, OT_LEN); - - PutDrawEnv(&(db->draw)); - PutDispEnv(&(db->disp)); - SetDispMask(1); - - db = &(ctx->db[!ctx->db_active]); - DrawOTag((u_long *) &(db->ot[OT_LEN - 1])); -} diff --git a/examples/system/dynlink/main.c b/examples/system/dynlink/main.c index 6d93e71..690371e 100644 --- a/examples/system/dynlink/main.c +++ b/examples/system/dynlink/main.c @@ -11,14 +11,14 @@ * however this may be expanded in the future. * * Being able to introspect local symbols at runtime, in turn, allows us to use - * the dl*() set of APIs to load, link and execute code from an external file + * another set of APIs to load, link and execute code from an external file * (compiled with the dll.ld linker script). A dynamically-loaded library can * reference and access any non-static function or variable within the main * executable (and the libraries the main executable has been compiled with); * the dynamic linker will automatically patch the DLL's code and resolve these * references so that they point to the addresses listed in the map file. DLLs * also have their own symbol tables, and any symbol in a DLL is accessible to - * the main executable through dlsym(). + * the main executable through DL_GetDLLSymbol(). * * This example shows how DLLs can be loaded and unloaded at any time. Pressing * START will unload the current DLL and load an alternate one on-the-fly. A @@ -47,6 +47,7 @@ #include <psxgte.h> #include <psxgpu.h> #include <psxpad.h> +#include <psxcd.h> #include "library/dll_common.h" @@ -67,14 +68,77 @@ const void *const DO_NOT_STRIP[] __attribute__((section(".dummy"))) = { }; static const char *const DLL_FILENAMES[] = { - "cdrom:CUBE.DLL;1", - "cdrom:BALLS.DLL;1" + "\\CUBE.DLL;1", + "\\BALLS.DLL;1" }; #define DLL_COUNT 2 -void init_context(CONTEXT *ctx); -void display(CONTEXT *ctx); +/* Display/GPU context utilities */ + +#define SCREEN_XRES 320 +#define SCREEN_YRES 240 + +#define BGCOLOR_R 48 +#define BGCOLOR_G 24 +#define BGCOLOR_B 0 + +void init_context(CONTEXT *ctx) { + DB *db; + + ResetGraph(0); + ctx->xres = SCREEN_XRES; + ctx->yres = SCREEN_YRES; + 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; + + // Set up the ordering tables and primitive buffers. + db = &(ctx->db[0]); + ctx->db_nextpri = db->p; + ClearOTagR((u_long *) db->ot, OT_LEN); + + PutDrawEnv(&(db->draw)); + //PutDispEnv(&(db->disp)); + + db = &(ctx->db[1]); + ClearOTagR((u_long *) db->ot, OT_LEN); + + // Create a text stream at the top of the screen. + FntLoad(960, 0); + FntOpen(4, 12, 312, 32, 2, 256); +} + +void display(CONTEXT *ctx) { + DB *db; + + DrawSync(0); + VSync(0); + ctx->db_active ^= 1; + + db = &(ctx->db[ctx->db_active]); + ctx->db_nextpri = db->p; + ClearOTagR((u_long *) db->ot, OT_LEN); + + PutDrawEnv(&(db->draw)); + PutDispEnv(&(db->disp)); + SetDispMask(1); + + db = &(ctx->db[!ctx->db_active]); + DrawOTag((u_long *) &(db->ot[OT_LEN - 1])); +} /* Symbol overriding example */ @@ -118,8 +182,8 @@ void *custom_resolver(DLL *dll, const char *name) { // Define a struct to store pointers to a DLL's functions into. This is not // strictly required, however looking up symbols is a relatively slow operation -// and the pointers returned by dlsym() should be saved and reused as much as -// possible. +// and the pointers returned by DL_GetDLLSymbol() should be saved and reused as +// much as possible. typedef struct { void (*init)(CONTEXT *); void (*render)(CONTEXT *, uint16_t buttons); @@ -134,47 +198,79 @@ static CONTEXT ctx; #define SHOW_STATUS(...) { FntPrint(-1, __VA_ARGS__); FntFlush(-1); display(&ctx); } #define SHOW_ERROR(...) { SHOW_STATUS(__VA_ARGS__); while (1) __asm__("nop"); } +// This is a simple function to read a CD-ROM file into memory (the dynamic +// linker no longer provides APIs that take file paths directly). You might +// want to replace this with code that e.g. loads the DLL from a compressed +// archive or even from the serial port. +size_t load_file(const char *filename, void **ptr) { + SHOW_STATUS("LOADING %s\n", filename); + + CdlFILE file; + if (!CdSearchFile(&file, filename)) + SHOW_ERROR("FAILED TO FIND %s\n", filename); + + // Round up the file size so it matches the number of sectors occupied by + // the file (as CdRead() can only return entire sectors). + size_t len = (file.size + 2047) & 0xfffff800; + void *_ptr = malloc(len); + if (!_ptr) + SHOW_ERROR("FAILED TO ALLOCATE %d BYTES\n", len); + + CdControl(CdlSetloc, &(file.pos), 0); + CdRead(len / 2048, _ptr, CdlModeSpeed); + if (CdReadSync(0, 0) < 0) + SHOW_ERROR("FAILED TO READ %s\n", filename); + + *ptr = _ptr; + return file.size; +} + void load_dll(const char *filename) { + // As we're passing RTLD_FREE_ON_DESTROY to DL_CreateDLL(), calling + // DL_DestroyDLL() will also deallocate the buffer the DLL was loaded into. if (dll) - dlclose(dll); + DL_DestroyDLL(dll); - SHOW_STATUS("LOADING %s\n", filename); + void *ptr; + size_t len = load_file(filename, &ptr); - dll = dlopen(filename, RTLD_LAZY); + dll = DL_CreateDLL(ptr, len, RTLD_LAZY | RTLD_FREE_ON_DESTROY); if (!dll) - SHOW_ERROR("FAILED TO LOAD %s\nERROR=%d\n", filename, (int32_t) dlerror()); + SHOW_ERROR("FAILED TO PARSE %s\nERROR=%d\n", filename, (int32_t) DL_GetLastError()); - dll_api.init = dlsym(dll, "init"); - dll_api.render = dlsym(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); // Unfortunately, due to how position-independent code works, function - // pointers returned by dlsym() can't be called directly. We have to use - // the DL_CALL() macro instead, which sets up register $t9 to ensure the - // function can locate and reference the DLL's relocation table. + // pointers returned by DL_GetDLLSymbol() can't be called directly. We have + // to use the DL_CALL() macro instead, which sets up register $t9 to ensure + // the function can locate and reference the DLL's relocation table. DL_CALL(dll_api.init, &ctx); } int main(int argc, const char* argv[]) { - // As DL_LoadSymbolMap() and dlopen() rely on BIOS file APIs, the BIOS CD - // driver must be initialized by calling _InitCd() prior to loading the - // symbol map (but after setting up the GPU, for some reason). init_context(&ctx); SHOW_STATUS("INITIALIZING CD\n"); - _InitCd(); + CdInit(); + + // Load the symbol map file, let the dynamic linker parse it and then + // unload it. + void *ptr; + size_t len = load_file("\\MAIN.MAP;1", &ptr); - SHOW_STATUS("LOADING SYMBOL MAP\n"); + if (!DL_ParseSymbolMap(ptr, len)) + SHOW_ERROR("FAILED TO PARSE SYMBOL MAP\nERROR=%d\n", (int32_t) DL_GetLastError()); - if (!DL_LoadSymbolMap("cdrom:MAIN.MAP;1")) - SHOW_ERROR("FAILED TO LOAD SYMBOL MAP\nERROR=%d\n", (int32_t) dlerror()); + free(ptr); // Try to obtain a reference to a local function. void (*_display)() = DL_GetSymbolByName("display"); if (!_display) - SHOW_ERROR("FAILED TO LOOK UP LOCAL FUNCTION\nERROR=%d\n", (int32_t) dlerror()); + SHOW_ERROR("FAILED TO LOOK UP LOCAL FUNCTION\nERROR=%d\n", (int32_t) DL_GetLastError()); printf("Symbol map test, display() @ %08x\n", _display); @@ -220,7 +316,7 @@ int main(int argc, const char* argv[]) { last_buttons = pad->btn; } - //dlclose(dll); + //DL_DestroyDLL(dll); //DL_UnloadSymbolMap(); return 0; } diff --git a/examples/system/timer/CMakeLists.txt b/examples/system/timer/CMakeLists.txt index 58daf9b..328e07e 100644 --- a/examples/system/timer/CMakeLists.txt +++ b/examples/system/timer/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( timer LANGUAGES C diff --git a/examples/system/tty/CMakeLists.txt b/examples/system/tty/CMakeLists.txt index 4e0ca36..0664502 100644 --- a/examples/system/tty/CMakeLists.txt +++ b/examples/system/tty/CMakeLists.txt @@ -3,10 +3,6 @@ cmake_minimum_required(VERSION 3.20) -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{PSN00BSDK_LIBS}) - set(CMAKE_TOOLCHAIN_FILE $ENV{PSN00BSDK_LIBS}/cmake/sdk.cmake) -endif() - project( tty LANGUAGES C |
