aboutsummaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorspicyjpeg <thatspicyjpeg@gmail.com>2022-10-16 23:52:19 +0200
committerspicyjpeg <thatspicyjpeg@gmail.com>2022-10-16 23:52:19 +0200
commit03434a230d8c3ed2e32a3885128e05e42ee11769 (patch)
treed7b06d4bce91472a1f01b9fb3a5965a9822c1a90 /examples
parent6eabb5aa549254c2272cedee26d4245f31f2dc7a (diff)
downloadpsn00bsdk-03434a230d8c3ed2e32a3885128e05e42ee11769.tar.gz
Add mdec/strvideo example, fix psxpress bug
Diffstat (limited to 'examples')
-rw-r--r--examples/mdec/strvideo/CMakeLists.txt24
-rw-r--r--examples/mdec/strvideo/iso.xml28
-rw-r--r--examples/mdec/strvideo/main.c439
-rw-r--r--examples/mdec/strvideo/system.cnf4
4 files changed, 495 insertions, 0 deletions
diff --git a/examples/mdec/strvideo/CMakeLists.txt b/examples/mdec/strvideo/CMakeLists.txt
new file mode 100644
index 0000000..d41556b
--- /dev/null
+++ b/examples/mdec/strvideo/CMakeLists.txt
@@ -0,0 +1,24 @@
+# PSn00bSDK example CMake script
+# (C) 2021 spicyjpeg - MPL licensed
+
+cmake_minimum_required(VERSION 3.21)
+
+project(
+ strvideo
+ LANGUAGES C
+ VERSION 1.0.0
+ DESCRIPTION "PSn00bSDK .STR video playback example"
+ HOMEPAGE_URL "http://lameguy64.net/?page=psn00bsdk"
+)
+
+file(GLOB _sources *.c)
+psn00bsdk_add_executable(strvideo GPREL ${_sources})
+#psn00bsdk_add_cd_image(strvideo_iso strvideo iso.xml DEPENDS strvideo)
+
+install(
+ FILES
+ #${PROJECT_BINARY_DIR}/strvideo.bin
+ #${PROJECT_BINARY_DIR}/strvideo.cue
+ ${PROJECT_BINARY_DIR}/strvideo.exe
+ TYPE BIN
+)
diff --git a/examples/mdec/strvideo/iso.xml b/examples/mdec/strvideo/iso.xml
new file mode 100644
index 0000000..65e0ff5
--- /dev/null
+++ b/examples/mdec/strvideo/iso.xml
@@ -0,0 +1,28 @@
+<?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 ="STRVIDEO"
+ volume_set ="STRVIDEO"
+ 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="STRVIDEO.EXE" type="data" source="strvideo.exe" />
+
+ <file name="VIDEO.STR" type="mixed" source="${PROJECT_SOURCE_DIR}/video.str"/>
+
+ <dummy sectors="1024"/>
+ </directory_tree>
+ </track>
+
+ <!--<track type="audio" source="track2.wav" />-->
+</iso_project>
diff --git a/examples/mdec/strvideo/main.c b/examples/mdec/strvideo/main.c
new file mode 100644
index 0000000..842bbb8
--- /dev/null
+++ b/examples/mdec/strvideo/main.c
@@ -0,0 +1,439 @@
+/*
+ * PSn00bSDK .STR FMV playback example
+ * (C) 2022 spicyjpeg - MPL licensed
+ *
+ * This example demonstrates playback of full-motion video in the standard .STR
+ * format, using the MDEC for frame decoding and XA for audio. Decoded frames
+ * are transferred directly to the main framebuffer in this example, but could
+ * also be output to another VRAM location and used as a background or texture
+ * for a 2D or 3D scene.
+ *
+ * Playing video files requires setting up a fairly complex pipeline, involving
+ * several buffers and components working in parallel:
+ *
+ * - .STR sectors are read continuously from the CD and each frame, usually
+ * spanning multiple sectors, is reassembled (demuxed) into a buffer in
+ * memory. In this example the task is performed by cd_sector_handler(). The
+ * CD drive handles XA-ADPCM sectors automatically, so no CPU intervention is
+ * necessary to play the audio track interleaved with the video.
+ * - Once a full frame has been demuxed, the bitstream data is parsed and
+ * decompressed by the CPU (using DecDCTvlc()) to an array of run-length
+ * codes to be fed to the MDEC. This is done in the main loop.
+ * - At the same time the last frame decompressed is read from RAM by the MDEC,
+ * which decodes it and outputs one 16-pixel-wide vertical slice at a time.
+ * - When a slice is ready, it is uploaded by mdec_dma_handler() to the current
+ * framebuffer in VRAM while the MDEC is decoding the next slice.
+ * A text overlay is drawn on top of the framebuffer using the GPU after the
+ * entire frame has been decoded.
+ *
+ * Since pretty much all buffers used are going to be read and written at the
+ * same time, double buffering is required for all of them. Every part of the
+ * pipeline must also run in lockstep with each other to prevent frame
+ * corruption, hence several functions and flag variables are used to stall the
+ * main loop until a frame is available for decoding and the MDEC is ready.
+ * Playback is stopped when a sector with the end-of-file flag set in the XA
+ * subheader (added at the end of the file by most .STR encoders) is
+ * encountered; in order to access the subheader, this example requests 2340
+ * bytes of data for each sector (rather than the usual 2048) from the drive.
+ *
+ * Note that PSn00bSDK's bitstream decoding API only supports version 1 and 2
+ * bitstreams currently, so make sure your .STR files are encoded as v2 and not
+ * v3.
+ */
+
+#include <stdint.h>
+#include <string.h>
+#include <psxetc.h>
+#include <psxapi.h>
+#include <psxgpu.h>
+#include <psxgte.h>
+#include <psxspu.h>
+#include <psxcd.h>
+#include <psxpress.h>
+#include <hwregs_c.h>
+
+// Uncomment to display the video in 24bpp mode. Note that the GPU does not
+// support 24bpp rendering, so the text overlay is only enabled in 16bpp mode.
+//#define DISP_24BPP
+
+/* Display/GPU context utilities */
+
+#define SCREEN_XRES 320
+#define SCREEN_YRES 240
+
+#define BGCOLOR_R 0
+#define BGCOLOR_G 0
+#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), 0, SCREEN_YRES, 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), 0, SCREEN_YRES, 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(4, 12, 312, 16, 2, 256);
+}
+
+void display(RenderContext *ctx, int sync) {
+ Framebuffer *db;
+ ctx->db_active ^= 1;
+
+ DrawSync(0);
+ if (sync)
+ VSync(0);
+
+ db = &(ctx->db[ctx->db_active]);
+ PutDrawEnv(&(db->draw));
+ PutDispEnv(&(db->disp));
+ SetDispMask(1);
+}
+
+/* CD and MDEC interrupt handlers */
+
+#ifdef DISP_24BPP
+#define BLOCK_SIZE 24
+#else
+#define BLOCK_SIZE 16
+#define DRAW_OVERLAY
+#endif
+
+#define VRAM_X_COORD(x) ((x) * BLOCK_SIZE / 16)
+
+// All non-audio sectors in .STR files begin with this 32-byte header, which
+// contains metadata about the sector and is followed by a chunk of frame
+// bitstream data.
+// https://problemkaputt.de/psx-spx.htm#cdromfilevideostrstreamingandbspicturecompressionsony
+typedef struct {
+ uint16_t magic; // Always 0x0160
+ uint16_t type; // 0x8001 for MDEC
+ uint16_t sector_id; // Chunk number (0 = first chunk of this frame)
+ uint16_t sector_count; // Total number of chunks for this frame
+ uint32_t frame_id; // Frame number
+ uint32_t bs_length; // Total length of this frame in bytes
+
+ uint16_t width, height;
+ uint8_t bs_header[8];
+ uint32_t _reserved;
+} STR_Header;
+
+// https://problemkaputt.de/psx-spx.htm#cdromxasubheaderfilechannelinterleave
+typedef struct {
+ uint8_t file, channel;
+ uint8_t submode, coding_info;
+} XA_Header;
+
+// https://problemkaputt.de/psx-spx.htm#cdromsectorencoding
+typedef struct {
+ CdlLOC pos;
+ XA_Header xa_header[2];
+ STR_Header str_header;
+ uint8_t data[2016];
+ uint32_t edc;
+ uint8_t ecc[276];
+} STR_Sector;
+
+typedef struct {
+ uint16_t width, height;
+ uint32_t bs_data[0x2000]; // Bitstream data read from the disc
+ uint32_t mdec_data[0x8000]; // Decompressed data to be fed to the MDEC
+} StreamBuffer;
+
+typedef struct {
+ StreamBuffer frames[2];
+ uint32_t slices[2][BLOCK_SIZE * SCREEN_YRES / 2];
+
+ int frame_id, sector_count;
+ int dropped_frames;
+ RECT slice_pos;
+ int frame_width;
+
+ volatile int8_t sector_pending, frame_ready;
+ volatile int8_t cur_frame, cur_slice;
+} StreamContext;
+
+StreamContext str_ctx;
+
+// This buffer is used by cd_sector_handler() as a temporary area for sectors
+// read from the CD. Due to DMA limitations it can't be allocated on the stack
+// (especially not in the interrupt callbacks' stack, whose size is very
+// limited).
+STR_Sector sector_buffer;
+
+void cd_sector_handler(void) {
+ // Fetch the .STR header of the sector that has been read and check if the
+ // end-of-file bit is set in the XA header.
+ CdGetSector(&sector_buffer, sizeof(STR_Sector) / 4);
+
+ if (
+ (sector_buffer.xa_header[0].submode & (1 << 7)) ||
+ (sector_buffer.xa_header[1].submode & (1 << 7))
+ ) {
+ CdControlF(CdlPause, 0);
+ str_ctx.frame_ready = -1;
+ return;
+ }
+
+ STR_Header *header = &sector_buffer.str_header;
+ StreamBuffer *frame = &str_ctx.frames[str_ctx.cur_frame];
+
+ // Ignore any non-MDEC sectors that might be present in the stream.
+ if (header->type != 0x8001)
+ return;
+
+ // If this sector is actually part of a new frame, validate the sectors
+ // that have been read so far and flip the bitstream data buffers.
+ if (header->frame_id != str_ctx.frame_id) {
+ // Do not set the ready flag if any sector has been missed.
+ if (str_ctx.sector_count)
+ str_ctx.dropped_frames++;
+ else
+ str_ctx.frame_ready = 1;
+
+ str_ctx.frame_id = header->frame_id;
+ str_ctx.sector_count = header->sector_count;
+ str_ctx.cur_frame ^= 1;
+
+ frame = &str_ctx.frames[str_ctx.cur_frame];
+
+ // Initialize the next frame. Dimensions must be rounded up to the
+ // nearest multiple of 16 as the MDEC operates on 16x16 pixel blocks.
+ frame->width = (header->width + 15) & 0xfff0;
+ frame->height = (header->height + 15) & 0xfff0;
+ }
+
+ // Append the payload contained in this sector to the current buffer.
+ memcpy(
+ &(frame->bs_data[2016 / 4 * header->sector_id]),
+ sector_buffer.data,
+ 2016
+ );
+ str_ctx.sector_count--;
+}
+
+void mdec_dma_handler(void) {
+ // Handle any sectors that were not processed by cd_event_handler() (see
+ // below) while a DMA transfer from the MDEC was in progress. As the MDEC
+ // has just finished decoding a slice, they can be safely handled now.
+ if (str_ctx.sector_pending) {
+ cd_sector_handler();
+ str_ctx.sector_pending = 0;
+ }
+
+ // Upload the decoded slice to VRAM and start decoding the next slice (into
+ // another buffer) if any.
+ LoadImage(&str_ctx.slice_pos, str_ctx.slices[str_ctx.cur_slice]);
+
+ str_ctx.cur_slice ^= 1;
+ str_ctx.slice_pos.x += BLOCK_SIZE;
+
+ if (str_ctx.slice_pos.x < str_ctx.frame_width)
+ DecDCTout(
+ str_ctx.slices[str_ctx.cur_slice],
+ BLOCK_SIZE * str_ctx.slice_pos.h / 2
+ );
+}
+
+void cd_event_handler(int event, uint8_t *payload) {
+ // Ignore all events other than a sector being ready.
+ if (event != CdlDataReady)
+ return;
+
+ // Only handle sectors immediately if the MDEC is not decoding a frame,
+ // otherwise defer handling to mdec_dma_handler(). This is a workaround for
+ // a hardware conflict between the DMA channels used for the CD drive and
+ // MDEC output, which shall not run simultaneously.
+ if (DecDCTinSync(1))
+ str_ctx.sector_pending = 1;
+ else
+ cd_sector_handler();
+}
+
+/* Stream helpers */
+
+void init_stream(void) {
+ EnterCriticalSection();
+ DMACallback(1, &mdec_dma_handler);
+ CdReadyCallback(&cd_event_handler);
+ ExitCriticalSection();
+
+ // Set the maximum amount of data DecDCTvlc() can output and copy the
+ // lookup table used for decompression to the scratchpad area. This is
+ // optional but makes the decompressor slightly faster. See the libpsxpress
+ // documentation for more details.
+ DecDCTvlcSize(0x8000);
+ DecDCTvlcCopyTable((DECDCTTAB *) 0x1f800000);
+
+ str_ctx.dropped_frames = 0;
+ str_ctx.cur_frame = 0;
+ str_ctx.cur_slice = 0;
+}
+
+StreamBuffer *get_next_frame(void) {
+ while (!str_ctx.frame_ready)
+ __asm__ volatile("");
+
+ if (str_ctx.frame_ready < 0)
+ return 0;
+
+ str_ctx.frame_ready = 0;
+ return &str_ctx.frames[str_ctx.cur_frame ^ 1];
+}
+
+void start_stream(CdlFILE *file) {
+ str_ctx.frame_id = -1;
+ str_ctx.sector_pending = 0;
+ str_ctx.frame_ready = 0;
+
+ CdSync(0, 0);
+
+ // Configure the CD drive to read 2340-byte sectors at 2x speed and to
+ // play any XA-ADPCM sectors that might be interleaved with the video data.
+ uint8_t mode = CdlModeSize | CdlModeRT | CdlModeSpeed;
+ CdControl(CdlSetmode, (const uint8_t *) &mode, 0);
+
+ // Start reading in real-time mode (i.e. without retrying in case of read
+ // errors) and wait for the first frame to be buffered.
+ CdControl(CdlReadS, &(file->pos), 0);
+
+ get_next_frame();
+}
+
+/* Main */
+
+static RenderContext ctx;
+
+#define SHOW_STATUS(...) { FntPrint(-1, __VA_ARGS__); FntFlush(-1); display(&ctx, 1); }
+#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();
+ InitGeom(); // Required for PSn00bSDK's DecDCTvlc()
+ DecDCTReset(0);
+
+ SHOW_STATUS("OPENING VIDEO FILE\n");
+
+ CdlFILE file;
+ if (!CdSearchFile(&file, "\\VIDEO.STR"))
+ SHOW_ERROR("FAILED TO FIND VIDEO.STR\n");
+
+ init_stream();
+ start_stream(&file);
+
+ // Disable framebuffer clearing to get rid of flickering during playback.
+ display(&ctx, 1);
+ ctx.db[0].draw.isbg = 0;
+ ctx.db[1].draw.isbg = 0;
+#ifdef DISP_24BPP
+ ctx.db[0].disp.isrgb24 = 1;
+ ctx.db[1].disp.isrgb24 = 1;
+#endif
+
+ int decode_errors = 0;
+
+ while (1) {
+ // Wait for a full frame to be read from the disc and decompress the
+ // bitstream into the format expected by the MDEC. If the video has
+ // ended, restart playback from the beginning.
+ StreamBuffer *frame = get_next_frame();
+ if (!frame) {
+ start_stream(&file);
+ continue;
+ }
+
+#ifdef DRAW_OVERLAY
+ // Measure CPU usage of the decompressor using the hblank counter.
+ int total_time = TIMER_VALUE(1) + 1;
+ TIMER_VALUE(1) = 0;
+#endif
+
+ if (DecDCTvlc(frame->bs_data, frame->mdec_data)) {
+ decode_errors++;
+ continue;
+ }
+
+#ifdef DRAW_OVERLAY
+ int cpu_usage = TIMER_VALUE(1) * 100 / total_time;
+#endif
+
+ // Wait for the MDEC to finish decoding the previous frame, then flip
+ // the framebuffers to display it and prepare the buffer for the next
+ // frame.
+ // NOTE: you should *not* call VSync(0) during playback, as the refresh
+ // rate of the GPU is not synced to the video's frame rate. If you want
+ // to minimize screen tearing, consider triple buffering instead (i.e.
+ // always keep 2 fully decoded frames in VRAM and use VSyncCallback()
+ // to register a function that displays the next decoded frame whenever
+ // vblank occurs).
+ DecDCTinSync(0);
+ DecDCToutSync(0);
+
+#ifdef DRAW_OVERLAY
+ FntPrint(-1, "FRAME:%5d READ ERRORS: %5d\n", str_ctx.frame_id, str_ctx.dropped_frames);
+ FntPrint(-1, "CPU: %5d%% DECODE ERRORS:%5d\n", cpu_usage, decode_errors);
+ FntFlush(-1);
+#endif
+ display(&ctx, 0);
+
+ // Feed the newly decompressed frame to the MDEC. The MDEC will not
+ // actually start decoding it until an output buffer is also configured
+ // by calling DecDCTout() (see below).
+#ifdef DISP_24BPP
+ DecDCTin(frame->mdec_data, DECDCT_MODE_24BPP);
+#else
+ DecDCTin(frame->mdec_data, DECDCT_MODE_16BPP);
+#endif
+
+ // Place the frame at the center of the currently active framebuffer
+ // and start decoding the first slice. Decoded slices will be uploaded
+ // to VRAM in the background by mdec_dma_handler().
+ RECT *fb_clip = &(ctx.db[ctx.db_active].draw.clip);
+ int x_offset = (fb_clip->w - frame->width) / 2;
+ int y_offset = (fb_clip->h - frame->height) / 2;
+
+ str_ctx.slice_pos.x = VRAM_X_COORD(fb_clip->x + x_offset);
+ str_ctx.slice_pos.y = fb_clip->y + y_offset;
+ str_ctx.slice_pos.w = BLOCK_SIZE;
+ str_ctx.slice_pos.h = frame->height;
+ str_ctx.frame_width = VRAM_X_COORD(frame->width);
+
+ DecDCTout(
+ str_ctx.slices[str_ctx.cur_slice],
+ BLOCK_SIZE * str_ctx.slice_pos.h / 2
+ );
+ }
+
+ return 0;
+}
diff --git a/examples/mdec/strvideo/system.cnf b/examples/mdec/strvideo/system.cnf
new file mode 100644
index 0000000..d199117
--- /dev/null
+++ b/examples/mdec/strvideo/system.cnf
@@ -0,0 +1,4 @@
+BOOT=cdrom:\strvideo.exe;1
+TCB=4
+EVENT=10
+STACK=801FFFF0