diff options
| author | spicyjpeg <88942473+spicyjpeg@users.noreply.github.com> | 2022-03-20 14:02:42 +0100 |
|---|---|---|
| committer | spicyjpeg <88942473+spicyjpeg@users.noreply.github.com> | 2022-03-20 14:02:42 +0100 |
| commit | 6c19e712e2588b52791f604feb31273acb074d41 (patch) | |
| tree | f864f22e4eb10b15e8c247d9eccf0113f54a62aa /examples/mdec | |
| parent | 4bbfe640a8c357137524e797a8d2bd0a94d3abfa (diff) | |
| download | psn00bsdk-6c19e712e2588b52791f604feb31273acb074d41.tar.gz | |
Add mdec/mdecimage example, psxpress fixes
Diffstat (limited to 'examples/mdec')
| -rw-r--r-- | examples/mdec/mdecimage/CMakeLists.txt | 20 | ||||
| -rw-r--r-- | examples/mdec/mdecimage/bunpattern.png | bin | 0 -> 59932 bytes | |||
| -rw-r--r-- | examples/mdec/mdecimage/encode_image.py | 218 | ||||
| -rw-r--r-- | examples/mdec/mdecimage/image.bin | bin | 0 -> 163072 bytes | |||
| -rw-r--r-- | examples/mdec/mdecimage/main.c | 82 |
5 files changed, 320 insertions, 0 deletions
diff --git a/examples/mdec/mdecimage/CMakeLists.txt b/examples/mdec/mdecimage/CMakeLists.txt new file mode 100644 index 0000000..b76adb4 --- /dev/null +++ b/examples/mdec/mdecimage/CMakeLists.txt @@ -0,0 +1,20 @@ +# PSn00bSDK example CMake script +# (C) 2021 spicyjpeg - MPL licensed + +cmake_minimum_required(VERSION 3.20) + +project( + mdecimage + LANGUAGES C ASM + VERSION 1.0.0 + DESCRIPTION "PSn00bSDK MDEC static image example" + HOMEPAGE_URL "http://lameguy64.net/?page=psn00bsdk" +) + +file(GLOB _sources *.c) +psn00bsdk_add_executable(mdecimage STATIC ${_sources}) +#psn00bsdk_add_cd_image(mdecimage_iso mdecimage iso.xml DEPENDS mdecimage) + +psn00bsdk_target_incbin(mdecimage PRIVATE mdec_image image.bin) + +install(FILES ${PROJECT_BINARY_DIR}/mdecimage.exe TYPE BIN) diff --git a/examples/mdec/mdecimage/bunpattern.png b/examples/mdec/mdecimage/bunpattern.png Binary files differnew file mode 100644 index 0000000..61524f8 --- /dev/null +++ b/examples/mdec/mdecimage/bunpattern.png diff --git a/examples/mdec/mdecimage/encode_image.py b/examples/mdec/mdecimage/encode_image.py new file mode 100644 index 0000000..3a5bcea --- /dev/null +++ b/examples/mdec/mdecimage/encode_image.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# Simple MDEC image encoder (requires PIL/Pillow and NumPy to be installed) +# (C) 2022 spicyjpeg - MPL licensed + +import math +from warnings import warn +from argparse import ArgumentParser, FileType + +import numpy +from PIL import Image + +LUMA_SCALE = 8 +CHROMA_SCALE = 16 + +## Tables + +ZIGZAG_TABLE = numpy.array(( + 0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63 +), numpy.uint8).argsort() + +# The default luma and chroma quantization table is based on the MPEG-1 +# quantization table, with the only difference being the first value (2 instead +# of 8). +QUANT_TABLE = numpy.array(( + 2, 16, 19, 22, 26, 27, 29, 34, + 16, 16, 22, 24, 27, 29, 34, 37, + 19, 22, 26, 27, 29, 34, 34, 38, + 22, 22, 26, 27, 29, 34, 37, 40, + 22, 26, 27, 29, 32, 35, 40, 48, + 26, 27, 29, 32, 35, 40, 48, 58, + 26, 27, 29, 34, 38, 46, 56, 69, + 27, 29, 35, 38, 46, 56, 69, 83 +), numpy.uint8).reshape(( 8, 8 )) + +S = [ math.cos((i or 4) / 16 * math.pi) / 2 for i in range(8) ] + +DCT_MATRIX = numpy.array(( + S[0], S[0], S[0], S[0], S[0], S[0], S[0], S[0], + S[1], S[3], S[5], S[7], -S[7], -S[5], -S[3], -S[1], + S[2], S[6], -S[6], -S[2], -S[2], -S[6], S[6], S[2], + S[3], -S[7], -S[1], -S[5], S[5], S[1], S[7], -S[3], + S[4], -S[4], -S[4], S[4], S[4], -S[4], -S[4], S[4], + S[5], -S[1], S[7], S[3], -S[3], -S[7], S[1], -S[5], + S[6], -S[2], S[2], -S[6], -S[6], S[2], -S[2], S[6], + S[7], -S[5], S[3], -S[1], S[1], -S[3], S[5], -S[7] +), numpy.float32).reshape(( 8, 8 )) + +## Helpers + +def to_int10(value): + clamped = min(max(int(value), -0x200), 0x1ff) + + return clamped + (0 if clamped >= 0 else 0x400) + +def rgb_to_ycbcr_planar(image): + scaled = image.astype(numpy.float32) / 255.0 + r, g, b = scaled.transpose(( 2, 0, 1 )) + + # https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion + y = 16 + r * 65.481 + g * 128.553 + b * 24.966 + cb = 128 - r * 37.797 - g * 74.203 + b * 112.000 + cr = 128 + r * 112.000 - g * 93.786 - b * 18.214 + + return y, cb, cr + +## Block encoder + +def encode_block(buffer, block, scale): + # Perform discrete cosine transform on the block, divide the coefficients by + # the quantization table and reorder them in zigzag order. + _block = block.astype(numpy.float32) - 128.0 + coeffs = (DCT_MATRIX @ _block @ DCT_MATRIX.T) / QUANT_TABLE + coeffs = coeffs.reshape(( 64, ))[ZIGZAG_TABLE] + + buffer[0] = (scale << 10) | to_int10(round(coeffs[0])) + offset = 1 + + # Divide the AC coefficients by the given quantization scale and encode them + # as run-length pairs by counting how many zeroes there are between each + # non-zero value. + ac_values = coeffs[1:] * 8.0 / scale + encoded = [] + run_length = 0 + + for ac in ac_values.round().astype(numpy.int32): + if ac: + buffer[offset] = (run_length << 10) | to_int10(ac) + offset += 1 + + run_length = 0 + else: + run_length += 1 + + # Flush any remaining zeroes. + if run_length: + buffer[offset] = (run_length - 1) << 10 + offset += 1 + + # Add 1 or 2 end-of-block codes depending on whether the number of 16-bit + # values output so far is odd or even. Some emulators will break if blocks + # are not 32-bit aligned. + buffer[offset] = 0xfe00 + offset += 1 + if offset % 2: + buffer[offset] = 0xfe00 + offset += 1 + + return offset + +def encode_macroblock(buffer, block, y_scale, c_scale): + #y, cb, cr = rgb_to_ycbcr_planar(block) + y, cb, cr = block.transpose(( 2, 0, 1 )) + offset = 0 + + # Split the macroblock into 6 monochrome 8x8 blocks (Cr, Cb at half + # resolution + Y1-4). The MDEC uses 4:2:0 chroma subsampling. + # TODO: use bilinear sampling instead of nearest-neighbor for chroma + offset += encode_block(buffer[offset:], cr[0:16:2, 0:16:2], c_scale) + offset += encode_block(buffer[offset:], cb[0:16:2, 0:16:2], c_scale) + offset += encode_block(buffer[offset:], y[0: 8, 0: 8], y_scale) + offset += encode_block(buffer[offset:], y[0: 8, 8:16], y_scale) + offset += encode_block(buffer[offset:], y[8:16, 0: 8], y_scale) + offset += encode_block(buffer[offset:], y[8:16, 8:16], y_scale) + + return offset + +## Main + +def get_args(): + parser = ArgumentParser( + description = "Generates uncompressed MDEC bitstream data from an image." + ) + parser.add_argument( + "input_file", + type = FileType("rb"), + help = "input image file" + ) + parser.add_argument( + "-o", "--output", + type = FileType("wb"), + default = "image.bin", + help = "where to output converted image data (image.bin by default)", + metavar = "file" + ) + parser.add_argument( + "-m", "--monochrome", + action = "store_true", + help = "encode image as monochrome (8x8 blocks) instead of color (16x16 macroblocks)" + ) + parser.add_argument( + "-y", "--luma", + type = int, + default = LUMA_SCALE, + help = f"quantization scale for luma/monochrome blocks (0-63, default {LUMA_SCALE})", + metavar = "scale" + ) + parser.add_argument( + "-c", "--chroma", + type = int, + default = CHROMA_SCALE, + help = f"quantization scale for chroma blocks (0-63, default {CHROMA_SCALE})", + metavar = "scale" + ) + + return parser.parse_args() + +def main(): + args = get_args() + if args.luma < 0 or args.luma > 63: + raise ValueError("luma quantization scale must be in 0-63 range") + if args.chroma < 0 or args.chroma > 63: + raise ValueError("chroma quantization scale must be in 0-63 range") + + image = Image.open(args.input_file, "r") + data = numpy.array(image.convert("YCbCr"), numpy.uint8) + size = 8 if args.monochrome else 16 + + if image.width % size: + warn(RuntimeWarning(f"image width is not a multiple of {size}, trimming")) + if image.height % size: + warn(RuntimeWarning(f"image height is not a multiple of {size}, trimming")) + + # Preallocate 1 MB for the converted image data (faster than expanding an + # array dynamically -- this script is too slow already). + buffer = numpy.empty(0x80000, numpy.uint16) + offset = 0 + + # Split the image into 8x8 or 16x16 blocks and encode them in column-major + # order. + for x in range(0, image.width, size): + for y in range(0, image.height, size): + block = data[y:(y + size), x:(x + size)] + + if args.monochrome: + offset += encode_block(buffer[offset:], block[:, :, 0], args.luma) + else: + offset += encode_macroblock(buffer[offset:], block, args.luma, args.chroma) + + # Pad the generated data to the size of a DMA chunk (32x 32-bit words or + # 128 bytes). + length = (offset + 63) & 0xffffffc0 + buffer[offset:length] = 0xfe00 + + if length > (0xffff * 2): + warn(RuntimeWarning("image is too large to be decoded with a single DecDCTin() call")) + + with args.output as _file: + buffer[0:length].tofile(_file) + +if __name__ == "__main__": + main() diff --git a/examples/mdec/mdecimage/image.bin b/examples/mdec/mdecimage/image.bin Binary files differnew file mode 100644 index 0000000..976b4b6 --- /dev/null +++ b/examples/mdec/mdecimage/image.bin diff --git a/examples/mdec/mdecimage/main.c b/examples/mdec/mdecimage/main.c new file mode 100644 index 0000000..b59fdaf --- /dev/null +++ b/examples/mdec/mdecimage/main.c @@ -0,0 +1,82 @@ +/* + * PSn00bSDK MDEC static image example + * (C) 2022 spicyjpeg - MPL licensed + * + * This is a modified version of the graphics/rgb24 example showing how to feed + * run-length encoded data into the MDEC and retrieve a decoded 24bpp image. To + * keep the example simple no additional compression is applied (usually MDEC + * data would be Huffman encoded to save more space, with the initial + * decompression being done in software). A Python script is included to encode + * an image into the format expected by the MDEC; quality and file size can be + * tweaked by changing the quantization scales with the -y and -c arguments. + * + * Using the MDEC to decode static images can be useful for e.g. menu + * backgrounds or loading screens, where smaller file sizes are desirable even + * if quality is sacrificed. + */ + +#include <stdint.h> +#include <stddef.h> +#include <psxgpu.h> +#include <psxpress.h> +#include <hwregs_c.h> + +extern const uint32_t mdec_image[]; +extern const size_t mdec_image_size; + +#define SCREEN_XRES 640 +#define SCREEN_YRES 480 + +//#define BLOCK_SIZE 8 // Monochrome (8x8), 15bpp display +//#define BLOCK_SIZE 12 // Monochrome (8x8), 24bpp display +//#define BLOCK_SIZE 16 // Color (16x16), 15bpp display +#define BLOCK_SIZE 24 // Color (16x16), 24bpp display + +int main(int argc, const char* argv[]) { + DISPENV disp; + + ResetGraph(0); + DecDCTReset(0); + + // Set up the GPU for 640x480 interlaced 24bpp output. + SetDefDispEnv(&disp, 0, 0, SCREEN_XRES, SCREEN_YRES); + disp.isrgb24 = 1; + disp.isinter = 1; + + PutDispEnv(&disp); + SetDispMask(1); + + // Start feeding image data to the MDEC. This doesn't immediately start the + // decoding, instead the MDEC will wait until a destination buffer is also + // set up. + MDEC0 = 0x30000000 | (mdec_image_size / 4); // 0x38000000 for 15bpp + DecDCTinRaw(mdec_image, mdec_image_size / 4); + + // Fetch decoded data from the MDEC in vertical 8x480 or 16x480 "slices". + // This is necessary as the MDEC doesn't buffer an entire frame but only + // returns a series of square macroblocks, which can't be placed into VRAM + // with a single LoadImage() call. + //for (uint32_t x = 0; x < SCREEN_XRES; x += BLOCK_SIZE) { // 15bpp + for (uint32_t x = 0; x < (SCREEN_XRES * 3 / 2); x += BLOCK_SIZE) { // 24bpp + RECT rect; + uint32_t slice[BLOCK_SIZE * SCREEN_YRES / 2]; + + rect.x = x; + rect.y = 0; + rect.w = BLOCK_SIZE; + rect.h = SCREEN_YRES; + + // Configure the MDEC to output to the slice buffer and let it finish + // decoding a slice, then upload it to the framebuffer. + DecDCTout(slice, BLOCK_SIZE * SCREEN_YRES / 2); + DecDCToutSync(0); + + LoadImage(&rect, (u_long *) slice); + DrawSync(0); + } + + for (;;) + __asm__ volatile(""); + + return 0; +} |
