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 ❤️