Skip to content

About

prs_rs is an acceptably fast, barebones implementation of the SEGA PRS encoding scheme.

It can compress, decompress and calculate the decompressed size of PRS encoded data.

Usage

These examples use Nightly Rust to initalize uninitialized memory

You can replicate with Stable Rust by doing something like this.

let layout = Layout::from_size_align(decompressed_len, size_of::<usize>()).unwrap();
let raw_ptr = unsafe { alloc(layout) };
let mut decompressed_data = unsafe { Box::from_raw(slice::from_raw_parts_mut(raw_ptr, decompressed_len)) };

Compress Data

let src: &[u8] = b"Your data here";

// Calculate max buffer size needed for compression
let max_comp_len = prs_calculate_max_compressed_size(src_len);

// Allocate enough memory for the compressed data
let mut decompressed_data = Box::<[u8]>::new_uninit_slice(max_comp_len);
let bytes_written = prs_compress_unsafe(src.as_ptr(), src.len(), dest.as_mut_ptr() as *mut u8);

// Tell Rust our memory is initialized and trim the returned slice.
let dest = dest.assume_init(); 
let compressed_data: &[u8] = &dest[..bytes_written];

This API accepts slices or raw pointers as destination.

Decompress Data

let compressed_data: &[u8] = &[]; // some data
let mut decompressed_data = Box::<[u8]>::new_uninit_slice(decompressed_len);
let decompressed_size = prs_rs::decomp::prs_decompress_unsafe(compressed_data.as_ptr(), decompressed_data.as_mut_ptr() as *mut u8);
let dest = decompressed_data.assume_init();

Calculate Decompressed Size

If you need to calculate the size of the decompressed data without actually decompressing it:

unsafe {
    let compressed_data: &[u8] = &[];
    let decompressed_size = prs_calculate_decompressed_size(compressed_data.as_ptr());
}

You can get header, static libraries and dynamic libraries in the Releases section.

AI Generated, if this is inaccurate, please send a PR fixing this.

Compress Data

unsigned char src[] = "Your data here";
size_t src_len = sizeof(src);
size_t max_compressed_size = prs_calculate_max_compressed_size(src_len);
unsigned char* dest = (unsigned char*)malloc(max_compressed_size); // Dynamically allocate memory
size_t bytes_written = prs_compress(src, dest, src_len);

Decompress Data

Decompressing data requires ensuring the destination buffer is adequately sized.

// Assuming `compressed_data` are available
unsigned char* compressed_data; // Placeholder for compressed data pointer

// Can also get decompressed size from e.g. archive header.
size_t decompressed_size = prs_calculate_decompressed_size(compressed_data);
unsigned char* dest = (unsigned char*)malloc(decompressed_size);
size_t actual_decompressed_size = prs_decompress(compressed_data, dest);

Calculate Decompressed Size

If you need to calculate the size of the decompressed data without actually decompressing it:

decompressed_size = prs_calculate_decompressed_size(compressed_data_ptr);

Published on NuGet as prs_rs.Net.Sys.

The .NET library only provides raw bindings to the C exports, if you want more idiomatic .NET APIs, please make another library on top of this one.

Below are some usage examples.

Compress Data

public static unsafe Span<byte> CompressData(Span<byte> sourceData)
{
    fixed (byte* srcPtr = sourceData)
    {
        // Get the maximum possible size of the compressed data
        nuint maxCompressedSize = NativeMethods.prs_calculate_max_compressed_size((nuint)sourceData.Length);
        byte[] dest = GC.AllocateUninitializedArray<byte>((int)maxCompressedSize);
        fixed (byte* destPtr = &dest[0])
        {
            nuint compressedSize = NativeMethods.prs_compress(srcPtr, destPtr, (nuint)sourceData.Length);
            return dest.AsSpan(0..(int)compressedSize);
        }
    }
}

Decompress Data

Decompressing data requires ensuring the destination buffer is adequately sized.

public static unsafe Span<byte> DecompressData(Span<byte> compressedData)
{
  // Calculate the decompressed size to allocate enough memory
  fixed (byte* srcPtr = compressedData)
  {
      // or get from file header etc.
      nuint decompressedSize = NativeMethods.prs_calculate_decompressed_size(srcPtr); 
      byte[] dest = GC.AllocateUninitializedArray<byte>((int)decompressedSize);
      fixed (byte* destPtr = &dest[0])
      {
          nuint actualDecompressedSize = NativeMethods.prs_decompress(srcPtr, destPtr);
          return dest.AsSpan(0..(int)decompressedSize);
      }
  }
}

Calculate Decompressed Size

If you need to calculate the size of the decompressed data without actually decompressing it:

public static unsafe nuint DecompressData(Span<byte> compressedData)
{
  // Calculate the decompressed size to allocate enough memory
  fixed (byte* srcPtr = compressedData)
  {
      // or get from file header etc.
      return NativeMethods.prs_calculate_decompressed_size(srcPtr); 
  }
}

Reference Performance Numbers

System Info

  • Library Version: 0.1.0 (07 Feb 2024)
  • CPU: AMD Ryzen 9 5900X (12C/24T)
  • RAM: 32GB DDR4-3000 (16-17-17-35)
  • OS: Archlinux

The following reference numbers are based on PGO optimized builds of the library.

Performance numbers greatly depend on nature of input data. Data with long repeating padding compresses quicker.

All numbers are for single threaded operations.

Estimate (Determine size of data to decompress):

  • 1.4-8.2 GiB/s.

Decompress (Decompress):

  • 0.89-1.9 GiB/s

Compress:

  • 19-350 MiB/s (Average: ~38MiB/s)

Memory Usage

Currently as of 1.0.4, the compressor uses a flat amount of memory. This is:

  • 1.4MB on 32-bit platforms
  • 2.4MB on 64-bit platforms

See compress.rs for more details.

Important Note

I'm not a compression expert, I just used some brain cells, poked around the web a bit, and tried to come up with a solution that is 'good enough'.

The compression is also optimal, i.e. it will generate the smallest possible PRS file for the given input data.

Technical Questions

If you have questions/bug reports/etc. feel free to Open an Issue.

Happy Documenting ❤️