Skip to content

GPU

Background Polygon Missing

The Japanese BIOS versions (? need to verify this pattern) seem to get the background wrong (for example SCPH1000.BIN and SCPH3000.BIN. No idea why, noticed this at Git commit 12a9d2716ceff315957238c8590fbffd31c52db9. Other BIOS versions like SCPH1001.BIN work fine.

Calculating Polygon Word Count

Polygon primitive base count is calculated based on vertices count bit:

pub struct DrawPolygonCommand(pub u32) {
    pub color: u32 @ 0..=23,
    pub raw_texture: bool @ 24,
    pub semi_transparent: bool @ 25,
    pub textured: bool @ 26,
    pub vertices_count: bool @ 27,
    pub gouraud: bool @ 28,
    pub command: u32 @ 29..=31,
}

Base count of extra data words is either 4 or 3 (will call the count vertices). 4 if vertices_count is set.

  • If gouraud is set, then vertices - 1 on top of that
  • If textured is set, then vertices

Basically:

Gp0Command::PolygonPrimitive(cmd) => {
    let vertices = if cmd.vertices_count() { 4 } else { 3 };
    let mut base = vertices;

    // requires color for vertices 1..n (color 0 in cmd word)
    if cmd.gouraud() {
        base += vertices - 1;
    }

    // requires UV for each vertex (no vertex in cmd word)
    if cmd.textured() {
        base += vertices;
    }

    base
}

Rasterizer

Given vertices v0, v1, v2, v3 to draw a rectangle/quad:

v0 --- v1        Triangle 1: v0 -> v1 -> v2
|       |        Triangle 2: v1 -> v2 -> v3
|       |
v2 --- v3        Note: 0-indexed, see below

Quads are internally processed as two triangles, the first consisting of vertices 1,2,3, and the second of vertices 2,3,4. Source

fn rasterize_polygon(vertices: &[(i16, i16)], colors: &[u32], vram: &mut [u8]) {
    // fun stuff
}
  • If vertices.len() == 4 then we know we have a quad and must split it into 2 triangles
  • If not, then we operate on "Triangle 1" only
  • colors maps each coordinate with a color which can later be used for coloring and applying gradient
  • vram is just a reference to the internal VRAM storage

To get the coordinate/corner positions:

let (x0, y0) = (vertices[0].0 as i32, vertices[0].1 as i32);
let (x1, y1) = (vertices[1].0 as i32, vertices[1].1 as i32);
let (x2, y2) = (vertices[2].0 as i32, vertices[2].1 as i32);

For optimization we can define a min/max (bounding box?) of the polygon so we can perform the math on this selection only:

let min_x = x0.min(x1).min(x2).max(0);
let max_x = x0.max(x1).max(x2).min(VRAM_WIDTH as i32 - 1);
let min_y = y0.min(y1).min(y2).max(0);
let max_y = y0.max(y1).max(y2).min(VRAM_HEIGHT as i32 - 1);

  • min_x: The leftmost point of any corner
  • max_x: The rightmost point of any corner
  • min_y: The topmost point of any corner
  • max_y: The bottommost point of any corner

Now to fill a triangle we'll have to go through each point in the bounding box (?) and check whether this point is:

  • to the left of a line A to B
  • to the right of a line A to B
  • on top of a line A to B
fn edge_function(ax: i32, ay: i32, bx: i32, by: i32, px: i32, py: i32) -> i32 {
    (bx - ax) * (py - ay) - (by - ay) * (px - ax)
}

let area = edge_function(x0, y0, x1, y1, x2, y2); // if area == 0 -> straight line
let clockwise = area < 0;

let w0 = edge_function(x1, y1, x2, y2, x, y);
let w1 = edge_function(x2, y2, x0, y0, x, y);
let w2 = edge_function(x0, y0, x1, y1, x, y);

let inside = if clockwise {
    w0 <= 0 && w1 <= 0 && w2 <= 0
} else {
    w0 >= 0 && w1 >= 0 && w2 >= 0
};

// if inside == true -> pixel x,y is inside of triangle

To interpolate the colors:

let r0 = (colors[0] & 0xFF) as i32;
let g0 = ((colors[0] >> 8) & 0xFF) as i32;
let b0 = ((colors[0] >> 16) & 0xFF) as i32;

let r1 = (colors[1] & 0xFF) as i32;
let g1 = ((colors[1] >> 8) & 0xFF) as i32;
let b1 = ((colors[1] >> 16) & 0xFF) as i32;

let r2 = (colors[2] & 0xFF) as i32;
let g2 = ((colors[2] >> 8) & 0xFF) as i32;
let b2 = ((colors[2] >> 16) & 0xFF) as i32;

// gouraud shading
let r = (r0 * w0 + r1 * w1 + r2 * w2) / area;
let g = (g0 * w0 + g1 * w1 + g2 * w2) / area;
let b = (b0 * w0 + b1 * w1 + b2 * w2) / area;

Since VRAM stores RGB values as RGB555 we'll have to modify them:

let r5 = ((r >> 3) & 0x1F) as u16;
let g5 = ((g >> 3) & 0x1F) as u16;
let b5 = ((b >> 3) & 0x1F) as u16;
let pixel = (b5 << 10) | (g5 << 5) | r5;

Half Textured Polygons

rasterize_polygon
 -> rasterize_triangle

If rasterize_triangle extracts CLUT and Texpage from UVs then that won't work as uv0 is uv1 and uv1 is uv2.

rasterize_triangle(
    // ...
    if textured {
        [uvs[0], uvs[1], uvs[2]]
    } else {
        [0, 0, 0]
    },
    // ...
);
rasterize_triangle(
    // ...
    if textured {
        [uvs[1], uvs[2], uvs[3]]
    } else {
        [0, 0, 0]
    },
    // ...
);
// Wont work!! need to get CLUT and Texpage before call rasterize_trinalgle since those are always inside uv0 and uv1

Missing Sony Logo Colors

In my case CLUT got overwritten!!

Polygons are displayed up to \ their lower-right coordinates. Source

Should be able to see them just below the grey area as white-ish dots. That disappears eventually. Bounding box needs to be exclusive.

let min_x = x0.min(x1).min(x2).max(0);
let max_x = x0.max(x1).max(x2).min(VRAM_WIDTH as i32);
let min_y = y0.min(y1).min(y2).max(0);
let max_y = y0.max(y1).max(y2).min(VRAM_HEIGHT as i32);

for y in min_y..max_y {
    for x in min_x..max_x {
        // rasterize
    }
}

Honorary mention: Chicho

Rainbows Behind Balls

This happens when the "Quick Rectangle Fill" GP0 command (Misc. Commands, subcommand 0x02) is not implemented.

This happens when you only sample textures for polygons. (see above image)

Missing White Text Color

This happens when I return transparency if the texel index is 0 as opposed to the texel itself. (see above)

Honorary mention: netwiss