Introduction

Welcome to the Rust on Sony PlayStation Vita Book: An introductory book about using the Rust Programming Language on Sony PlayStation Vita.

Here you can find guidelines on how to start making applications for Sony PlayStation Vita in Rust with std using open source Vita SDK homebrew toolchain.

Considerations

Though it's technically possible to run nostd applications written in pure Rust on Vita (without any additional toolchains), this book focuses on running applications with std. Rust std for Vita target is implemented via newlib, and relies on the Vita SDK toolchain - patched gcc compiler, patched binutils, as well as a number of CLI tools that allow converting an armv7 elf into an artifact runnable on Vita.

Getting started

Requirements

Vita is a tier 3 target with std available when compiled from source by the user.

To build Rust application for Sony PlayStation Vita you need the following installed:

  • Rust nightly (since building std is an unstable feature)
  • Vita SDK (for std and tools to build a runnable artifact)
  • cargo-vita (to simplify the process of building runnable artifacts)

Cargo-vita uses Vita SDK toolchain for compilation, and requires you to set VITASDK environment variable to your Vita SDK installation path.

Also, you will have to add the following section to your projects Cargo.toml:

[package.metadata.vita]
# A unique identifier for your project. 9 chars, alphanumeric.
title_id = "RUSTAPP01"
# A title that will be shown on a bubble. Optional, will take the crate name as the default
title_name = "My application"
# Optional. A path to static files relative to the project.
assets = "static"
# Optional, this is the default
build_std = "std,panic_unwind"
# Optional, this is the default
vita_strip_flags = ["-g"]
# Optional, this is the default
vita_make_fself_flags = ["-s"]
# Optional, this is the default
vita_mksfoex_flags = ["-d", "ATTRIBUTE2=12"]

Optional tools

Optionally you may want to have a number of tools installed to simplify the development and debugging process.

Building

From here on you can write your code the same way you would write it for any other platform!

use std::time::Duration;

pub fn main() {
    std::thread::sleep(Duration::from_secs(10))
}

The usual final output for the Vita target is a vpk file. A vpk file is essentially a zip archive with your applications binary, resources and a manifest for Vita bubbles.

A process of building a vpk involves a number of steps:

  1. Building a usual eld with Rust std. The standard library can be compiled in rust using an unstable flag:
    cargo build -Z build-std=std,panic_abort --target=armv7-sony-vita-newlibeabihf --release
    
  2. (Optional) Stripping unnecessary symbols from the binary to shrink it's size:
    arm-vita-eabi-strip -g your-binary.elf
    
  3. Buildin a velf from your elf. This can be done using a tool from Vita SDK toolchain:
    vita-elf-create your-binary.elf your-binary.velf
    
  4. Create an eboot.bin from the velf. This can be done using a tool from Vita SDK toolchain:
    vita-make-fself -s your-binary.velf eboot.bin
    
  5. Pack the eboot.bin, assets and a manifest into a vpk file. This can be done using vita-mksfoex and vita-pack-vpk tools from Vita SDK toolchain.

All these steps can be by using cargo-vita:

# Build a elf
cargo vita build elf {{all arguments here are passed directly to cargo build}}
# Build a velf. Implicitly executes `cargo vita build elf`
cargo vita build velf {{all arguments here are passed directly to cargo build}}
# Build a eboot.bin. Implicitly executes `cargo vita build velf`
cargo vita build eboot {{all arguments here are passed directly to cargo build}}
# Build a eboot.bin. Implicitly executes `cargo vita build eboot`
cargo vita build vpk {{all arguments here are passed directly to cargo build}}

Just like the usual cargo build this tool will also build all workspace crates, unless a specific project is specified as a CLI flag. Here are some more examples:

# Build all workspace crates with --release flag, and upload the resulting vpk files to ux0:/download/
cargo build vpk --upload --release
# Build std tests from examples
cargo vita build vpk --release --package vita-std-tests --tests
# Build a eboot for an already installed vpk, upload it to Vita, and run the application
cargo vita build eboot --update --run --release --package vita-example-http
# Build test suite named socket in a project that does not have [package.metadata.vita] section in Cargo.toml
cargo vita build --default-title-id=RUST00001 eboot --update --run --test socket --features all

Optimizations

To reduce the size of the resulting artifacts, prefer using release builds, and add the following to your root Cargo.toml:

[profile.release]
panic = 'abort'
lto = true
opt-level = 3

Linking native dependencies

Currently newlib on Vita does not support dynamic linking. Any native dependency you may want to use must be statically linked. This can be done in a number of ways.

You can declare them in your code:

#![allow(unused)]
fn main() {
#[link(name = "mathneon", kind = "static")]
extern "C" {}
}

Or you can define them in build.rs:


fn main() {
    println!("cargo:rustc-link-lib=mathneon");
}

Building in a CI pipeline

If you want to build your vpk in a CI pipeline, you can use a ghcr.io/vita-rust/vitasdk-rs docker image. This image is based on alpine, and contains Vita SDK, Rust nightly and cargo-vita.

The image is rebuilt automatically by a Github Actions pipeline with the latest dependencies on a weekly basis.

Development

To support platform dependent aspects of your application, such as input, audio and graphics, you will have to use User and Kernel functions exported by Vita modules. These functions become available when linking against the appropriate stubs provided by Vita SDK. The descriptions of the exported data structures and functions can be found in Vita SDK docs.

To use them in Rust, you must use vitasdk-sys. This crate uses bindgen to generate Rust bindings from C headers. The exported functions are grouped by their stubs and become available after enabling the corresponding feature in vitasdk-sys. The documentation can be found in vitasdk-sys docs.

Memory

Sony PlayStation Vita SoC has two memory dies:

  • 512 MiB of LPDDR2 DRAM (which in allocations is further separated into MAIN, CDLG and PHYCONT)
  • 128 MiB of CDRAM, usually used as video memory

Budget

Not all of this memory would usually be available for your application though (see more about the Memory Budget). By default your application has access only to 256 MiB of MAIN memory, 112 MiB of CDRAM, 26 MiB of PHYCONT and around 8 MiB of CDLG.

It is possible to increase the default MAIN memory budget to 365 MiB by passing "-d ATTRIBUTE2=12 to vita-mksfoex tool of Vita SDK when building a vpk. If you are using cargo-vita tool to build your project, this flag is set by default, and can be overriden by setting the following in your projects Cargo.toml:

[package.metadata.vita]
# ...
# Disables extended memory budget
vita_mksfoex_flags = []
# ...

Stack

Stack is allocated on MAIN memory. The stack size for a main thread is 4 KiB. To change the stack size of your main thread, export the following static variable:

#![allow(unused)]
fn main() {
#[used]
#[export_name = "sceUserMainThreadStackSize"]
pub static SCE_USER_MAIN_THREAD_STACK_SIZE: u32 = 1 * 1024 * 1024; // 1 MiB
}

When creating an new thread, you can set the stack size of it as usual:

pub fn main() {
    std::thread::Builder::new()
        .stack_size(1 * 1024 * 1024) // 1 MiB
        .spawn(move || todo!("work"))
        .expect("Unable to spawn thread")
        .join()
        .expect("Unable to join thread");
}

Heap

The default System allocator in Rust will use newlib heap. This heap pre-allocates a memory block of a maximum heap size and never resizes. All allocation happen within this pre-allocated memory block in MAIN memory.

The default newlib heap size is 128 MiB. You can set a custom heap size by exporting the following static variable:

#![allow(unused)]
fn main() {
#[used]
#[export_name = "_newlib_heap_size_user"]
pub static _NEWLIB_HEAP_SIZE_USER: u32 = 256 * 1024 * 1024; // 256 MiB
}

Considerations

Besides the newlib heap, if you are linking against native libraries or using syscalls of various modules provided by stubs, they may do custom allocations with sceKernelAllocMemBlock syscall.

Some of them may also be using sceLibc heap, which is another heap allocated on MAIN memory. If you experience OOM errors in such cases, you can resize the sceLibc heap size:

#![allow(unused)]
fn main() {
#[used]
#[export_name = "sceLibcHeapSize"]
pub static SCE_LIBC_HEAP_SIZE: u32 = 10 * 1024 * 1024; // 10 MiB
}

Keep in mind that stack, newlib heap and sceLibc heap are all located in MAIN memory, so their sum should not exceed the allowed memory budget.

Manual allocation

In some cases (e.g when implementing a custom rasterizer with framebuffers), you may want to allocate memory outside of the heap or MAIN memory.

The low-level way to allocate memory on Sony PlayStation Vita is by using a sceKernelAllocMemBlock syscall

Here is an example of it may be used:

#![allow(unused)]
fn main() {
use vitasdk_sys::*;

pub struct Buffer {
    buf: *mut c_void,
    block_uid: SceUID,
    size: u32,
}

impl Buffer {
    fn new(size: u32) -> Buffer {
        let mut buf: *mut c_void = ::core::ptr::null_mut();

        // The minimal allocation unit is a memory page
        let unit = 0x40000 - 1;
        let size = (size + unit) & !unit;

        let block_uid = unsafe {
            let block_uid: SceUID = sceKernelAllocMemBlock(
                b"display\0".as_ptr() as *const c_char,
                SCE_KERNEL_MEMBLOCK_TYPE_USER_CDRAM_RW,
                size,
                ::core::ptr::null_mut(),
            );
            sceKernelGetMemBlockBase(block_uid, &mut buf);
            block_uid
        };

        Buffer {
            buf: buf as _,
            size,
            block_uid,
        }
    }
}

impl Drop for Buffer {
    fn drop(&mut self) {
        unsafe {
            sceKernelFreeMemBlock(self.block_uid);
        }
    }
}
}

Network

Sony PlayStation Vita has a support for IPv4 stack (IPv6 is not supported). Network API is exposed via usual POSIX socket API by newlib.

Most of the existing popular libraries for should already either support Vita target, or it should be fairly trivial to port them by enabling it with conditional checks, and disabling unsupported parts.

Non-blocking sockets

In POSIX the usual way to make a socket non-blocking, or check if it is non-blocking is by using fcntl syscall. Currently this syscall is not implemented in newlib.

When porting network libraries for Vita, to change sockets non-blocking flag, or get this flags value, use setsockopt and getsockopt syscalls:

#![allow(unused)]
fn main() {
fn nonblocking(fd: libc::c_int) -> bool {
    let mut non_block: libc::c_int = 0;
    let mut len: libc::socklen_t = 0;
    unsafe {
        libc::getsockopt(
            fd,
            libc::SOL_SOCKET,
            libc::SO_NONBLOCK,
            &mut nonblock as *mut libc::c_int as _,
            &mut len as _,
        );
    }

    non_block != 0
}

fn set_nonblocking(fd: libc::c_int, non_block: bool) {
    let non_block = non_block as libc::c_int;
    unsafe {
        libc::setsockopt(
            fd,
            libc::SOL_SOCKET,
            libc::SO_NONBLOCK,
            &non_block as *const libc::c_int as _,
            std::mem::size_of::<libc::c_int>() as libc::socklen_t,
        );
    }
}
}

HTTPS

In rust ecosystem when using TLS you usually can choose between two implementations - OpenSSL and rustls.

Vita does not natively provide OpenSSL and instead has it's own API for TLS. But the usual installation of Vita SDK provides OpenSSL implementation which statically be linked to.

So in a nutshell, both choices should work on Vita.

There is an inconvenience with CA certificates though. Usually on a hacked Vita you would have iTLS-Enso installed for the latest CA certificates. These certificates are only available for the native TLS api. And the native TLS API provided by Vita does not have a way to export the CA certificates themselves. The certificate file is located on the filesystem on vs0:data/external/cert/CA_LIST.cer, but the vs0 partition is not available for the safe applications.

In practice this means the following:

  • You can use rustls with webpki-roots crate to provide CA certificates. The certificates will be then bundled inside of your binary.

  • Use either rustls with rustls-native-certs or native-tls crate for OpenSSL implementation. In both the scenarios your application will try to find CA certificated provided by the operating system.

    You will have to set a SSL_CERT_FILE environment variable in your code:

    fn main() {
        std::env::set_var("SSL_CERT_FILE", "vs0:data/external/cert/CA_LIST.cer");
        // ... your code
    }

    as well as make your application unsafe in order for it to have access to the vs0 partition. To do that add the following to your Cargo.toml

    [package.metadata.vita]
    # ...
    # This disables safe mode (default -s flag) for reading OpenSSl certs
    vita_make_fself_flags = []
    

Graphics

There are multiple ways you can approach graphics programming on Vita.

Approaches

Native

You can use the native Vita modules for graphics programming. You can find the documentation for the API in Vita SDK docs and the bindings to the syscalls in vitasdk-sys crate.

SDL2

The easiest way to start working with SDL2 in Rust would be using rust-sdl2 crate, which fully works on Vita without any additional work.

Keep in mind that graphics API provided by SDL2 is rudimentary and suitable mostly for simple 2D games. If you need something more complicated, you probably want to go the OpenGL route.

OpenGL ES2

There exist two homebrew implementations for OpenGL. Keep in mind that probably neither of the solutions supports the full OpenGL ES2 spec, and may be limited by the capabilities of Vita GPU.

vitaGL

VitaGL provides OpenGL ES2 compatible API by using native GXM API internally and vitaShaRK for shader compilation.

It is possible to use vitaGL together with SDL2 as an abstraction layer for input, but the SDL2 library provided by Vita SDK does not support vitaGL, so for you will have to use this SDL2 fork.

PVR_PSP2

TODO PVR_PSP2

Running

After you have built a vpk file, you have two options to run it:

  • On a hacked Sony PlayStation Vita device
  • On a Vita3K emulator

Running on the real hardware

If you are going to test your application on physical hardware you first need to deliver and install your vpk on your Vita.

This can be either:

Running on the emulator

After installing the emulator and setting it up correctly, installing and running your vpk is as simple as:

# Optionally remove the previous vpk with your title_id
vita3k -d RUST00001
# Installs the vpk and runs it
vita3k ./target/armv7-sony-vita-newlibeabihf/release/your_app.vpk

Keep in mind that an emulator is not a real device, and some programs that would work on real hardware won't work in an emulator, and vice versa.

Unsupported features

Some std features are not supported on Vita target:

  • std::process