GB/视频/代码实现
视频模块部分的完整代码实现比较复杂, 比较合理的代码阅读顺序是从 draw_bg
和 draw_sprites
两个函数入手, 如果在任何地方有疑惑, 随时翻阅前几节内容, 或阅读该技术文档: http://gbdev.gg8.se/wiki/articles/Video_Display.
use super::convention::Term;
use super::intf::{Flag, Intf};
use super::memory::Memory;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Eq, PartialEq)]
pub enum HdmaMode {
// When using this transfer method, all data is transferred at once. The execution of the program is halted until
// the transfer has completed. Note that the General Purpose DMA blindly attempts to copy the data, even if the
// CD controller is currently accessing VRAM. So General Purpose DMA should be used only if the Display is disabled,
// or during V-Blank, or (for rather short blocks) during H-Blank. The execution of the program continues when the
// transfer has been completed, and FF55 then contains a value of FFh.
Gdma,
// The H-Blank DMA transfers 10h bytes of data during each H-Blank, ie. at LY=0-143, no data is transferred during
// V-Blank (LY=144-153), but the transfer will then continue at LY=00. The execution of the program is halted
// during the separate transfers, but the program execution continues during the 'spaces' between each data block.
// Note that the program should not change the Destination VRAM bank (FF4F), or the Source ROM/RAM bank (in case
// data is transferred from bankable memory) until the transfer has completed! (The transfer should be paused as
// described below while the banks are switched) Reading from Register FF55 returns the remaining length (divided
// by 10h, minus 1), a value of 0FFh indicates that the transfer has completed. It is also possible to terminate
// an active H-Blank transfer by writing zero to Bit 7 of FF55. In that case reading from FF55 will return how many
// $10 "blocks" remained (minus 1) in the lower 7 bits, but Bit 7 will be read as "1". Stopping the transfer
// doesn't set HDMA1-4 to $FF.
Hdma,
}
pub struct Hdma {
// These two registers specify the address at which the transfer will read data from. Normally, this should be
// either in ROM, SRAM or WRAM, thus either in range 0000-7FF0 or A000-DFF0. [Note : this has yet to be tested on
// Echo RAM, OAM, FEXX, IO and HRAM]. Trying to specify a source address in VRAM will cause garbage to be copied.
// The four lower bits of this address will be ignored and treated as 0.
pub src: u16,
// These two registers specify the address within 8000-9FF0 to which the data will be copied. Only bits 12-4 are
// respected; others are ignored. The four lower bits of this address will be ignored and treated as 0.
pub dst: u16,
pub active: bool,
pub mode: HdmaMode,
pub remain: u8,
}
impl Hdma {
pub fn power_up() -> Self {
Self {
src: 0x0000,
dst: 0x8000,
active: false,
mode: HdmaMode::Gdma,
remain: 0x00,
}
}
}
impl Memory for Hdma {
fn get(&self, a: u16) -> u8 {
match a {
0xff51 => (self.src >> 8) as u8,
0xff52 => self.src as u8,
0xff43 => (self.dst >> 8) as u8,
0xff54 => self.dst as u8,
0xff55 => self.remain | if self.active { 0x00 } else { 0x80 },
_ => panic!(""),
}
}
fn set(&mut self, a: u16, v: u8) {
match a {
0xff51 => self.src = (u16::from(v) << 8) | (self.src & 0x00ff),
0xff52 => self.src = (self.src & 0xff00) | u16::from(v & 0xf0),
0xff53 => self.dst = 0x8000 | (u16::from(v & 0x1f) << 8) | (self.dst & 0x00ff),
0xff54 => self.dst = (self.dst & 0xff00) | u16::from(v & 0xf0),
0xff55 => {
if self.active && self.mode == HdmaMode::Hdma {
if v & 0x80 == 0x00 {
self.active = false;
};
return;
}
self.active = true;
self.remain = v & 0x7f;
self.mode = if v & 0x80 != 0x00 {
HdmaMode::Hdma
} else {
HdmaMode::Gdma
};
}
_ => panic!(""),
};
}
}
// LCDC is the main LCD Control register. Its bits toggle what elements are displayed on the screen, and how.
pub struct Lcdc {
data: u8,
}
#[rustfmt::skip]
impl Lcdc {
pub fn power_up() -> Self {
Self { data: 0b0100_1000 }
}
// LCDC.7 - LCD Display Enable
// This bit controls whether the LCD is on and the PPU is active. Setting it to 0 turns both off, which grants
// immediate and full access to VRAM, OAM, etc.
fn bit7(&self) -> bool { self.data & 0b1000_0000 != 0x00 }
// LCDC.6 - Window Tile Map Display Select
// This bit controls which background map the Window uses for rendering. When it's reset, the $9800 tilemap is used,
// otherwise it's the $9C00 one.
fn bit6(&self) -> bool { self.data & 0b0100_0000 != 0x00 }
// LCDC.5 - Window Display Enable
// This bit controls whether the window shall be displayed or not. (TODO : what happens when toggling this
// mid-scanline ?) This bit is overridden on DMG by bit 0 if that bit is reset.
// Note that on CGB models, setting this bit to 0 then back to 1 mid-frame may cause the second write to be ignored.
fn bit5(&self) -> bool { self.data & 0b0010_0000 != 0x00 }
// LCDC.4 - BG & Window Tile Data Select
// This bit controls which addressing mode the BG and Window use to pick tiles.
// Sprites aren't affected by this, and will always use $8000 addressing mode.
fn bit4(&self) -> bool { self.data & 0b0001_0000 != 0x00 }
// LCDC.3 - BG Tile Map Display Select
// This bit works similarly to bit 6: if the bit is reset, the BG uses tilemap $9800, otherwise tilemap $9C00.
fn bit3(&self) -> bool { self.data & 0b0000_1000 != 0x00 }
// LCDC.2 - OBJ Size
// This bit controls the sprite size (1 tile or 2 stacked vertically).
// Be cautious when changing this mid-frame from 8x8 to 8x16 : "remnants" of the sprites intended for 8x8 could
// "leak" into the 8x16 zone and cause artifacts.
fn bit2(&self) -> bool { self.data & 0b0000_0100 != 0x00 }
// LCDC.1 - OBJ Display Enable
// This bit toggles whether sprites are displayed or not.
// This can be toggled mid-frame, for example to avoid sprites being displayed on top of a status bar or text box.
// (Note: toggling mid-scanline might have funky results on DMG? Investigation needed.)
fn bit1(&self) -> bool { self.data & 0b0000_0010 != 0x00 }
// LCDC.0 - BG/Window Display/Priority
// LCDC.0 has different meanings depending on Gameboy type and Mode:
// Monochrome Gameboy, SGB and CGB in Non-CGB Mode: BG Display
// When Bit 0 is cleared, both background and window become blank (white), and the Window Display Bit is ignored in
// that case. Only Sprites may still be displayed (if enabled in Bit 1).
// CGB in CGB Mode: BG and Window Master Priority
// When Bit 0 is cleared, the background and window lose their priority - the sprites will be always displayed on
// top of background and window, independently of the priority flags in OAM and BG Map attributes.
fn bit0(&self) -> bool { self.data & 0b0000_0001 != 0x00 }
}
// LCD Status Register.
pub struct Stat {
// Bit 6 - LYC=LY Coincidence Interrupt (1=Enable) (Read/Write)
enable_ly_interrupt: bool,
// Bit 5 - Mode 2 OAM Interrupt (1=Enable) (Read/Write)
enable_m2_interrupt: bool,
// Bit 4 - Mode 1 V-Blank Interrupt (1=Enable) (Read/Write)
enable_m1_interrupt: bool,
// Bit 3 - Mode 0 H-Blank Interrupt (1=Enable) (Read/Write)
enable_m0_interrupt: bool,
// Bit 1-0 - Mode Flag (Mode 0-3, see below) (Read Only)
// 0: During H-Blank
// 1: During V-Blank
// 2: During Searching OAM
// 3: During Transferring Data to LCD Driver
mode: u8,
}
impl Stat {
pub fn power_up() -> Self {
Self {
enable_ly_interrupt: false,
enable_m2_interrupt: false,
enable_m1_interrupt: false,
enable_m0_interrupt: false,
mode: 0x00,
}
}
}
// This register is used to address a byte in the CGBs Background Palette Memory. Each two byte in that memory define a
// color value. The first 8 bytes define Color 0-3 of Palette 0 (BGP0), and so on for BGP1-7.
// Bit 0-5 Index (00-3F)
// Bit 7 Auto Increment (0=Disabled, 1=Increment after Writing)
// Data can be read/written to/from the specified index address through Register FF69. When the Auto Increment bit is
// set then the index is automatically incremented after each <write> to FF69. Auto Increment has no effect when
// <reading> from FF69, so the index must be manually incremented in that case. Writing to FF69 during rendering still
// causes auto-increment to occur.
// Unlike the following, this register can be accessed outside V-Blank and H-Blank.
struct Bgpi {
i: u8,
auto_increment: bool,
}
impl Bgpi {
fn power_up() -> Self {
Self {
i: 0x00,
auto_increment: false,
}
}
fn get(&self) -> u8 {
let a = if self.auto_increment { 0x80 } else { 0x00 };
a | self.i
}
fn set(&mut self, v: u8) {
self.auto_increment = v & 0x80 != 0x00;
self.i = v & 0x3f;
}
}
pub enum GrayShades {
White = 0xff,
Light = 0xc0,
Dark = 0x60,
Black = 0x00,
}
// Bit7 OBJ-to-BG Priority (0=OBJ Above BG, 1=OBJ Behind BG color 1-3)
// (Used for both BG and Window. BG color 0 is always behind OBJ)
// Bit6 Y flip (0=Normal, 1=Vertically mirrored)
// Bit5 X flip (0=Normal, 1=Horizontally mirrored)
// Bit4 Palette number **Non CGB Mode Only** (0=OBP0, 1=OBP1)
// Bit3 Tile VRAM-Bank **CGB Mode Only** (0=Bank 0, 1=Bank 1)
// Bit2-0 Palette number **CGB Mode Only** (OBP0-7)
struct Attr {
priority: bool,
yflip: bool,
xflip: bool,
palette_number_0: usize,
bank: bool,
palette_number_1: usize,
}
impl From<u8> for Attr {
fn from(u: u8) -> Self {
Self {
priority: u & (1 << 7) != 0,
yflip: u & (1 << 6) != 0,
xflip: u & (1 << 5) != 0,
palette_number_0: u as usize & (1 << 4),
bank: u & (1 << 3) != 0,
palette_number_1: u as usize & 0x07,
}
}
}
pub const SCREEN_W: usize = 160;
pub const SCREEN_H: usize = 144;
pub struct Gpu {
// Digital image with mode RGB. Size = 144 * 160 * 3.
// 3---------
// ----------
// ----------
// ---------- 160
// 144
pub data: [[[u8; 3]; SCREEN_W]; SCREEN_H],
pub intf: Rc<RefCell<Intf>>,
pub term: Term,
pub h_blank: bool,
pub v_blank: bool,
lcdc: Lcdc,
stat: Stat,
// Scroll Y (R/W), Scroll X (R/W)
// Specifies the position in the 256x256 pixels BG map (32x32 tiles) which is to be displayed at the upper/left LCD
// display position. Values in range from 0-255 may be used for X/Y each, the video controller automatically wraps
// back to the upper (left) position in BG map when drawing exceeds the lower (right) border of the BG map area.
sy: u8,
sx: u8,
// Window Y Position (R/W), Window X Position minus 7 (R/W)
wy: u8,
wx: u8,
// The LY indicates the vertical line to which the present data is transferred to the LCD Driver. The LY can take
// on any value between 0 through 153. The values between 144 and 153 indicate the V-Blank period. Writing will
// reset the counter.
ly: u8,
// The Gameboy permanently compares the value of the LYC and LY registers. When both values are identical, the
// coincident bit in the STAT register becomes set, and (if enabled) a STAT interrupt is requested.
lc: u8,
// This register assigns gray shades to the color numbers of the BG and Window tiles.
bgp: u8,
// This register assigns gray shades for sprite palette 0. It works exactly as BGP (FF47), except that the lower
// two bits aren't used because sprite data 00 is transparent.
op0: u8,
// This register assigns gray shades for sprite palette 1. It works exactly as BGP (FF47), except that the lower
// two bits aren't used because sprite data 00 is transparent.
op1: u8,
cbgpi: Bgpi,
// This register allows to read/write data to the CGBs Background Palette Memory, addressed through Register FF68.
// Each color is defined by two bytes (Bit 0-7 in first byte).
// Bit 0-4 Red Intensity (00-1F)
// Bit 5-9 Green Intensity (00-1F)
// Bit 10-14 Blue Intensity (00-1F)
// Much like VRAM, data in Palette Memory cannot be read/written during the time when the LCD Controller is
// reading from it. (That is when the STAT register indicates Mode 3). Note: All background colors are initialized
// as white by the boot ROM, but it's a good idea to initialize at least one color yourself (for example if you
// include a soft-reset mechanic).
//
// Note: Type [[[u8; 3]; 4]; 8] equals with [u8; 64].
cbgpd: [[[u8; 3]; 4]; 8],
cobpi: Bgpi,
cobpd: [[[u8; 3]; 4]; 8],
ram: [u8; 0x4000],
ram_bank: usize,
// VRAM Sprite Attribute Table (OAM)
// Gameboy video controller can display up to 40 sprites either in 8x8 or in 8x16 pixels. Because of a limitation of
// hardware, only ten sprites can be displayed per scan line. Sprite patterns have the same format as BG tiles, but
// they are taken from the Sprite Pattern Table located at $8000-8FFF and have unsigned numbering.
// Sprite attributes reside in the Sprite Attribute Table (OAM - Object Attribute Memory) at $FE00-FE9F. Each of the 40
// entries consists of four bytes with the following meanings:
// Byte0 - Y Position
// Specifies the sprites vertical position on the screen (minus 16). An off-screen value (for example, Y=0 or
// Y>=160) hides the sprite.
//
// Byte1 - X Position
// Specifies the sprites horizontal position on the screen (minus 8). An off-screen value (X=0 or X>=168) hides the
// sprite, but the sprite still affects the priority ordering - a better way to hide a sprite is to set its
// Y-coordinate off-screen.
//
// Byte2 - Tile/Pattern Number
// Specifies the sprites Tile Number (00-FF). This (unsigned) value selects a tile from memory at 8000h-8FFFh. In
// CGB Mode this could be either in VRAM Bank 0 or 1, depending on Bit 3 of the following byte. In 8x16 mode, the
// lower bit of the tile number is ignored. IE: the upper 8x8 tile is "NN AND FEh", and the lower 8x8 tile
// is "NN OR 01h".
//
// Byte3 - Attributes/Flags:
// Bit7 OBJ-to-BG Priority (0=OBJ Above BG, 1=OBJ Behind BG color 1-3)
// (Used for both BG and Window. BG color 0 is always behind OBJ)
// Bit6 Y flip (0=Normal, 1=Vertically mirrored)
// Bit5 X flip (0=Normal, 1=Horizontally mirrored)
// Bit4 Palette number **Non CGB Mode Only** (0=OBP0, 1=OBP1)
// Bit3 Tile VRAM-Bank **CGB Mode Only** (0=Bank 0, 1=Bank 1)
// Bit2-0 Palette number **CGB Mode Only** (OBP0-7)
oam: [u8; 0xa0],
prio: [(bool, usize); SCREEN_W],
// The LCD controller operates on a 222 Hz = 4.194 MHz dot clock. An entire frame is 154 scanlines, 70224 dots, or
// 16.74 ms. On scanlines 0 through 143, the LCD controller cycles through modes 2, 3, and 0 once every 456 dots.
// Scanlines 144 through 153 are mode 1.
dots: u32,
}
impl Gpu {
pub fn power_up(term: Term, intf: Rc<RefCell<Intf>>) -> Self {
Self {
data: [[[0xffu8; 3]; SCREEN_W]; SCREEN_H],
intf,
term,
h_blank: false,
v_blank: false,
lcdc: Lcdc::power_up(),
stat: Stat::power_up(),
sy: 0x00,
sx: 0x00,
wx: 0x00,
wy: 0x00,
ly: 0x00,
lc: 0x00,
bgp: 0x00,
op0: 0x00,
op1: 0x01,
cbgpi: Bgpi::power_up(),
cbgpd: [[[0u8; 3]; 4]; 8],
cobpi: Bgpi::power_up(),
cobpd: [[[0u8; 3]; 4]; 8],
ram: [0x00; 0x4000],
ram_bank: 0x00,
oam: [0x00; 0xa0],
prio: [(true, 0); SCREEN_W],
dots: 0,
}
}
fn get_ram0(&self, a: u16) -> u8 {
self.ram[a as usize - 0x8000]
}
fn get_ram1(&self, a: u16) -> u8 {
self.ram[a as usize - 0x6000]
}
// This register assigns gray shades to the color numbers of the BG and Window tiles.
// Bit 7-6 - Shade for Color Number 3
// Bit 5-4 - Shade for Color Number 2
// Bit 3-2 - Shade for Color Number 1
// Bit 1-0 - Shade for Color Number 0
// The four possible gray shades are:
// 0 White
// 1 Light gray
// 2 Dark gray
// 3 Black
fn get_gray_shades(v: u8, i: usize) -> GrayShades {
match v >> (2 * i) & 0x03 {
0x00 => GrayShades::White,
0x01 => GrayShades::Light,
0x02 => GrayShades::Dark,
_ => GrayShades::Black,
}
}
// Grey scale.
fn set_gre(&mut self, x: usize, g: u8) {
self.data[self.ly as usize][x] = [g, g, g];
}
// When developing graphics on PCs, note that the RGB values will have different appearance on CGB displays as on
// VGA/HDMI monitors calibrated to sRGB color. Because the GBC is not lit, the highest intensity will produce Light
// Gray color rather than White. The intensities are not linear; the values 10h-1Fh will all appear very bright,
// while medium and darker colors are ranged at 00h-0Fh.
// The CGB display's pigments aren't perfectly saturated. This means the colors mix quite oddly; increasing
// intensity of only one R,G,B color will also influence the other two R,G,B colors. For example, a color setting
// of 03EFh (Blue=0, Green=1Fh, Red=0Fh) will appear as Neon Green on VGA displays, but on the CGB it'll produce a
// decently washed out Yellow. See image on the right.
fn set_rgb(&mut self, x: usize, r: u8, g: u8, b: u8) {
assert!(r <= 0x1f);
assert!(g <= 0x1f);
assert!(b <= 0x1f);
let r = u32::from(r);
let g = u32::from(g);
let b = u32::from(b);
let lr = ((r * 13 + g * 2 + b) >> 1) as u8;
let lg = ((g * 3 + b) << 1) as u8;
let lb = ((r * 3 + g * 2 + b * 11) >> 1) as u8;
self.data[self.ly as usize][x] = [lr, lg, lb];
}
pub fn next(&mut self, cycles: u32) {
if !self.lcdc.bit7() {
return;
}
self.h_blank = false;
// The LCD controller operates on a 222 Hz = 4.194 MHz dot clock. An entire frame is 154 scanlines, 70224 dots,
// or 16.74 ms. On scanlines 0 through 143, the LCD controller cycles through modes 2, 3, and 0 once every 456
// dots. Scanlines 144 through 153 are mode 1.
//
// 1 scanline = 456 dots
//
// The following are typical when the display is enabled:
// Mode 2 2_____2_____2_____2_____2_____2___________________2____
// Mode 3 _33____33____33____33____33____33__________________3___
// Mode 0 ___000___000___000___000___000___000________________000
// Mode 1 ____________________________________11111111111111_____
if cycles == 0 {
return;
}
let c = (cycles - 1) / 80 + 1;
for i in 0..c {
if i == (c - 1) {
self.dots += cycles % 80
} else {
self.dots += 80
}
let d = self.dots;
self.dots %= 456;
if d != self.dots {
self.ly = (self.ly + 1) % 154;
if self.stat.enable_ly_interrupt && self.ly == self.lc {
self.intf.borrow_mut().hi(Flag::LCDStat);
}
}
if self.ly >= 144 {
if self.stat.mode == 1 {
continue;
}
self.stat.mode = 1;
self.v_blank = true;
self.intf.borrow_mut().hi(Flag::VBlank);
if self.stat.enable_m1_interrupt {
self.intf.borrow_mut().hi(Flag::LCDStat);
}
} else if self.dots <= 80 {
if self.stat.mode == 2 {
continue;
}
self.stat.mode = 2;
if self.stat.enable_m2_interrupt {
self.intf.borrow_mut().hi(Flag::LCDStat);
}
} else if self.dots <= (80 + 172) {
self.stat.mode = 3;
} else {
if self.stat.mode == 0 {
continue;
}
self.stat.mode = 0;
self.h_blank = true;
if self.stat.enable_m0_interrupt {
self.intf.borrow_mut().hi(Flag::LCDStat);
}
// Render scanline
if self.term == Term::GBC || self.lcdc.bit0() {
self.draw_bg();
}
if self.lcdc.bit1() {
self.draw_sprites();
}
}
}
}
fn draw_bg(&mut self) {
let show_window = self.lcdc.bit5() && self.wy <= self.ly;
let tile_base = if self.lcdc.bit4() { 0x8000 } else { 0x8800 };
let wx = self.wx.wrapping_sub(7);
let py = if show_window {
self.ly.wrapping_sub(self.wy)
} else {
self.sy.wrapping_add(self.ly)
};
let ty = (u16::from(py) >> 3) & 31;
for x in 0..SCREEN_W {
let px = if show_window && x as u8 >= wx {
x as u8 - wx
} else {
self.sx.wrapping_add(x as u8)
};
let tx = (u16::from(px) >> 3) & 31;
// Background memory base addr.
let bg_base = if show_window && x as u8 >= wx {
if self.lcdc.bit6() {
0x9c00
} else {
0x9800
}
} else if self.lcdc.bit3() {
0x9c00
} else {
0x9800
};
// Tile data
// Each tile is sized 8x8 pixels and has a color depth of 4 colors/gray shades.
// Each tile occupies 16 bytes, where each 2 bytes represent a line:
// Byte 0-1 First Line (Upper 8 pixels)
// Byte 2-3 Next Line
// etc.
let tile_addr = bg_base + ty * 32 + tx;
let tile_number = self.get_ram0(tile_addr);
let tile_offset = if self.lcdc.bit4() {
i16::from(tile_number)
} else {
i16::from(tile_number as i8) + 128
} as u16
* 16;
let tile_location = tile_base + tile_offset;
let tile_attr = Attr::from(self.get_ram1(tile_addr));
let tile_y = if tile_attr.yflip { 7 - py % 8 } else { py % 8 };
let tile_y_data: [u8; 2] = if self.term == Term::GBC && tile_attr.bank {
let a = self.get_ram1(tile_location + u16::from(tile_y * 2));
let b = self.get_ram1(tile_location + u16::from(tile_y * 2) + 1);
[a, b]
} else {
let a = self.get_ram0(tile_location + u16::from(tile_y * 2));
let b = self.get_ram0(tile_location + u16::from(tile_y * 2) + 1);
[a, b]
};
let tile_x = if tile_attr.xflip { 7 - px % 8 } else { px % 8 };
// Palettes
let color_l = if tile_y_data[0] & (0x80 >> tile_x) != 0 { 1 } else { 0 };
let color_h = if tile_y_data[1] & (0x80 >> tile_x) != 0 { 2 } else { 0 };
let color = color_h | color_l;
// Priority
self.prio[x] = (tile_attr.priority, color);
if self.term == Term::GBC {
let r = self.cbgpd[tile_attr.palette_number_1][color][0];
let g = self.cbgpd[tile_attr.palette_number_1][color][1];
let b = self.cbgpd[tile_attr.palette_number_1][color][2];
self.set_rgb(x as usize, r, g, b);
} else {
let color = Self::get_gray_shades(self.bgp, color) as u8;
self.set_gre(x, color);
}
}
}
// Gameboy video controller can display up to 40 sprites either in 8x8 or in 8x16 pixels. Because of a limitation
// of hardware, only ten sprites can be displayed per scan line. Sprite patterns have the same format as BG tiles,
// but they are taken from the Sprite Pattern Table located at $8000-8FFF and have unsigned numbering.
//
// Sprite attributes reside in the Sprite Attribute Table (OAM - Object Attribute Memory) at $FE00-FE9F. Each of
// the 40 entries consists of four bytes with the following meanings:
// Byte0 - Y Position
// Specifies the sprites vertical position on the screen (minus 16). An off-screen value (for example, Y=0 or
// Y>=160) hides the sprite.
//
// Byte1 - X Position
// Specifies the sprites horizontal position on the screen (minus 8). An off-screen value (X=0 or X>=168) hides
// the sprite, but the sprite still affects the priority ordering - a better way to hide a sprite is to set its
// Y-coordinate off-screen.
//
// Byte2 - Tile/Pattern Number
// Specifies the sprites Tile Number (00-FF). This (unsigned) value selects a tile from memory at 8000h-8FFFh. In
// CGB Mode this could be either in VRAM Bank 0 or 1, depending on Bit 3 of the following byte. In 8x16 mode, the
// lower bit of the tile number is ignored. IE: the upper 8x8 tile is "NN AND FEh", and the lower 8x8 tile is
// "NN OR 01h".
//
// Byte3 - Attributes/Flags:
// Bit7 OBJ-to-BG Priority (0=OBJ Above BG, 1=OBJ Behind BG color 1-3)
// (Used for both BG and Window. BG color 0 is always behind OBJ)
// Bit6 Y flip (0=Normal, 1=Vertically mirrored)
// Bit5 X flip (0=Normal, 1=Horizontally mirrored)
// Bit4 Palette number **Non CGB Mode Only** (0=OBP0, 1=OBP1)
// Bit3 Tile VRAM-Bank **CGB Mode Only** (0=Bank 0, 1=Bank 1)
// Bit2-0 Palette number **CGB Mode Only** (OBP0-7)
fn draw_sprites(&mut self) {
// Sprite tile size 8x8 or 8x16(2 stacked vertically).
let sprite_size = if self.lcdc.bit2() { 16 } else { 8 };
for i in 0..40 {
let sprite_addr = 0xfe00 + (i as u16) * 4;
let py = self.get(sprite_addr).wrapping_sub(16);
let px = self.get(sprite_addr + 1).wrapping_sub(8);
let tile_number = self.get(sprite_addr + 2) & if self.lcdc.bit2() { 0xfe } else { 0xff };
let tile_attr = Attr::from(self.get(sprite_addr + 3));
// If this is true the scanline is out of the area we care about
if py <= 0xff - sprite_size + 1 {
if self.ly < py || self.ly > py + sprite_size - 1 {
continue;
}
} else {
if self.ly > py.wrapping_add(sprite_size) - 1 {
continue;
}
}
if px >= (SCREEN_W as u8) && px <= (0xff - 7) {
continue;
}
let tile_y = if tile_attr.yflip {
sprite_size - 1 - self.ly.wrapping_sub(py)
} else {
self.ly.wrapping_sub(py)
};
let tile_y_addr = 0x8000u16 + u16::from(tile_number) * 16 + u16::from(tile_y) * 2;
let tile_y_data: [u8; 2] = if self.term == Term::GBC && tile_attr.bank {
let b1 = self.get_ram1(tile_y_addr);
let b2 = self.get_ram1(tile_y_addr + 1);
[b1, b2]
} else {
let b1 = self.get_ram0(tile_y_addr);
let b2 = self.get_ram0(tile_y_addr + 1);
[b1, b2]
};
for x in 0..8 {
if px.wrapping_add(x) >= (SCREEN_W as u8) {
continue;
}
let tile_x = if tile_attr.xflip { 7 - x } else { x };
// Palettes
let color_l = if tile_y_data[0] & (0x80 >> tile_x) != 0 { 1 } else { 0 };
let color_h = if tile_y_data[1] & (0x80 >> tile_x) != 0 { 2 } else { 0 };
let color = color_h | color_l;
if color == 0 {
continue;
}
// Confirm the priority of background and sprite.
let prio = self.prio[px.wrapping_add(x) as usize];
let skip = if self.term == Term::GBC && !self.lcdc.bit0() {
prio.1 == 0
} else if prio.0 {
prio.1 != 0
} else {
tile_attr.priority && prio.1 != 0
};
if skip {
continue;
}
if self.term == Term::GBC {
let r = self.cobpd[tile_attr.palette_number_1][color][0];
let g = self.cobpd[tile_attr.palette_number_1][color][1];
let b = self.cobpd[tile_attr.palette_number_1][color][2];
self.set_rgb(px.wrapping_add(x) as usize, r, g, b);
} else {
let color = if tile_attr.palette_number_0 == 1 {
Self::get_gray_shades(self.op1, color) as u8
} else {
Self::get_gray_shades(self.op0, color) as u8
};
self.set_gre(px.wrapping_add(x) as usize, color);
}
}
}
}
}
impl Memory for Gpu {
fn get(&self, a: u16) -> u8 {
match a {
0x8000...0x9fff => self.ram[self.ram_bank * 0x2000 + a as usize - 0x8000],
0xfe00...0xfe9f => self.oam[a as usize - 0xfe00],
0xff40 => self.lcdc.data,
0xff41 => {
let bit6 = if self.stat.enable_ly_interrupt { 0x40 } else { 0x00 };
let bit5 = if self.stat.enable_m2_interrupt { 0x20 } else { 0x00 };
let bit4 = if self.stat.enable_m1_interrupt { 0x10 } else { 0x00 };
let bit3 = if self.stat.enable_m0_interrupt { 0x08 } else { 0x00 };
let bit2 = if self.ly == self.lc { 0x04 } else { 0x00 };
bit6 | bit5 | bit4 | bit3 | bit2 | self.stat.mode
}
0xff42 => self.sy,
0xff43 => self.sx,
0xff44 => self.ly,
0xff45 => self.lc,
0xff47 => self.bgp,
0xff48 => self.op0,
0xff49 => self.op1,
0xff4a => self.wy,
0xff4b => self.wx,
0xff4f => 0xfe | self.ram_bank as u8,
0xff68 => self.cbgpi.get(),
0xff69 => {
let r = self.cbgpi.i as usize >> 3;
let c = self.cbgpi.i as usize >> 1 & 0x3;
if self.cbgpi.i & 0x01 == 0x00 {
let a = self.cbgpd[r][c][0];
let b = self.cbgpd[r][c][1] << 5;
a | b
} else {
let a = self.cbgpd[r][c][1] >> 3;
let b = self.cbgpd[r][c][2] << 2;
a | b
}
}
0xff6a => self.cobpi.get(),
0xff6b => {
let r = self.cobpi.i as usize >> 3;
let c = self.cobpi.i as usize >> 1 & 0x3;
if self.cobpi.i & 0x01 == 0x00 {
let a = self.cobpd[r][c][0];
let b = self.cobpd[r][c][1] << 5;
a | b
} else {
let a = self.cobpd[r][c][1] >> 3;
let b = self.cobpd[r][c][2] << 2;
a | b
}
}
_ => panic!(""),
}
}
fn set(&mut self, a: u16, v: u8) {
match a {
0x8000...0x9fff => self.ram[self.ram_bank * 0x2000 + a as usize - 0x8000] = v,
0xfe00...0xfe9f => self.oam[a as usize - 0xfe00] = v,
0xff40 => {
self.lcdc.data = v;
if !self.lcdc.bit7() {
self.dots = 0;
self.ly = 0;
self.stat.mode = 0;
// Clean screen.
self.data = [[[0xffu8; 3]; SCREEN_W]; SCREEN_H];
self.v_blank = true;
}
}
0xff41 => {
self.stat.enable_ly_interrupt = v & 0x40 != 0x00;
self.stat.enable_m2_interrupt = v & 0x20 != 0x00;
self.stat.enable_m1_interrupt = v & 0x10 != 0x00;
self.stat.enable_m0_interrupt = v & 0x08 != 0x00;
}
0xff42 => self.sy = v,
0xff43 => self.sx = v,
0xff44 => {}
0xff45 => self.lc = v,
0xff47 => self.bgp = v,
0xff48 => self.op0 = v,
0xff49 => self.op1 = v,
0xff4a => self.wy = v,
0xff4b => self.wx = v,
0xff4f => self.ram_bank = (v & 0x01) as usize,
0xff68 => self.cbgpi.set(v),
0xff69 => {
let r = self.cbgpi.i as usize >> 3;
let c = self.cbgpi.i as usize >> 1 & 0x03;
if self.cbgpi.i & 0x01 == 0x00 {
self.cbgpd[r][c][0] = v & 0x1f;
self.cbgpd[r][c][1] = (self.cbgpd[r][c][1] & 0x18) | (v >> 5);
} else {
self.cbgpd[r][c][1] = (self.cbgpd[r][c][1] & 0x07) | ((v & 0x03) << 3);
self.cbgpd[r][c][2] = (v >> 2) & 0x1f;
}
if self.cbgpi.auto_increment {
self.cbgpi.i += 0x01;
self.cbgpi.i &= 0x3f;
}
}
0xff6a => self.cobpi.set(v),
0xff6b => {
let r = self.cobpi.i as usize >> 3;
let c = self.cobpi.i as usize >> 1 & 0x03;
if self.cobpi.i & 0x01 == 0x00 {
self.cobpd[r][c][0] = v & 0x1f;
self.cobpd[r][c][1] = (self.cobpd[r][c][1] & 0x18) | (v >> 5);
} else {
self.cobpd[r][c][1] = (self.cobpd[r][c][1] & 0x07) | ((v & 0x03) << 3);
self.cobpd[r][c][2] = (v >> 2) & 0x1f;
}
if self.cobpi.auto_increment {
self.cobpi.i += 0x01;
self.cobpi.i &= 0x3f;
}
}
_ => panic!(""),
}
}
}
将编写号的 GPU 模块嵌入内存管理模块, 同时由于 DMA 功能涉及不同模块之间的内存读写, 因此需要在内存管理模块中实现 DMA 逻辑.
pub struct Mmunit {
pub gpu: Gpu,
hdma: Hdma,
hram: [u8; 0x7f],
wram: [u8; 0x8000],
wram_bank: usize,
}
impl Mmunit {
fn run_dma(&mut self) -> u32 {
if !self.hdma.active {
return 0;
}
match self.hdma.mode {
HdmaMode::Gdma => {
let len = u32::from(self.hdma.remain) + 1;
for _ in 0..len {
self.run_dma_hrampart();
}
self.hdma.active = false;
len * 8
}
HdmaMode::Hdma => {
if !self.gpu.h_blank {
return 0;
}
self.run_dma_hrampart();
if self.hdma.remain == 0x7f {
self.hdma.active = false;
}
8
}
}
}
fn run_dma_hrampart(&mut self) {
let mmu_src = self.hdma.src;
for i in 0..0x10 {
let b: u8 = self.get(mmu_src + i);
self.gpu.set(self.hdma.dst + i, b);
}
self.hdma.src += 0x10;
self.hdma.dst += 0x10;
if self.hdma.remain == 0 {
self.hdma.remain = 0x7f;
} else {
self.hdma.remain -= 1;
}
}
}
impl Memory for Mmunit {
fn get(&self, a: u16) -> u8 {
match a {
0x8000...0x9fff => self.gpu.get(a),
0xc000...0xcfff => self.wram[a as usize - 0xc000],
0xd000...0xdfff => self.wram[a as usize - 0xd000 + 0x1000 * self.wram_bank],
0xe000...0xefff => self.wram[a as usize - 0xe000],
0xf000...0xfdff => self.wram[a as usize - 0xf000 + 0x1000 * self.wram_bank],
0xfe00...0xfe9f => self.gpu.get(a),
0xff40...0xff45 | 0xff47...0xff4b | 0xff4f => self.gpu.get(a),
0xff51...0xff55 => self.hdma.get(a),
0xff68...0xff6b => self.gpu.get(a),
0xff70 => self.wram_bank as u8,
0xff80...0xfffe => self.hram[a as usize - 0xff80],
}
}
fn set(&mut self, a: u16, v: u8) {
match a {
0x8000...0x9fff => self.gpu.set(a, v),
0xa000...0xbfff => self.cartridge.set(a, v),
0xc000...0xcfff => self.wram[a as usize - 0xc000] = v,
0xd000...0xdfff => self.wram[a as usize - 0xd000 + 0x1000 * self.wram_bank] = v,
0xe000...0xefff => self.wram[a as usize - 0xe000] = v,
0xf000...0xfdff => self.wram[a as usize - 0xf000 + 0x1000 * self.wram_bank] = v,
0xfe00...0xfe9f => self.gpu.set(a, v),
0xff46 => {
// Writing to this register launches a DMA transfer from ROM or RAM to OAM memory (sprite attribute
// table).
// See: http://gbdev.gg8.se/wiki/articles/Video_Display#FF46_-_DMA_-_DMA_Transfer_and_Start_Address_.28R.2FW.29
assert!(v <= 0xf1);
let base = u16::from(v) << 8;
for i in 0..0xa0 {
let b = self.get(base + i);
self.set(0xfe00 + i, b);
}
}
0xff40...0xff45 | 0xff47...0xff4b | 0xff4f => self.gpu.set(a, v),
0xff51...0xff55 => self.hdma.set(a, v),
0xff68...0xff6b => self.gpu.set(a, v),
0xff70 => {
self.wram_bank = match v & 0x7 {
0 => 1,
n => n as usize,
};
}
0xff80...0xfffe => self.hram[a as usize - 0xff80] = v,
}
}
}