core: gpu: Implement rotation/scale background rendering

Displaying sbb_aff.gba and rsbin demos correctly.

Problems in mode7:
* Incorrect first scanline in m7_demo.gba
* Mariokart scaling is off


Former-commit-id: 27655a7a3cde0e9cdabd727a6ec9c99270a99b35
This commit is contained in:
Michel Heily 2020-01-16 19:46:36 +02:00
parent 12ebcf44a2
commit d97d07774f
6 changed files with 362 additions and 197 deletions

View file

@ -12,6 +12,8 @@ use crate::num::FromPrimitive;
mod render;
use render::Point;
mod mosaic;
mod rgb15;
mod sfx;
@ -154,6 +156,8 @@ pub struct BgAffine {
pub pd: i16, // dmy
pub x: i32,
pub y: i32,
pub internal_x: i32,
pub internal_y: i32,
}
#[derive(Debug, Copy, Clone)]
@ -282,6 +286,14 @@ impl Gpu {
self.frame_buffer[index2d!(usize, x, y, DISPLAY_WIDTH)] = p.to_rgb24();
}
pub fn get_ref_point(&self, bg: usize) -> Point {
assert!(bg == 2 || bg == 3);
(
self.bg_aff[bg - 2].internal_x,
self.bg_aff[bg - 2].internal_y,
)
}
pub fn render_scanline(&mut self) {
match self.dispcnt.mode() {
0 => {
@ -343,6 +355,73 @@ impl Gpu {
}
}
pub fn on_state_completed(
&mut self,
completed: GpuState,
sb: &mut SysBus,
irqs: &mut IrqBitmask,
) {
match self.state {
HDraw => {
// Transition to HBlank
self.state = HBlank;
self.cycles_left_for_current_state = CYCLES_HBLANK;
self.dispstat.set_hblank_flag(true);
if self.dispstat.hblank_irq_enable() {
irqs.set_LCD_HBlank(true);
};
sb.io.dmac.notify_hblank();
}
HBlank => {
self.update_vcount(self.vcount + 1, irqs);
if self.vcount < DISPLAY_HEIGHT {
self.state = HDraw;
self.dispstat.set_hblank_flag(false);
self.render_scanline();
// update BG2/3 reference points on the end of a scanline
for i in 0..2 {
self.bg_aff[i].internal_x += self.bg_aff[i].pb as i16 as i32;
self.bg_aff[i].internal_y += self.bg_aff[i].pd as i16 as i32;
}
self.cycles_left_for_current_state = CYCLES_HDRAW;
} else {
self.state = VBlank;
// latch BG2/3 reference points on vblank
for i in 0..2 {
self.bg_aff[i].internal_x = self.bg_aff[i].x;
self.bg_aff[i].internal_y = self.bg_aff[i].y;
}
self.dispstat.set_vblank_flag(true);
self.dispstat.set_hblank_flag(false);
if self.dispstat.vblank_irq_enable() {
irqs.set_LCD_VBlank(true);
};
sb.io.dmac.notify_vblank();
self.video_device.borrow_mut().render(&self.frame_buffer);
self.cycles_left_for_current_state = CYCLES_SCANLINE;
}
}
VBlank => {
if self.vcount < DISPLAY_HEIGHT + VBLANK_LINES - 1 {
self.update_vcount(self.vcount + 1, irqs);
self.cycles_left_for_current_state = CYCLES_SCANLINE;
} else {
self.update_vcount(0, irqs);
self.dispstat.set_vblank_flag(false);
self.render_scanline();
self.state = HDraw;
self.cycles_left_for_current_state = CYCLES_HDRAW;
}
}
};
}
// Returns the new gpu state
pub fn step(
&mut self,
@ -354,51 +433,7 @@ impl Gpu {
if self.cycles_left_for_current_state <= cycles {
let overshoot = cycles - self.cycles_left_for_current_state;
// handle the state change
match self.state {
HDraw => {
// Transition to HBlank
self.state = HBlank;
self.cycles_left_for_current_state = CYCLES_HBLANK;
self.dispstat.set_hblank_flag(true);
if self.dispstat.hblank_irq_enable() {
irqs.set_LCD_HBlank(true);
};
sb.io.dmac.notify_hblank();
}
HBlank => {
self.dispstat.set_hblank_flag(false);
self.update_vcount(self.vcount + 1, irqs);
if self.vcount < DISPLAY_HEIGHT {
self.render_scanline();
self.state = HDraw;
self.cycles_left_for_current_state = CYCLES_HDRAW;
} else {
self.state = VBlank;
self.cycles_left_for_current_state = CYCLES_SCANLINE;
self.dispstat.set_vblank_flag(true);
if self.dispstat.vblank_irq_enable() {
irqs.set_LCD_VBlank(true);
};
sb.io.dmac.notify_vblank();
self.video_device.borrow_mut().render(&self.frame_buffer);
}
}
VBlank => {
if self.vcount < DISPLAY_HEIGHT + VBLANK_LINES - 1 {
self.update_vcount(self.vcount + 1, irqs);
self.cycles_left_for_current_state = CYCLES_SCANLINE;
} else {
self.update_vcount(0, irqs);
self.dispstat.set_vblank_flag(false);
self.render_scanline();
self.state = HDraw;
self.cycles_left_for_current_state = CYCLES_HDRAW;
}
}
};
self.on_state_completed(self.state, sb, irqs);
// handle the overshoot
if overshoot < self.cycles_left_for_current_state {

View file

@ -44,14 +44,9 @@ impl BgControl {
}
}
pub fn size_affine(&self) -> (u32, u32) {
match self.bg_size() {
0b00 => (128, 128),
0b01 => (256, 256),
0b10 => (512, 512),
0b11 => (1024, 1024),
_ => unreachable!(),
}
pub fn size_affine(&self) -> (i32, i32) {
let x = 128 << self.bg_size();
(x, x)
}
pub fn tile_format(&self) -> (u32, PixelFormat) {

View file

@ -1,3 +1,48 @@
pub(super) mod bitmap;
pub(super) mod obj;
pub(super) mod text;
pub(super) type Point = (i32, i32);
#[derive(Debug)]
pub(super) struct ViewPort {
pub origin: Point,
pub w: i32,
pub h: i32,
}
impl ViewPort {
pub fn new(w: i32, h: i32) -> ViewPort {
ViewPort {
origin: (0, 0),
w: w,
h: h,
}
}
pub fn contains_point(&self, p: Point) -> bool {
let (mut x, mut y) = p;
x -= self.origin.0;
y -= self.origin.1;
x >= 0 && x < self.w && y >= 0 && y < self.h
}
}
use super::consts::{DISPLAY_HEIGHT, DISPLAY_WIDTH};
pub(super) static SCREEN_VIEWPORT: ViewPort = ViewPort {
origin: (0, 0),
w: DISPLAY_WIDTH as i32,
h: DISPLAY_HEIGHT as i32,
};
pub(super) mod utils {
use super::Point;
#[inline]
pub fn transform_bg_point(ref_point: Point, screen_x: i32, pa: i32, pc: i32) -> Point {
let (ref_x, ref_y) = ref_point;
((ref_x + screen_x * pa) >> 8, (ref_y + screen_x * pc) >> 8)
}
}

View file

@ -1,38 +1,59 @@
//! Rendering for modes 4-5
use super::super::consts::*;
use super::super::Gpu;
use super::super::Rgb15;
use crate::core::Bus;
impl Gpu {
pub(in super::super) fn render_mode3(&mut self, bg: usize) {
let y = self.vcount;
for x in 0..DISPLAY_WIDTH {
let pixel_index = index2d!(u32, x, y, DISPLAY_WIDTH);
let pixel_ofs = 2 * pixel_index;
let color = Rgb15(self.vram.read_16(pixel_ofs));
self.bg[bg].line[x] = color;
}
}
pub(in super::super) fn render_mode4(&mut self, bg: usize) {
let page_ofs: u32 = match self.dispcnt.display_frame() {
0 => 0x0600_0000 - VRAM_ADDR,
1 => 0x0600_a000 - VRAM_ADDR,
_ => unreachable!(),
};
let y = self.vcount;
for x in 0..DISPLAY_WIDTH {
let bitmap_index = index2d!(x, y, DISPLAY_WIDTH);
let bitmap_ofs = page_ofs + (bitmap_index as u32);
let index = self.vram.read_8(bitmap_ofs) as u32;
let color = self.get_palette_color(index, 0, 0);
self.bg[bg].line[x] = color;
}
}
}
//! Rendering for modes 4-5
use super::super::consts::*;
use super::super::Gpu;
use super::super::Rgb15;
use super::{utils, SCREEN_VIEWPORT};
use crate::core::Bus;
impl Gpu {
pub(in super::super) fn render_mode3(&mut self, bg: usize) {
let y = self.vcount;
let pa = self.bg_aff[bg - 2].pa as i32;
let pc = self.bg_aff[bg - 2].pc as i32;
let ref_point = self.get_ref_point(bg);
for x in 0..DISPLAY_WIDTH {
let t = utils::transform_bg_point(ref_point, x as i32, pa, pc);
if !SCREEN_VIEWPORT.contains_point(t) {
self.bg[bg].line[x] = Rgb15::TRANSPARENT;
continue;
}
let pixel_index = index2d!(u32, t.0, t.1, DISPLAY_WIDTH);
let pixel_ofs = 2 * pixel_index;
let color = Rgb15(self.vram.read_16(pixel_ofs));
self.bg[bg].line[x] = color;
}
}
pub(in super::super) fn render_mode4(&mut self, bg: usize) {
let page_ofs: u32 = match self.dispcnt.display_frame() {
0 => 0x0600_0000 - VRAM_ADDR,
1 => 0x0600_a000 - VRAM_ADDR,
_ => unreachable!(),
};
let y = self.vcount;
let pa = self.bg_aff[bg - 2].pa as i32;
let pc = self.bg_aff[bg - 2].pc as i32;
let ref_point = self.get_ref_point(bg);
for x in 0..DISPLAY_WIDTH {
let t = utils::transform_bg_point(ref_point, x as i32, pa, pc);
if !SCREEN_VIEWPORT.contains_point(t) {
self.bg[bg].line[x] = Rgb15::TRANSPARENT;
continue;
}
let bitmap_index = index2d!(u32, t.0, t.1, DISPLAY_WIDTH);
let bitmap_ofs = page_ofs + (bitmap_index as u32);
let index = self.vram.read_8(bitmap_ofs) as u32;
let color = self.get_palette_color(index, 0, 0);
self.bg[bg].line[x] = color;
}
}
}

View file

@ -1,94 +1,136 @@
//! Rendering for modes 0-3
use super::super::consts::*;
use super::super::{Gpu, PixelFormat, Scanline, SCREEN_BLOCK_SIZE};
use crate::core::Bus;
impl Gpu {
pub(in super::super) fn render_reg_bg(&mut self, bg: usize) {
let (h_ofs, v_ofs) = (self.bg[bg].bghofs as u32, self.bg[bg].bgvofs as u32);
let tileset_base = self.bg[bg].bgcnt.char_block();
let tilemap_base = self.bg[bg].bgcnt.screen_block();
let (tile_size, pixel_format) = self.bg[bg].bgcnt.tile_format();
let (bg_width, bg_height) = self.bg[bg].bgcnt.size_regular();
let screen_y = self.vcount as u32;
let mut screen_x = 0;
// calculate the bg coords at the top-left corner, including wraparound
let bg_x = (screen_x + h_ofs) % bg_width;
let bg_y = (screen_y + v_ofs) % bg_height;
// calculate the initial screen entry index
// | (256,256) | (512,256) | (256,512) | (512,512) |
// |-----------|-----------|-------------|-----------|
// | | | [1] | [2][3] |
// | [0] | [0][1] | [0] | [0][1] |
// |___________|___________|_____________|___________|
//
let mut sbb = match (bg_width, bg_height) {
(256, 256) => 0,
(512, 256) => bg_x / 256,
(256, 512) => bg_y / 256,
(512, 512) => index2d!(u32, bg_x / 256, bg_y / 256, 2),
_ => unreachable!(),
} as u32;
let mut se_row = (bg_x / 8) % 32;
let se_column = (bg_y / 8) % 32;
// this will be non-zero if the h-scroll lands in a middle of a tile
let mut start_tile_x = bg_x % 8;
let tile_py = (bg_y % 8) as u32;
loop {
let mut map_addr =
tilemap_base + SCREEN_BLOCK_SIZE * sbb + 2 * index2d!(u32, se_row, se_column, 32);
for _ in se_row..32 {
let entry = TileMapEntry(self.vram.read_16(map_addr - VRAM_ADDR));
let tile_addr = tileset_base + entry.tile_index() * tile_size;
for tile_px in start_tile_x..8 {
let index = self.read_pixel_index(
tile_addr,
if entry.x_flip() { 7 - tile_px } else { tile_px },
if entry.y_flip() { 7 - tile_py } else { tile_py },
pixel_format,
);
let palette_bank = match pixel_format {
PixelFormat::BPP4 => entry.palette_bank() as u32,
PixelFormat::BPP8 => 0u32,
};
let color = self.get_palette_color(index as u32, palette_bank, 0);
self.bg[bg].line[screen_x as usize] = color;
screen_x += 1;
if (DISPLAY_WIDTH as u32) == screen_x {
return;
}
}
start_tile_x = 0;
map_addr += 2;
}
se_row = 0;
if bg_width == 512 {
sbb = sbb ^ 1;
}
}
}
pub(in super::super) fn render_aff_bg(&mut self, bg: usize) {
// TODO
self.bg[bg].line = Scanline::default();
}
}
bitfield! {
struct TileMapEntry(u16);
u16;
u32, tile_index, _: 9, 0;
x_flip, _ : 10;
y_flip, _ : 11;
palette_bank, _ : 15, 12;
}
//! Rendering for modes 0-3
use super::super::consts::*;
use super::super::Rgb15;
use super::super::{Gpu, PixelFormat, SCREEN_BLOCK_SIZE};
use super::{utils, ViewPort};
use crate::core::Bus;
impl Gpu {
pub(in super::super) fn render_reg_bg(&mut self, bg: usize) {
let (h_ofs, v_ofs) = (self.bg[bg].bghofs as u32, self.bg[bg].bgvofs as u32);
let tileset_base = self.bg[bg].bgcnt.char_block();
let tilemap_base = self.bg[bg].bgcnt.screen_block();
let (tile_size, pixel_format) = self.bg[bg].bgcnt.tile_format();
let (bg_width, bg_height) = self.bg[bg].bgcnt.size_regular();
let screen_y = self.vcount as u32;
let mut screen_x = 0;
// calculate the bg coords at the top-left corner, including wraparound
let bg_x = (screen_x + h_ofs) % bg_width;
let bg_y = (screen_y + v_ofs) % bg_height;
// calculate the initial screen entry index
// | (256,256) | (512,256) | (256,512) | (512,512) |
// |-----------|-----------|-------------|-----------|
// | | | [1] | [2][3] |
// | [0] | [0][1] | [0] | [0][1] |
// |___________|___________|_____________|___________|
//
let mut sbb = match (bg_width, bg_height) {
(256, 256) => 0,
(512, 256) => bg_x / 256,
(256, 512) => bg_y / 256,
(512, 512) => index2d!(u32, bg_x / 256, bg_y / 256, 2),
_ => unreachable!(),
} as u32;
let mut se_row = (bg_x / 8) % 32;
let se_column = (bg_y / 8) % 32;
// this will be non-zero if the h-scroll lands in a middle of a tile
let mut start_tile_x = bg_x % 8;
let tile_py = (bg_y % 8) as u32;
loop {
let mut map_addr =
tilemap_base + SCREEN_BLOCK_SIZE * sbb + 2 * index2d!(u32, se_row, se_column, 32);
for _ in se_row..32 {
let entry = TileMapEntry(self.vram.read_16(map_addr - VRAM_ADDR));
let tile_addr = tileset_base + entry.tile_index() * tile_size;
for tile_px in start_tile_x..8 {
let index = self.read_pixel_index(
tile_addr,
if entry.x_flip() { 7 - tile_px } else { tile_px },
if entry.y_flip() { 7 - tile_py } else { tile_py },
pixel_format,
);
let palette_bank = match pixel_format {
PixelFormat::BPP4 => entry.palette_bank() as u32,
PixelFormat::BPP8 => 0u32,
};
let color = self.get_palette_color(index as u32, palette_bank, 0);
self.bg[bg].line[screen_x as usize] = color;
screen_x += 1;
if (DISPLAY_WIDTH as u32) == screen_x {
return;
}
}
start_tile_x = 0;
map_addr += 2;
}
se_row = 0;
if bg_width == 512 {
sbb = sbb ^ 1;
}
}
}
pub(in super::super) fn render_aff_bg(&mut self, bg: usize) {
assert!(bg == 2 || bg == 3);
let texture_size = 128 << self.bg[bg].bgcnt.bg_size();
let viewport = ViewPort::new(texture_size, texture_size);
let ref_point = self.get_ref_point(bg);
let pa = self.bg_aff[bg - 2].pa as i16 as i32;
let pc = self.bg_aff[bg - 2].pc as i16 as i32;
let screen_block = self.bg[bg].bgcnt.screen_block();
let char_block = self.bg[bg].bgcnt.char_block();
let wraparound = self.bg[bg].bgcnt.affine_wraparound();
for screen_x in 0..(DISPLAY_WIDTH as i32) {
let mut t = utils::transform_bg_point(ref_point, screen_x, pa, pc);
if !viewport.contains_point(t) {
if wraparound {
t.0 = t.0.rem_euclid(texture_size);
t.1 = t.1.rem_euclid(texture_size);
} else {
self.bg[bg].line[screen_x as usize] = Rgb15::TRANSPARENT;
continue;
}
}
let map_addr = screen_block + index2d!(u32, t.0 / 8, t.1 / 8, texture_size / 8);
let tile_index = self.vram.read_8(map_addr - VRAM_ADDR) as u32;
let tile_addr = char_block + tile_index * 0x40;
let color = self.get_palette_color(
self.read_pixel_index(
tile_addr,
(t.0 % 8) as u32,
(t.1 % 8) as u32,
PixelFormat::BPP8,
) as u32,
0,
0,
);
self.bg[bg].line[screen_x as usize] = color;
}
}
}
bitfield! {
struct TileMapEntry(u16);
u16;
u32, tile_index, _: 9, 0;
x_flip, _ : 10;
y_flip, _ : 11;
palette_bank, _ : 15, 12;
}

View file

@ -4,7 +4,6 @@ use super::gpu::*;
use super::interrupt::InterruptController;
use super::keypad;
use super::sound::SoundController;
use super::sysbus::BoxedMemory;
use super::timer::Timers;
use super::{Addr, Bus};
@ -27,8 +26,6 @@ pub struct IoDevices {
pub post_boot_flag: bool,
pub waitcnt: WaitControl, // TODO also implement 4000800
pub haltcnt: HaltState,
mem: BoxedMemory,
}
impl IoDevices {
@ -39,7 +36,6 @@ impl IoDevices {
timers: Timers::new(),
dmac: DmaController::new(),
intc: InterruptController::new(),
mem: BoxedMemory::new(vec![0; 0x800].into_boxed_slice()),
post_boot_flag: false,
haltcnt: HaltState::Running,
keyinput: keypad::KEYINPUT_ALL_RELEASED,
@ -135,14 +131,40 @@ impl Bus for IoDevices {
REG_BG2VOFS => io.gpu.bg[2].bgvofs = value & 0x1ff,
REG_BG3HOFS => io.gpu.bg[3].bghofs = value & 0x1ff,
REG_BG3VOFS => io.gpu.bg[3].bgvofs = value & 0x1ff,
REG_BG2X_L => io.gpu.bg_aff[0].x |= (value as u32) as i32,
REG_BG2X_H => io.gpu.bg_aff[0].x |= ((value as u32) << 16) as i32,
REG_BG2Y_L => io.gpu.bg_aff[0].y |= (value as u32) as i32,
REG_BG2Y_H => io.gpu.bg_aff[0].y |= ((value as u32) << 16) as i32,
REG_BG3X_L => io.gpu.bg_aff[1].x |= (value as u32) as i32,
REG_BG3X_H => io.gpu.bg_aff[1].x |= ((value as u32) << 16) as i32,
REG_BG3Y_L => io.gpu.bg_aff[1].y |= (value as u32) as i32,
REG_BG3Y_H => io.gpu.bg_aff[1].y |= ((value as u32) << 16) as i32,
REG_BG2X_L | REG_BG3X_L => {
let index = (io_addr - REG_BG2X_L) / 0x10;
let t = io.gpu.bg_aff[index as usize].x as u32;
io.gpu.bg_aff[index as usize].x = ((t & 0xffff0000) + (value as u32)) as i32;
if io.gpu.state != GpuState::VBlank {
io.gpu.bg_aff[index as usize].internal_x = io.gpu.bg_aff[index as usize].x;
}
}
REG_BG2Y_L | REG_BG3Y_L => {
let index = (io_addr - REG_BG2X_L) / 0x10;
let t = io.gpu.bg_aff[index as usize].y as u32;
io.gpu.bg_aff[index as usize].y = ((t & 0xffff0000) + (value as u32)) as i32;
if io.gpu.state != GpuState::VBlank {
io.gpu.bg_aff[index as usize].internal_y = io.gpu.bg_aff[index as usize].y;
}
}
REG_BG2X_H | REG_BG3X_H => {
let index = (io_addr - REG_BG2X_L) / 0x10;
let t = io.gpu.bg_aff[index as usize].x;
io.gpu.bg_aff[index as usize].x =
(t & 0xffff) | ((sign_extend_i32((value & 0xfff) as i32, 12)) << 16);
if io.gpu.state != GpuState::VBlank {
io.gpu.bg_aff[index as usize].internal_x = io.gpu.bg_aff[index as usize].x;
}
}
REG_BG2Y_H | REG_BG3Y_H => {
let index = (io_addr - REG_BG2X_L) / 0x10;
let t = io.gpu.bg_aff[index as usize].y;
io.gpu.bg_aff[index as usize].y =
(t & 0xffff) | ((sign_extend_i32((value & 0xfff) as i32, 12)) << 16);
if io.gpu.state != GpuState::VBlank {
io.gpu.bg_aff[index as usize].internal_y = io.gpu.bg_aff[index as usize].y;
}
}
REG_BG2PA => io.gpu.bg_aff[0].pa = value as i16,
REG_BG2PB => io.gpu.bg_aff[0].pb = value as i16,
REG_BG2PC => io.gpu.bg_aff[0].pc = value as i16,
@ -479,3 +501,8 @@ pub fn io_reg_string(addr: u32) -> &'static str {
_ => "UNKNOWN",
}
}
fn sign_extend_i32(value: i32, size: u32) -> i32 {
let shift = 32 - size;
((value << shift) as i32) >> shift
}