tutorial:framebuffer_graphics

Framebuffer Graphics

This tutorial covers the simplest possible way to draw graphics on the PSP without necessarily going into detail on working with the GU pipeline and the complexities of 3D graphics. We'll be implementing a very simple software 2D renderer with double buffering for smooth rendering.

Your screen is made of pixels. An array of pixels actually, with each pixel containing data for the color displayed. In the most common format for images (RGBA8888), we specify the Red, Green, Blue, and Alpha components with 8 bits each. This means the color of each pixel can be represented as a u32 (4×8 = 32 bits). Now put those u32s in an array and we would have what's called a framebuffer - where we create a SCREEN WIDTH x SCREEN HEIGHT buffer of pixels (u32s). The framebuffer is usually stored in Video RAM on most devices including the PSP. You can write directly to this frame buffer and draw on the screen using a few pointers and simple code - but that's not quite enough… In this model, this is called single-buffer rendering, where you overwrite the last frame as you draw it - which is full of flickering and visual artifacts and isn't very friendly to users. The other way we can do this is through double-buffer rendering, where you draw to a separate buffer and then once rendering is done, you swap the buffers and display all at once. In this way, we have a much smoother experience.

Our basic API here will implement a few simple concepts - an initialization of some sort, a clear function that blanks the screen to a color, a swap buffers function for the end of drawing, and a function that draws a rectangle with a size and bounds and a color. Initialization should set up our two buffers and set up the screen for drawing. The clear function is a simple loop that sets all pixels in the buffer to a color. The swap buffers function swaps the array pointers and sets the screen to update from the other frame buffer. Finally, the draw rectangle method needs to do a few bounds checks on where we're drawing to, and then iterate over that patch and set the colors in order to draw onto the screen.

gfx.h

#include <stdint.h>
 
void GFX_init();
void GFX_clear(uint32_t color);
void GFX_swap_buffers();
void GFX_draw_rect(unsigned int x, unsigned int y, unsigned int w, unsigned int h, uint32_t color);

gfx.c

#include "gfx.h"
#include <pspge.h>
#include <pspdisplay.h>
#include <psputils.h>
 
uint32_t* draw_buffer;
uint32_t* disp_buffer;
 
 
void GFX_init() {
    draw_buffer = sceGeEdramGetAddr();
    disp_buffer = (uint32_t*)sceGeEdramGetAddr() + (272 * 512 * 4);
 
    sceDisplaySetMode(0, 480, 272);
    sceDisplaySetFrameBuf(disp_buffer, 512, PSP_DISPLAY_PIXEL_FORMAT_8888, 1);
}
 
void GFX_clear(uint32_t color) {
    for(int i = 0; i < 512 * 272; i++) {
        draw_buffer[i] = color;
    }
}
 
void GFX_swap_buffers() {
    uint32_t* temp = disp_buffer;
    disp_buffer = draw_buffer;
    draw_buffer = temp;
 
    sceKernelDcacheWritebackInvalidateAll();
    sceDisplaySetFrameBuf(disp_buffer, 512, PSP_DISPLAY_PIXEL_FORMAT_8888, PSP_DISPLAY_SETBUF_NEXTFRAME);
}
 
void GFX_draw_rect(unsigned int x, unsigned int y, unsigned int w, unsigned int h, uint32_t color) {
    if(x > 480) {
        x = 480;
    }
    if(y > 480) {
        y = 480;
    }
 
    if(x + w > 480){
        w = 480 - x;
    }
 
    if(y + h > 272){
        h = 272 - y;
    }
 
    int off = x + (y * 512);
    for(int y1 = 0; y1 < h; y1++) {
        for(int x1 = 0; x1 < w; x1++) {
            draw_buffer[x1 + off + y1 * 512] = color;
        }
    }
}

gfx.hpp

#include <cstdint>
 
namespace GFX{
    void init();
    void clear(uint32_t color);
    void swapBuffers();
    void drawRect(unsigned int x, unsigned int y, unsigned int w, unsigned int h, uint32_t color);
}

gfx.cpp

#include "gfx.hpp"
#include <pspge.h>
#include <pspdisplay.h>
#include <psputils.h>
 
namespace GFX{
    uint32_t* draw_buffer;
    uint32_t* disp_buffer;
 
    void init() {
        //Stronger casts here as is required.
        draw_buffer = static_cast<uint32_t*>(sceGeEdramGetAddr());
        disp_buffer = static_cast<uint32_t*>(sceGeEdramGetAddr()) + (272 * 512 * 4);
 
        sceDisplaySetMode(0, 480, 272);
        sceDisplaySetFrameBuf(disp_buffer, 512, PSP_DISPLAY_PIXEL_FORMAT_8888, 1);
    }
 
    void clear(uint32_t color) {
        /*Same as C*/
    }
 
    void swapBuffers() {
        /*Same as C*/
    }
 
    void drawRect(unsigned int x, unsigned int y, unsigned int w, unsigned int h, uint32_t color) {
        /*Same as C*/
    }
}

main.c

/**
 * Same as before.
**/
int main() {
    setupCallbacks();
    GFX_init();
 
    while(1) {
        GFX_clear(0xFFFFCA82); //#82CAFFFF RGBA in Hex -> 0xFFFFCA82
 
        GFX_draw_rect(10, 10, 30, 30, 0xFF00FFFF);
 
        GFX_swap_buffers();
        sceDisplayWaitVblankStart();
    }
}

main.cpp

auto main() -> int {
    setupCallbacks();
    GFX::init();
 
    while(1) {
        GFX::clear(0xFFFFCA82); //#82CAFFFF RGBA in Hex -> 0xFFFFCA82
 
        GFX::drawRect(10, 10, 30, 30, 0xFF00FFFF);
 
        GFX::swapBuffers();
        sceDisplayWaitVblankStart();
    }
}

One thing you'll notice is that there is C++ and C code in this example - and primarily this is due to small differences. C has no concept of a namespace or a module, and therefore you need to specify a prefix like GFX_ whereas C++ has explicit namespaces. The code otherwise is identical between the two with the exception of static_cast<> for C++ casting where C normally coerces types. As stated earlier in the General Theory, all of the functions are rather simple and follow the same structure as the API proposal.

gfx.rs

use psp::sys::{DisplayMode, DisplayPixelFormat, DisplaySetBufSync};
 
pub struct Renderer {
    draw_buffer: *mut u32,
    disp_buffer: *mut u32,
}
 
impl Renderer {
    pub unsafe fn new() -> Self {
        let draw_buffer = psp::sys::sceGeEdramGetAddr() as *mut u32;
        let disp_buffer = psp::sys::sceGeEdramGetAddr().add(512 * 272 * 4) as *mut u32;
 
        psp::sys::sceDisplaySetMode(DisplayMode::Lcd, 480, 272);
        psp::sys::sceDisplaySetFrameBuf(
            disp_buffer as *const u8,
            512,
            DisplayPixelFormat::Psm8888,
            DisplaySetBufSync::NextFrame,
        );
 
        Self {
            draw_buffer,
            disp_buffer,
        }
    }
 
    pub fn clear(&self, color: u32) {
        unsafe {
            for i in 0..512 * 272 {
                *self.draw_buffer.add(i as usize) = color;
            }
        }
    }
 
    pub fn swap_buffers(&mut self) {
        core::mem::swap(&mut self.disp_buffer, &mut self.draw_buffer);
 
        unsafe {
            psp::sys::sceKernelDcacheWritebackInvalidateAll();
            psp::sys::sceDisplaySetFrameBuf(
                self.disp_buffer as *const u8,
                512,
                DisplayPixelFormat::Psm8888,
                DisplaySetBufSync::NextFrame,
            );
        }
    }
 
    pub fn draw_rect(&self, x: usize, y: usize, w: usize, h: usize, color: u32) {
        for y1 in 0..h {
            for x1 in 0..w {
                if let Some(ptr) = self.calculate_offset(x + x1, y + y1) {
                    unsafe {
                        *ptr = color;
                    }
                }
            }
        }
    }
 
    #[inline]
    fn calculate_offset(&self, x: usize, y: usize) -> Option<*mut u32> {
        unsafe {
            if x <= 480 && y <= 272 {
                Some(self.draw_buffer.add(x + y * 512) as *mut u32)
            } else {
                None
            }
        }
    }
}

main.rs

#![no_std]
#![no_main]
 
psp::module!("Tutorial", 1, 0);
 
mod gfx;
 
pub fn psp_main() {
    psp::enable_home_button();
 
    unsafe{
        let mut renderer = gfx::Renderer::new();
 
        loop {
            renderer.clear(0xFFFFCA82);
            renderer.draw_rect(10, 10, 30, 30, 0xFF00FFFF);
 
            renderer.swap_buffers();
            psp::sys::sceDisplayWaitVblankStart();
        }
    }
}

The Rust Example here takes a bit of a change from the other examples. Instead of a global module, we create a struct with methods to encapsulate our renderer and do it that way. The renderer instantiation should only occur once and it is the burden of the programmer to make sure it is only created once. In this case the instantiation method is new(); since we are creating a struct object rather than initializing a global module. In this code, the bounds checks are performed via an inline function to calculate the pointer offset to a given coordinate.

gfx.zig

usingnamespace @import("Zig-PSP/src/psp/include/pspge.zig");
usingnamespace @import("Zig-PSP/src/psp/include/psputils.zig");
usingnamespace @import("Zig-PSP/src/psp/include/pspdisplay.zig");
usingnamespace @import("Zig-PSP/src/psp/utils/psp.zig");
 
var draw_buffer: ?[*]u32 = null;
var disp_buffer: ?[*]u32 = null;
 
pub fn init() void {
    draw_buffer = @intToPtr(?[*]u32, @ptrToInt(sceGeEdramGetAddr()));
    disp_buffer = @intToPtr(?[*]u32, @ptrToInt(sceGeEdramGetAddr()) + (272 * 512 * 4) );
 
    _ = sceDisplaySetMode(0, 480, 272);
    _ = sceDisplaySetFrameBuf(disp_buffer, 512, @enumToInt(PspDisplayPixelFormats.Format8888), @enumToInt(PspDisplaySetBufSync.Nextframe));
}
 
pub fn swapBuffers() void {
    var temp = disp_buffer;
    disp_buffer = draw_buffer;
    draw_buffer = temp;
 
    sceKernelDcacheWritebackInvalidateAll();
    _ = sceDisplaySetFrameBuf(disp_buffer, 512, @enumToInt(PspDisplayPixelFormats.Format8888), @enumToInt(PspDisplaySetBufSync.Nextframe));
}
 
pub fn clear(color: u32) void {
    var i: usize = 0;
    while(i < 512 * 272) : (i += 1) {
        draw_buffer.?[i] = color;
    }
}
 
pub fn drawRect(x: usize, y: usize, w: usize, h: usize, color: u32) void {
    var x0 = x;
    var y0 = y;
    var w0 = w;
    var h0 = h;
 
    if(x0 > 480) {
        x0 = 480;
    }
 
    if(y0 > 272) {
        y0 = 272;
    }
 
    if(x0 + w0 > 480) {
        w0 = 480 - x0;
    }
 
    if(y0 + h0 > 272) {
        h0 = 272 - y0;
    }
 
    var off: usize = x0 + (y0 * 512);
 
    var y1: usize = 0;
    while(y1 < h0) : (y1 += 1){
        var x1: usize = 0;
        while(x1 < w0) : (x1 += 1){
            draw_buffer.?[x1 + off + y1 * 512] = color;
        }
    }
}

main.zig

//Same as before
 
pub fn main() !void {
    psp.utils.enableHBCB();
    gfx.init();
 
    while(true){
        gfx.clear(0xffffca82); //#82caffff = skyblue (r,g,b,a) -> 0xffffca82 is the equivalent
 
        gfx.drawRect(10, 10, 30, 30, 0xFF00FFFF); //Yellow
 
        gfx.swapBuffers();
        _ = sceDisplayWaitVblankStart();
    }
}

The Zig code here is pretty minimal with a few notes. The draw and display buffer are optionally set to null and then casted to from the integer address returned by sceGeEdramGetAddr(); - the sceDisplaySetFrameBuf method possibly could be swapped to the wrapped method to avoid the enum to int. Draw rect makes a mutable copy of the arguments because the arguments to a function are constant by default unless passed by reference.

  • tutorial/framebuffer_graphics.txt
  • Last modified: 2020/11/13 21:00
  • by iridescence