mirror of
https://gitlab.com/flio/rustation-ng.git
synced 2025-04-02 10:31:55 -04:00
Compare commits
4 commits
3da3160084
...
56d31456ee
Author | SHA1 | Date | |
---|---|---|---|
|
56d31456ee | ||
|
be548a855a | ||
|
39a07768b0 | ||
|
074c663dc6 |
8 changed files with 385 additions and 145 deletions
|
@ -7,11 +7,13 @@ pub trait Bitwise {
|
|||
fn set_bit(&mut self, bitpos: u8, v: bool);
|
||||
|
||||
/// Sets the given bit in self to 1
|
||||
#[test]
|
||||
fn set_bit_h(&mut self, bitpos: u8) {
|
||||
self.set_bit(bitpos, true)
|
||||
}
|
||||
|
||||
/// Sets the given bit in self to 0
|
||||
#[test]
|
||||
fn set_bit_l(&mut self, bitpos: u8) {
|
||||
self.set_bit(bitpos, false)
|
||||
}
|
||||
|
|
32
src/lib.rs
32
src/lib.rs
|
@ -393,22 +393,27 @@ impl Context {
|
|||
}
|
||||
|
||||
fn get_geometry(&self) -> libretro::GameGeometry {
|
||||
let upscaling = options::CoreOptions::internal_upscale_factor();
|
||||
let upscale_shift = options::CoreOptions::internal_upscale_factor();
|
||||
|
||||
let vram_display_mode = options::CoreOptions::display_full_vram();
|
||||
|
||||
let (w, h) = vram_display_mode.max_resolution();
|
||||
let aspect_ratio = vram_display_mode.aspect_ratio();
|
||||
|
||||
let w = w as u32;
|
||||
let h = h as u32;
|
||||
|
||||
let max_width;
|
||||
let max_height;
|
||||
if w <= 640 && h <= 576 {
|
||||
max_width = (640 * upscaling) as libc::c_uint;
|
||||
max_height = (576 * upscaling) as libc::c_uint;
|
||||
max_width = (640 << upscale_shift) as libc::c_uint;
|
||||
max_height = (576 << upscale_shift) as libc::c_uint;
|
||||
} else {
|
||||
// Normally only used in "weird" modes such as "show full VRAM"
|
||||
max_width = (w * upscaling) as libc::c_uint;
|
||||
max_height = (h * upscaling) as libc::c_uint;
|
||||
//
|
||||
// Does not scale with upscale_shift
|
||||
max_width = w as libc::c_uint;
|
||||
max_height = h as libc::c_uint;
|
||||
}
|
||||
|
||||
libretro::GameGeometry {
|
||||
|
@ -693,6 +698,11 @@ impl libretro::Context for Context {
|
|||
.gpu
|
||||
.set_rasterizer_option(RasterizerOption::DrawPolygons(draw_poly));
|
||||
|
||||
let upscale_shift = options::CoreOptions::internal_upscale_factor();
|
||||
self.psx
|
||||
.gpu
|
||||
.set_rasterizer_option(RasterizerOption::UpscaleShift(upscale_shift));
|
||||
|
||||
self.psx
|
||||
.cd
|
||||
.set_cd_loading_speed(options::CoreOptions::cd_speed() / 2);
|
||||
|
@ -1062,7 +1072,7 @@ pub enum VRamDisplayMode {
|
|||
}
|
||||
|
||||
impl VRamDisplayMode {
|
||||
fn max_resolution(self) -> (u32, u32) {
|
||||
fn max_resolution(self) -> (u16, u16) {
|
||||
match self {
|
||||
// Maximum resolution supported by the PlayStation video output is 640x576. That high a
|
||||
// vertical resolution would mean no blanking however, so it doesn't make a lot of
|
||||
|
@ -1116,8 +1126,8 @@ mod options {
|
|||
|
||||
libretro_variables!(
|
||||
pub struct CoreOptions (prefix = "rustation") {
|
||||
internal_upscale_factor: u32, parse_upscale
|
||||
=> "Internal upscaling factor; 1x (native)";
|
||||
internal_upscale_factor: u8, parse_upscale
|
||||
=> "Internal upscaling factor; 1x (native)|2x|4x";
|
||||
internal_color_depth: u8, parse_u8
|
||||
=> "Internal color depth; dithered 15bpp (native)|24bpp";
|
||||
display_full_vram: VRamDisplayMode, parse_full_vram
|
||||
|
@ -1178,10 +1188,12 @@ mod options {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_upscale(opt: &str) -> Result<u32, <u32 as FromStr>::Err> {
|
||||
fn parse_upscale(opt: &str) -> Result<u8, <u8 as FromStr>::Err> {
|
||||
let num = opt.trim_matches(|c: char| !c.is_numeric());
|
||||
|
||||
num.parse()
|
||||
let n: u8 = num.parse()?;
|
||||
|
||||
Ok(n.ilog2() as u8)
|
||||
}
|
||||
|
||||
fn parse_percent(opt: &str) -> Result<f32, <u32 as FromStr>::Err> {
|
||||
|
|
|
@ -22,9 +22,11 @@ pub struct Metadata {
|
|||
///
|
||||
/// This value can be set to `None` if the correct address has not been determined for this
|
||||
/// particular BIOS.
|
||||
#[allow(dead_code)]
|
||||
pub animation_jump_hook: Option<u32>,
|
||||
/// Method used to patch the BIOS to enable the debug UART or `None` if the method hasn't been
|
||||
/// found.
|
||||
#[allow(dead_code)]
|
||||
pub patch_debug_uart: Option<fn(&mut Bios)>,
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
//! The timings code is copied from mednafen
|
||||
|
||||
use super::cop0::Exception;
|
||||
use super::{cop0, debugger, map, AccessWidth, Addressable, CycleCount, Psx};
|
||||
use super::{cop0, map, AccessWidth, Addressable, CycleCount, Psx};
|
||||
|
||||
#[cfg(feature = "debugger")]
|
||||
use super::debugger;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
|
@ -268,7 +271,10 @@ pub fn run_next_instruction(psx: &mut Psx) {
|
|||
psx.cpu.branch = false;
|
||||
|
||||
// Debugger entrypoint: used for code breakpoints and stepping
|
||||
debugger::pc_change(psx);
|
||||
#[cfg(feature = "debugger")]
|
||||
{
|
||||
debugger::pc_change(psx);
|
||||
}
|
||||
|
||||
if psx.cpu.current_pc % 4 != 0 {
|
||||
// PC is not correctly aligned!
|
||||
|
@ -428,7 +434,10 @@ fn store<T: Addressable>(psx: &mut Psx, addr: u32, v: T) {
|
|||
return;
|
||||
}
|
||||
|
||||
debugger::memory_write(psx, addr);
|
||||
#[cfg(feature = "debugger")]
|
||||
{
|
||||
debugger::memory_write(psx, addr);
|
||||
}
|
||||
|
||||
psx.store(addr, v);
|
||||
}
|
||||
|
@ -439,7 +448,10 @@ fn load<T: Addressable>(psx: &mut Psx, addr: u32, from_lwc: bool) -> (T, u8) {
|
|||
// Any pending load must terminate before we attempt to start a new one
|
||||
psx.cpu.load_sync();
|
||||
|
||||
debugger::memory_read(psx, addr);
|
||||
#[cfg(feature = "debugger")]
|
||||
{
|
||||
debugger::memory_read(psx, addr);
|
||||
}
|
||||
|
||||
// The Scratch Pad is the CPU data cache, it therefore has very low latency and needs to be
|
||||
// special-cased
|
||||
|
@ -621,12 +633,15 @@ fn op_syscall(psx: &mut Psx, _: Instruction) {
|
|||
|
||||
/// Break
|
||||
fn op_break(psx: &mut Psx, _: Instruction) {
|
||||
if psx.cpu.debug_on_break {
|
||||
info!("BREAK instruction while debug_on_break is active");
|
||||
debugger::trigger_break(psx);
|
||||
} else {
|
||||
exception(psx, Exception::Break);
|
||||
#[cfg(feature = "debugger")]
|
||||
{
|
||||
if psx.cpu.debug_on_break {
|
||||
info!("BREAK instruction while debug_on_break is active");
|
||||
debugger::trigger_break(psx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
exception(psx, Exception::Break);
|
||||
}
|
||||
|
||||
/// Block if the current DIV(U) or MULT(U) instruction has not yet finished
|
||||
|
|
|
@ -771,7 +771,7 @@ fn cmd_nop(psx: &mut Psx) {
|
|||
|
||||
/// Placeholder function
|
||||
fn cmd_unimplemented(psx: &mut Psx) {
|
||||
unimplemented!("GPU command {:08x}", psx.gpu.command_fifo.pop());
|
||||
warn!("GPU command {:08x}", psx.gpu.command_fifo.pop());
|
||||
}
|
||||
|
||||
/// LUT for all GP0 commands (indexed by opcode, bits[31:24] of the first command word)
|
||||
|
|
|
@ -10,13 +10,15 @@ use crate::psx::gpu::commands::{vram_access_dimensions, Shaded};
|
|||
use crate::psx::gpu::commands::{NoShading, Position, Transparent};
|
||||
use crate::psx::gpu::commands::{NoTexture, Opaque, ShadingMode, TextureBlending, TextureRaw};
|
||||
use crate::psx::gpu::commands::{TextureMode, TransparencyMode};
|
||||
use crate::BoxArray;
|
||||
use crate::VRamDisplayMode;
|
||||
|
||||
use crate::psx::gpu::{DisplayMode, DrawMode, MaskSettings, TextureWindow, TransparencyFunction};
|
||||
use fixed_point::{FpCoord, FpVar};
|
||||
use serde::de::{self, Deserialize, Deserializer, SeqAccess, Visitor};
|
||||
use serde::ser::{Serialize, SerializeTuple, Serializer};
|
||||
use std::cmp::{max, min};
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug)]
|
||||
enum State {
|
||||
|
@ -31,7 +33,7 @@ enum State {
|
|||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct Rasterizer {
|
||||
vram: BoxArray<Pixel, VRAM_PIXELS>,
|
||||
vram: VRam,
|
||||
state: State,
|
||||
/// Frame currently being drawn
|
||||
#[serde(skip)]
|
||||
|
@ -81,7 +83,6 @@ pub struct Rasterizer {
|
|||
/// position.
|
||||
/// * `[_; 4]`: y % 4 to select the right position in the dithering pattern based on the vram
|
||||
/// position.
|
||||
///
|
||||
/// * `[_; 0x200]`: input value, from 0x000 to 0x1ff. Values above 0xff are saturated to 0xff
|
||||
#[serde(with = "serialize_dither_table")]
|
||||
dither_table: [[[u8; 0x200]; 4]; 4],
|
||||
|
@ -106,7 +107,7 @@ pub struct Rasterizer {
|
|||
impl Rasterizer {
|
||||
pub fn new() -> Rasterizer {
|
||||
Rasterizer {
|
||||
vram: BoxArray::from_vec(vec![Pixel::black(); 1024 * 512]),
|
||||
vram: VRam::with_upscale_shift(0),
|
||||
state: State::WaitingForCommand,
|
||||
cur_frame: Frame::new(0, 0),
|
||||
clip_x_min: 0,
|
||||
|
@ -209,11 +210,15 @@ impl Rasterizer {
|
|||
let p1 = Pixel::from_mbgr1555((*v >> 16) as u16);
|
||||
|
||||
for &p in [p0, p1].iter() {
|
||||
let vram_off = store.target_vram_offset();
|
||||
let (x, y) = store.target_vram_offset();
|
||||
|
||||
let target = &mut self.vram[vram_off];
|
||||
if self.mask_settings.can_draw_to(*target) {
|
||||
*target = self.mask_settings.mask(p);
|
||||
let target = self.vram.native_pixel(x, y);
|
||||
if self.mask_settings.can_draw_to(target) {
|
||||
self.vram.set_native_pixel(
|
||||
x,
|
||||
y,
|
||||
self.mask_settings.mask(p),
|
||||
);
|
||||
}
|
||||
|
||||
if store.next().is_none() {
|
||||
|
@ -318,9 +323,40 @@ impl Rasterizer {
|
|||
}
|
||||
RasterizerOption::Wireframe(v) => self.draw_wireframe = v,
|
||||
RasterizerOption::DrawPolygons(v) => self.draw_polygons = v,
|
||||
RasterizerOption::UpscaleShift(v) => self.set_upscale_shift(v),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_upscale_shift(&mut self, upscale_shift: u8) {
|
||||
if self.vram.upscale_shift == upscale_shift {
|
||||
return;
|
||||
}
|
||||
|
||||
self.clip_x_min >>= self.vram.upscale_shift;
|
||||
self.clip_y_min >>= self.vram.upscale_shift;
|
||||
self.clip_x_max >>= self.vram.upscale_shift;
|
||||
self.clip_x_max >>= self.vram.upscale_shift;
|
||||
|
||||
self.clip_x_min <<= upscale_shift;
|
||||
self.clip_y_min <<= upscale_shift;
|
||||
self.clip_x_max <<= upscale_shift;
|
||||
self.clip_x_max <<= upscale_shift;
|
||||
|
||||
// The clip is inclusive, so we need to offset when upscaling
|
||||
self.clip_x_max += (1 << self.vram.upscale_shift) - 1;
|
||||
self.clip_y_max += (1 << self.vram.upscale_shift) - 1;
|
||||
|
||||
let mut vram = VRam::with_upscale_shift(upscale_shift);
|
||||
|
||||
for y in 0..512 {
|
||||
for x in 0..1024 {
|
||||
vram.set_native_pixel(x, y, self.vram.native_pixel(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
self.vram = vram;
|
||||
}
|
||||
|
||||
/// Called when we should output a line to the output buffer
|
||||
pub fn finish_line(&mut self, line: u16) {
|
||||
if self.vram_display_mode != VRamDisplayMode::Native {
|
||||
|
@ -349,9 +385,9 @@ impl Rasterizer {
|
|||
}
|
||||
|
||||
fn output_line(&mut self, x_start: u16, vram_y: u16, frame_y: u16) {
|
||||
let x_start = x_start as u32;
|
||||
let frame_y = frame_y as u32;
|
||||
let vram_y = vram_y as i32;
|
||||
let x_start = u32::from(x_start) << self.vram.upscale_shift;
|
||||
let frame_y = u32::from(frame_y) << self.vram.upscale_shift;
|
||||
let vram_y = i32::from(vram_y) << self.vram.upscale_shift;
|
||||
|
||||
if frame_y >= self.cur_frame.height {
|
||||
// Out-of-frame. This should only happen if the video mode changed within the current
|
||||
|
@ -359,7 +395,9 @@ impl Rasterizer {
|
|||
return;
|
||||
}
|
||||
|
||||
let width = min(self.cur_frame.width, self.display_mode.xres() as u32);
|
||||
let xres = u32::from(self.display_mode.xres()) << self.vram.upscale_shift;
|
||||
|
||||
let width = min(self.cur_frame.width, xres);
|
||||
|
||||
if self.display_mode.output_24bpp() {
|
||||
// GPU is in 24bpp mode, we need to do some bitwise magic to recreate the values
|
||||
|
@ -395,9 +433,12 @@ impl Rasterizer {
|
|||
}
|
||||
} else {
|
||||
// GPU outputs pixels "normally", 15bpp native
|
||||
for x in 0..width {
|
||||
let p = self.read_pixel((x_start + x) as i32, vram_y);
|
||||
self.cur_frame.set_pixel(x, frame_y, p.to_rgb888());
|
||||
for y in 0..(1i32 << self.vram.upscale_shift) {
|
||||
for x in 0..width {
|
||||
let p = self.read_pixel((x_start + x) as i32, vram_y + y);
|
||||
self.cur_frame
|
||||
.set_pixel(x, frame_y + (y as u32), p.to_rgb888());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -453,7 +494,10 @@ impl Rasterizer {
|
|||
height -= 1;
|
||||
}
|
||||
|
||||
(width as u32, height as u32)
|
||||
let w = u32::from(width) << self.vram.upscale_shift;
|
||||
let h = u32::from(height) << self.vram.upscale_shift;
|
||||
|
||||
(w, h)
|
||||
}
|
||||
mode => {
|
||||
let (w, h) = mode.max_resolution();
|
||||
|
@ -464,37 +508,42 @@ impl Rasterizer {
|
|||
// It's possible for this to fail if `vram_display_mode` was just switched and the
|
||||
// current frame doesn't have the proper dimensions. In this case we're going to return
|
||||
// a partially drawn leftover frame which is probably not too important.
|
||||
if self.cur_frame.width >= w && self.cur_frame.height >= h {
|
||||
for i in 0..((w * h) as usize) {
|
||||
self.cur_frame.pixels[i] = match mode {
|
||||
VRamDisplayMode::Native => unreachable!(),
|
||||
VRamDisplayMode::Full16bpp => self.vram[i].to_rgb888(),
|
||||
VRamDisplayMode::Full8bpp => {
|
||||
let p = self.vram[i >> 1].vram_bytes();
|
||||
if self.cur_frame.width >= u32::from(w) && self.cur_frame.height >= u32::from(h) {
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let px_off = usize::from(y) * usize::from(w) + usize::from(x);
|
||||
self.cur_frame.pixels[px_off] = match mode {
|
||||
VRamDisplayMode::Native => unreachable!(),
|
||||
VRamDisplayMode::Full16bpp => {
|
||||
self.vram.native_pixel(x, y).to_rgb888()
|
||||
}
|
||||
VRamDisplayMode::Full8bpp => {
|
||||
let p = self.vram.native_pixel(x >> 1, y).vram_bytes();
|
||||
|
||||
let g = p[i & 1];
|
||||
let g = p[usize::from(x & 1)];
|
||||
|
||||
Pixel::from_rgb(g, g, g).0
|
||||
}
|
||||
VRamDisplayMode::Full4bpp => {
|
||||
let p = self.vram[i >> 2].to_mbgr1555();
|
||||
let n = [
|
||||
(p & 0xf) as u8,
|
||||
((p >> 4) & 0xf) as u8,
|
||||
((p >> 8) & 0xf) as u8,
|
||||
((p >> 12) & 0xf) as u8,
|
||||
];
|
||||
Pixel::from_rgb(g, g, g).0
|
||||
}
|
||||
VRamDisplayMode::Full4bpp => {
|
||||
let p = self.vram.native_pixel(x >> 2, y).to_mbgr1555();
|
||||
let n = [
|
||||
(p & 0xf) as u8,
|
||||
((p >> 4) & 0xf) as u8,
|
||||
((p >> 8) & 0xf) as u8,
|
||||
((p >> 12) & 0xf) as u8,
|
||||
];
|
||||
|
||||
let g = n[i & 3];
|
||||
let g = g | g << 4;
|
||||
let g = n[usize::from(x & 3)];
|
||||
let g = g | g << 4;
|
||||
|
||||
Pixel::from_rgb(g, g, g).0
|
||||
}
|
||||
};
|
||||
Pixel::from_rgb(g, g, g).0
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(w, h)
|
||||
(u32::from(w), u32::from(h))
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -513,23 +562,17 @@ impl Rasterizer {
|
|||
/// Create a new frame with the given `width` and `height` and containing the pixels in the VRAM
|
||||
/// region locatied at `left`x`top`. Used to implement VRAM reads
|
||||
fn copy_vram_rect(&mut self, left: u16, top: u16, width: u16, height: u16) -> Frame {
|
||||
let left = left as u32;
|
||||
let top = top as u32;
|
||||
let height = height as u32;
|
||||
let width = width as u32;
|
||||
|
||||
let mut frame = Frame::new(width, height);
|
||||
let mut frame = Frame::new(u32::from(width), u32::from(height));
|
||||
|
||||
for y in 0..height {
|
||||
let y_off = ((top + y) & 0x1ff) * 1024;
|
||||
let vram_y = (top + y) & 0x1ff;
|
||||
for x in 0..width {
|
||||
let x_off = (left + x) & 0x3ff;
|
||||
let vram_pos = y_off + x_off;
|
||||
let vram_x = (left + x) & 0x3ff;
|
||||
|
||||
let p = self.vram[vram_pos as usize];
|
||||
let p = self.vram.native_pixel(vram_x, vram_y);
|
||||
// XXX I reuse Frame here for simplicity but since VRAM loads are 16 bits we waste
|
||||
// half the storage here.
|
||||
frame.set_pixel(x, y, p.to_mbgr1555() as u32);
|
||||
frame.set_pixel(u32::from(x), u32::from(y), u32::from(p.to_mbgr1555()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -616,13 +659,21 @@ impl Rasterizer {
|
|||
}
|
||||
|
||||
fn read_pixel(&self, x: i32, y: i32) -> Pixel {
|
||||
debug_assert!((0..1024).contains(&x), "x out of bounds ({})", x);
|
||||
debug_assert!((0..1024).contains(&y), "y out of bounds ({})", y);
|
||||
debug_assert!(
|
||||
(0..(1024 << self.vram.upscale_shift)).contains(&x),
|
||||
"x out of bounds ({})",
|
||||
x
|
||||
);
|
||||
debug_assert!(
|
||||
(0..(1024 << self.vram.upscale_shift)).contains(&y),
|
||||
"y out of bounds ({})",
|
||||
y
|
||||
);
|
||||
|
||||
let y = y & 0x1ff;
|
||||
let vram_off = y * 1024 + x;
|
||||
let y = (y & ((0x200 << self.vram.upscale_shift) - 1)) as u32;
|
||||
let x = x as u32;
|
||||
|
||||
self.vram[vram_off as usize]
|
||||
self.vram.pixel(x, y)
|
||||
}
|
||||
|
||||
fn draw_pixel<Transparency, Texture>(&mut self, x: i32, y: i32, mut color: Pixel)
|
||||
|
@ -630,18 +681,25 @@ impl Rasterizer {
|
|||
Transparency: TransparencyMode,
|
||||
Texture: TextureMode,
|
||||
{
|
||||
debug_assert!((0..1024).contains(&x), "x out of bounds ({})", x);
|
||||
debug_assert!((0..1024).contains(&y), "y out of bounds ({})", y);
|
||||
debug_assert!(
|
||||
(0..(1024 << self.vram.upscale_shift)).contains(&x),
|
||||
"x out of bounds ({})",
|
||||
x
|
||||
);
|
||||
debug_assert!(
|
||||
(0..(1024 << self.vram.upscale_shift)).contains(&y),
|
||||
"y out of bounds ({})",
|
||||
y
|
||||
);
|
||||
|
||||
// Apparently the PlayStation GPU supports 2MB VRAM (1024x1024, used in some arcade
|
||||
// machines apparently) but the bottom half isn't installed so it wraps around.
|
||||
let y = y & 0x1ff;
|
||||
let y = (y & ((0x200 << self.vram.upscale_shift) - 1)) as u32;
|
||||
let x = x as u32;
|
||||
|
||||
let vram_off = y * 1024 + x;
|
||||
let bg_pixel = self.vram.pixel(x, y);
|
||||
|
||||
let vram_pixel = &mut self.vram[vram_off as usize];
|
||||
|
||||
if !self.mask_settings.can_draw_to(*vram_pixel) {
|
||||
if !self.mask_settings.can_draw_to(bg_pixel) {
|
||||
// Masked
|
||||
return;
|
||||
}
|
||||
|
@ -652,7 +710,6 @@ impl Rasterizer {
|
|||
Transparency::is_transparent() && (!Texture::is_textured() || color.mask());
|
||||
|
||||
if is_transparent {
|
||||
let bg_pixel = *vram_pixel;
|
||||
let mode = self.tex_mapper.draw_mode.transparency_mode();
|
||||
|
||||
// XXX if we wanted to be extra-accurate we might want to truncate the color here to
|
||||
|
@ -665,14 +722,12 @@ impl Rasterizer {
|
|||
color.set_mask();
|
||||
}
|
||||
} else if self.force_transparency {
|
||||
let bg_pixel = *vram_pixel;
|
||||
|
||||
color.apply_transparency(bg_pixel, TransparencyFunction::Average);
|
||||
}
|
||||
|
||||
color = self.mask_settings.mask(color);
|
||||
|
||||
*vram_pixel = color;
|
||||
self.vram.set_pixel(x, y, color);
|
||||
}
|
||||
|
||||
fn draw_triangle<Transparency, Texture, Shading>(&mut self, mut vertices: [Vertex; 3])
|
||||
|
@ -720,7 +775,7 @@ impl Rasterizer {
|
|||
let y_min = a.position.y;
|
||||
let y_max = c.position.y;
|
||||
|
||||
if y_max - y_min >= 512 {
|
||||
if y_max - y_min >= (512 << self.vram.upscale_shift) {
|
||||
// Triangle is too tall, give up
|
||||
return;
|
||||
}
|
||||
|
@ -749,7 +804,7 @@ impl Rasterizer {
|
|||
|
||||
let x_max = vertices.iter().map(|v| v.position.x).max().unwrap();
|
||||
|
||||
if x_max - x_min >= 1024 {
|
||||
if x_max - x_min >= (1024 << self.vram.upscale_shift) {
|
||||
// Triangle is too large, give up
|
||||
return;
|
||||
}
|
||||
|
@ -1092,28 +1147,34 @@ impl Rasterizer {
|
|||
(0, 0)
|
||||
};
|
||||
|
||||
/* We always draw rects at native res */
|
||||
let clip_x_min = self.clip_x_min >> self.vram.upscale_shift;
|
||||
let clip_y_min = self.clip_y_min >> self.vram.upscale_shift;
|
||||
let clip_x_max = self.clip_x_max >> self.vram.upscale_shift;
|
||||
let clip_y_max = self.clip_y_max >> self.vram.upscale_shift;
|
||||
|
||||
let mut x_start = origin.x();
|
||||
let x_end = min(x_start + width, self.clip_x_max + 1);
|
||||
let x_end = min(x_start + width, clip_x_max + 1);
|
||||
|
||||
let mut y_start = origin.y();
|
||||
let y_end = min(y_start + height, self.clip_y_max + 1);
|
||||
let y_end = min(y_start + height, clip_y_max + 1);
|
||||
|
||||
if x_start < self.clip_x_min {
|
||||
if x_start < clip_x_min {
|
||||
if Texture::is_textured() {
|
||||
let skip = (self.clip_x_min - x_start) * u_inc;
|
||||
let skip = (clip_x_min - x_start) * u_inc;
|
||||
|
||||
u_start = u_start.wrapping_add(skip as u8);
|
||||
}
|
||||
x_start = self.clip_x_min;
|
||||
x_start = clip_x_min;
|
||||
}
|
||||
|
||||
if y_start < self.clip_y_min {
|
||||
if y_start < clip_y_min {
|
||||
if Texture::is_textured() {
|
||||
let skip = (self.clip_y_min - y_start) * u_inc;
|
||||
let skip = (clip_y_min - y_start) * u_inc;
|
||||
|
||||
v = v.wrapping_add(skip as u8);
|
||||
}
|
||||
y_start = self.clip_y_min;
|
||||
y_start = clip_y_min;
|
||||
}
|
||||
|
||||
if x_end <= x_start || y_end <= y_start {
|
||||
|
@ -1141,18 +1202,32 @@ impl Rasterizer {
|
|||
let texel = self.get_texel(u, v);
|
||||
// If the pixel is equal to 0 (including mask bit) then we don't draw it
|
||||
if !texel.is_nul() {
|
||||
if Texture::is_raw_texture() {
|
||||
self.draw_pixel::<Transparency, Texture>(x, y, texel);
|
||||
} else {
|
||||
// Texture blending: the final color is a combination of the texel and
|
||||
// the solid color. Rect are never dithered.
|
||||
let blend = self.blend(texel, origin.color);
|
||||
self.draw_pixel::<Transparency, Texture>(x, y, blend);
|
||||
for y in
|
||||
(y << self.vram.upscale_shift)..((y + 1) << self.vram.upscale_shift)
|
||||
{
|
||||
for x in
|
||||
(x << self.vram.upscale_shift)..((x + 1) << self.vram.upscale_shift)
|
||||
{
|
||||
if Texture::is_raw_texture() {
|
||||
self.draw_pixel::<Transparency, Texture>(x, y, texel);
|
||||
} else {
|
||||
// Texture blending: the final color is a combination of the texel and
|
||||
// the solid color. Rect are never dithered.
|
||||
let blend = self.blend(texel, origin.color);
|
||||
self.draw_pixel::<Transparency, Texture>(x, y, blend);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No texture
|
||||
self.draw_pixel::<Transparency, Texture>(x, y, color);
|
||||
for y in (y << self.vram.upscale_shift)..((y + 1) << self.vram.upscale_shift) {
|
||||
for x in
|
||||
(x << self.vram.upscale_shift)..((x + 1) << self.vram.upscale_shift)
|
||||
{
|
||||
self.draw_pixel::<Transparency, Texture>(x, y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
u = u.wrapping_add(u_inc as u8);
|
||||
}
|
||||
|
@ -1188,7 +1263,7 @@ impl Rasterizer {
|
|||
return;
|
||||
}
|
||||
|
||||
if dx >= 1024 || dy >= 512 {
|
||||
if dx >= (1024 << self.vram.upscale_shift) || dy >= (512 << self.vram.upscale_shift) {
|
||||
// Line is too long, ignore
|
||||
return;
|
||||
}
|
||||
|
@ -1637,7 +1712,7 @@ impl TextureMapper {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_clut(&mut self, clut: u32, vram: &[Pixel; 1024 * 512]) {
|
||||
pub fn set_clut(&mut self, clut: u32, vram: &VRam) {
|
||||
let pts = self.pixel_to_texel_shift as u32;
|
||||
if pts == 0 {
|
||||
// We're in "truecolor" mode, the palette is not in use
|
||||
|
@ -1656,13 +1731,13 @@ impl TextureMapper {
|
|||
|
||||
self.clut_tag = tag;
|
||||
|
||||
let clut_x = (clut & 0x3f) << 4;
|
||||
let clut_y = (clut >> 6) & 0x1ff;
|
||||
let clut_x = ((clut & 0x3f) << 4) as u16;
|
||||
let clut_y = ((clut >> 6) & 0x1ff) as u16;
|
||||
|
||||
let clut_off = (clut_y * 1024 + clut_x) as usize;
|
||||
// let clut_off = (clut_y * 1024 + clut_x) as usize;
|
||||
|
||||
// 256 for 8bpp, 16 for 4bpp
|
||||
let nentries = 256 >> ((pts - 1) * 4);
|
||||
let nentries = 256u16 >> ((pts - 1) * 4);
|
||||
|
||||
// Dino Crisis 2 sets a weird palette when displaying the Superintendent hologram:
|
||||
//
|
||||
|
@ -1671,7 +1746,7 @@ impl TextureMapper {
|
|||
// Naturally this goes out of bounds, but I presume that we're supposed to wrap around (not
|
||||
// tested on real hardware)
|
||||
for i in 0..nentries {
|
||||
self.clut_cache[i] = vram[(clut_off + i) % (1024 * 512)];
|
||||
self.clut_cache[i as usize] = vram.native_pixel((clut_x + i) & 0x3ff, clut_y)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1717,7 +1792,7 @@ impl TextureMapper {
|
|||
self.v_offset += tp_y;
|
||||
}
|
||||
|
||||
pub fn get_texel(&mut self, u: u8, v: u8, vram: &[Pixel; 1024 * 512]) -> Pixel {
|
||||
pub fn get_texel(&mut self, u: u8, v: u8, vram: &VRam) -> Pixel {
|
||||
let pts = u16::from(self.pixel_to_texel_shift);
|
||||
let fb_u = u16::from(u & self.u_mask) + self.u_offset;
|
||||
let fb_v = u16::from(v & self.v_mask) + self.v_offset;
|
||||
|
@ -1741,19 +1816,18 @@ impl TextureMapper {
|
|||
|
||||
let cacheline = &mut self.texture_cache[usize::from(cache_index)];
|
||||
|
||||
let fb_off = u32::from(fb_y) * 1024 + u32::from(fb_x);
|
||||
let tag = fb_off & !3;
|
||||
let tag = (u32::from(fb_y) * 1024 + u32::from(fb_x)) & !3;
|
||||
|
||||
if cacheline.tag != tag {
|
||||
// Need to fetch the cacheline
|
||||
cacheline.tag = tag;
|
||||
|
||||
let tag = tag as usize;
|
||||
|
||||
cacheline.pixels.copy_from_slice(&vram[tag..(tag + 4)])
|
||||
for i in 0..4u16 {
|
||||
cacheline.pixels[usize::from(i)] = vram.native_pixel((fb_x & !3) + i, fb_y);
|
||||
}
|
||||
}
|
||||
|
||||
let raw = cacheline.pixels[(fb_off & 3) as usize];
|
||||
let raw = cacheline.pixels[usize::from(fb_x & 3)];
|
||||
|
||||
if pts == 0 {
|
||||
// True color mode, we return the texture value as-is
|
||||
|
@ -2053,6 +2127,9 @@ where
|
|||
vertex.position.x = extend_to_i32(vertex.position.x as u32, 11);
|
||||
vertex.position.y = extend_to_i32(vertex.position.y as u32, 11);
|
||||
|
||||
vertex.position.x <<= rasterizer.vram.upscale_shift;
|
||||
vertex.position.y <<= rasterizer.vram.upscale_shift;
|
||||
|
||||
if Texture::is_textured() {
|
||||
if v == 0 {
|
||||
clut = params[index];
|
||||
|
@ -2144,6 +2221,9 @@ where
|
|||
vertex.position.x = extend_to_i32(vertex.position.x as u32, 11);
|
||||
vertex.position.y = extend_to_i32(vertex.position.y as u32, 11);
|
||||
|
||||
vertex.position.x <<= rasterizer.vram.upscale_shift;
|
||||
vertex.position.y <<= rasterizer.vram.upscale_shift;
|
||||
|
||||
if Texture::is_textured() {
|
||||
if v == 0 {
|
||||
clut = params[index];
|
||||
|
@ -2284,6 +2364,8 @@ where
|
|||
|
||||
v.position.x += rasterizer.draw_offset_x;
|
||||
v.position.y += rasterizer.draw_offset_y;
|
||||
v.position.x <<= rasterizer.vram.upscale_shift;
|
||||
v.position.y <<= rasterizer.vram.upscale_shift;
|
||||
|
||||
(opcode as u8, v)
|
||||
}
|
||||
|
@ -2301,6 +2383,9 @@ where
|
|||
end_vertex.position.x += rasterizer.draw_offset_x;
|
||||
end_vertex.position.y += rasterizer.draw_offset_y;
|
||||
|
||||
end_vertex.position.x <<= rasterizer.vram.upscale_shift;
|
||||
end_vertex.position.y <<= rasterizer.vram.upscale_shift;
|
||||
|
||||
rasterizer.draw_line::<Transparency, Shading>(start_vertex, end_vertex.clone());
|
||||
|
||||
rasterizer.state = State::PolyLine(opcode, end_vertex);
|
||||
|
@ -2323,6 +2408,8 @@ where
|
|||
index += 1;
|
||||
start_vertex.position.x += rasterizer.draw_offset_x;
|
||||
start_vertex.position.y += rasterizer.draw_offset_y;
|
||||
start_vertex.position.x <<= rasterizer.vram.upscale_shift;
|
||||
start_vertex.position.y <<= rasterizer.vram.upscale_shift;
|
||||
|
||||
let mut end_vertex = Vertex::new(0);
|
||||
|
||||
|
@ -2337,6 +2424,9 @@ where
|
|||
end_vertex.position.x += rasterizer.draw_offset_x;
|
||||
end_vertex.position.y += rasterizer.draw_offset_y;
|
||||
|
||||
end_vertex.position.x <<= rasterizer.vram.upscale_shift;
|
||||
end_vertex.position.y <<= rasterizer.vram.upscale_shift;
|
||||
|
||||
rasterizer.draw_line::<Transparency, Shading>(start_vertex, end_vertex);
|
||||
}
|
||||
|
||||
|
@ -2365,11 +2455,11 @@ impl VRamStore {
|
|||
}
|
||||
}
|
||||
|
||||
fn target_vram_offset(&self) -> usize {
|
||||
let x = (self.x & 0x3ff) as usize;
|
||||
let y = (self.y & 0x1ff) as usize;
|
||||
fn target_vram_offset(&self) -> (u16, u16) {
|
||||
let x = self.x & 0x3ff;
|
||||
let y = self.y & 0x1ff;
|
||||
|
||||
y * 1024 + x
|
||||
(x, y)
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Option<()> {
|
||||
|
@ -2394,10 +2484,10 @@ fn cmd_vram_copy(rasterizer: &mut Rasterizer, params: &[u32]) {
|
|||
let dst = params[2];
|
||||
let dim = params[3];
|
||||
|
||||
let src_x = (src & 0x3ff) as i32;
|
||||
let src_y = ((src >> 16) & 0x3ff) as i32;
|
||||
let dst_x = (dst & 0x3ff) as i32;
|
||||
let dst_y = ((dst >> 16) & 0x3ff) as i32;
|
||||
let src_x = ((src & 0x3ff) as i32) << rasterizer.vram.upscale_shift;
|
||||
let src_y = (((src >> 16) & 0x3ff) as i32) << rasterizer.vram.upscale_shift;
|
||||
let dst_x = ((dst & 0x3ff) as i32) << rasterizer.vram.upscale_shift;
|
||||
let dst_y = (((dst >> 16) & 0x3ff) as i32) << rasterizer.vram.upscale_shift;
|
||||
|
||||
let (width, height) = vram_access_dimensions(dim, false);
|
||||
|
||||
|
@ -2405,23 +2495,27 @@ fn cmd_vram_copy(rasterizer: &mut Rasterizer, params: &[u32]) {
|
|||
// different buffer?
|
||||
rasterizer.tex_mapper.cache_invalidate();
|
||||
|
||||
let xmask = (0x400 << rasterizer.vram.upscale_shift) - 1;
|
||||
let ymask = (0x200 << rasterizer.vram.upscale_shift) - 1;
|
||||
|
||||
for y in 0..height {
|
||||
let sy = (y + src_y) & 0x1ff;
|
||||
let ty = (y + dst_y) & 0x1ff;
|
||||
let sy = (y + src_y) & ymask;
|
||||
let ty = (y + dst_y) & ymask;
|
||||
|
||||
for x in (0..width).step_by(128) {
|
||||
// The use of a 128px intermediate buffer is taken from mednafen
|
||||
// XXX should we scale with the upscale_shift?
|
||||
let mut copy_buf: [Pixel; 128] = [Pixel(0); 128];
|
||||
|
||||
let w = std::cmp::min(width - x, 128);
|
||||
|
||||
for dx in 0..w {
|
||||
let sx = (src_x + x + dx) & 0x3ff;
|
||||
let sx = (src_x + x + dx) & xmask;
|
||||
copy_buf[dx as usize] = rasterizer.read_pixel(sx, sy);
|
||||
}
|
||||
|
||||
for dx in 0..w {
|
||||
let tx = (dst_x + x + dx) & 0x3ff;
|
||||
let tx = (dst_x + x + dx) & xmask;
|
||||
|
||||
let p = copy_buf[dx as usize];
|
||||
|
||||
|
@ -2475,11 +2569,11 @@ fn cmd_fill_rect(rasterizer: &mut Rasterizer, params: &[u32]) {
|
|||
let dst = params[1];
|
||||
let dim = params[2];
|
||||
|
||||
let start_x = dst & 0x3f0;
|
||||
let start_y = (dst >> 16) & 0x3ff;
|
||||
let start_x = (dst & 0x3f0) as u16;
|
||||
let start_y = ((dst >> 16) & 0x3ff) as u16;
|
||||
|
||||
let width = ((dim & 0x3ff) + 0xf) & !0xf;
|
||||
let height = (dim >> 16) & 0x1ff;
|
||||
let width = (((dim & 0x3ff) + 0xf) & !0xf) as u16;
|
||||
let height = ((dim >> 16) & 0x1ff) as u16;
|
||||
|
||||
// XXX Pretty sure there's no dithering for this commands
|
||||
let color = rasterizer.truncate_color(color);
|
||||
|
@ -2497,8 +2591,7 @@ fn cmd_fill_rect(rasterizer: &mut Rasterizer, params: &[u32]) {
|
|||
// XXX Probably worth adding a test just in case.
|
||||
let x_pos = (start_x + x) & 1023;
|
||||
|
||||
let vram_index = y_pos * 1024 + x_pos;
|
||||
rasterizer.vram[vram_index as usize] = color;
|
||||
rasterizer.vram.set_native_pixel(x_pos, y_pos, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2510,15 +2603,19 @@ fn cmd_tex_window(rasterizer: &mut Rasterizer, params: &[u32]) {
|
|||
fn cmd_clip_top_left(rasterizer: &mut Rasterizer, params: &[u32]) {
|
||||
let clip = params[0];
|
||||
|
||||
rasterizer.clip_x_min = (clip & 0x3ff) as i32;
|
||||
rasterizer.clip_y_min = ((clip >> 10) & 0x3ff) as i32;
|
||||
rasterizer.clip_x_min = ((clip & 0x3ff) as i32) << rasterizer.vram.upscale_shift;
|
||||
rasterizer.clip_y_min = (((clip >> 10) & 0x3ff) as i32) << rasterizer.vram.upscale_shift;
|
||||
}
|
||||
|
||||
fn cmd_clip_bot_right(rasterizer: &mut Rasterizer, params: &[u32]) {
|
||||
let clip = params[0];
|
||||
|
||||
rasterizer.clip_x_max = (clip & 0x3ff) as i32;
|
||||
rasterizer.clip_y_max = ((clip >> 10) & 0x3ff) as i32;
|
||||
rasterizer.clip_x_max = ((clip & 0x3ff) as i32) << rasterizer.vram.upscale_shift;
|
||||
rasterizer.clip_y_max = (((clip >> 10) & 0x3ff) as i32) << rasterizer.vram.upscale_shift;
|
||||
|
||||
// The clip is inclusive, so we need to offset when upscaling
|
||||
rasterizer.clip_x_max += (1 << rasterizer.vram.upscale_shift) - 1;
|
||||
rasterizer.clip_y_max += (1 << rasterizer.vram.upscale_shift) - 1;
|
||||
}
|
||||
|
||||
fn cmd_draw_mode(rasterizer: &mut Rasterizer, params: &[u32]) {
|
||||
|
@ -2551,7 +2648,7 @@ fn cmd_clear_cache(rasterizer: &mut Rasterizer, _params: &[u32]) {
|
|||
|
||||
/// Placeholder function
|
||||
fn cmd_unimplemented(_rasterizer: &mut Rasterizer, params: &[u32]) {
|
||||
unimplemented!("GPU command {:08x}", params[0]);
|
||||
warn!("GPU command {:08x}", params[0]);
|
||||
}
|
||||
|
||||
/// LUT for all GP0 commands (indexed by opcode, bits[31:24] of the first command word)
|
||||
|
@ -3599,9 +3696,6 @@ pub static GP0_COMMANDS: [CommandHandler; 0x100] = [
|
|||
},
|
||||
];
|
||||
|
||||
/// Total number of pixels in the VRAM
|
||||
const VRAM_PIXELS: usize = 1024 * 512;
|
||||
|
||||
mod serialize_dither_table {
|
||||
use serde::{de::Deserializer, ser::Serializer};
|
||||
|
||||
|
@ -3642,3 +3736,116 @@ impl CacheLine {
|
|||
self.tag = CacheLine::TAG_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
struct VRam {
|
||||
pixels: Vec<Pixel>,
|
||||
/// Upscale shift value. 0 for native.
|
||||
upscale_shift: u8,
|
||||
}
|
||||
|
||||
impl VRam {
|
||||
fn with_upscale_shift(upscale_shift: u8) -> VRam {
|
||||
VRam {
|
||||
pixels: vec![Pixel::black(); (1024 << upscale_shift) * (512 << upscale_shift)],
|
||||
upscale_shift,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the pixel at x, y where x an y are in native 1x coordinates.
|
||||
fn native_pixel(&self, x: u16, y: u16) -> Pixel {
|
||||
self.pixel(
|
||||
u32::from(x) << self.upscale_shift,
|
||||
u32::from(y) << self.upscale_shift,
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets the pixel at x, y where x an y are in native 1x coordinates.
|
||||
fn set_native_pixel(&mut self, x: u16, y: u16, p: Pixel) {
|
||||
for yo in 0..(1 << self.upscale_shift) {
|
||||
for xo in 0..(1 << self.upscale_shift) {
|
||||
self.set_pixel(
|
||||
(u32::from(x) << self.upscale_shift) + xo,
|
||||
(u32::from(y) << self.upscale_shift) + yo,
|
||||
p,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the pixel at x, y where x and y are in upscaled coordinates
|
||||
fn pixel(&self, x: u32, y: u32) -> Pixel {
|
||||
let x = x as usize;
|
||||
let y = y as usize;
|
||||
|
||||
self.pixels[(1024 << self.upscale_shift) * y + x]
|
||||
}
|
||||
|
||||
/// Sets the pixel at x, y where x and y are in upscaled coordinates
|
||||
fn set_pixel(&mut self, x: u32, y: u32, p: Pixel) {
|
||||
let x = x as usize;
|
||||
let y = y as usize;
|
||||
|
||||
self.pixels[(1024 << self.upscale_shift) * y + x] = p
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for VRam {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut tup = serializer.serialize_tuple(1024 * 512)?;
|
||||
|
||||
for y in 0..512 {
|
||||
for x in 0..1024 {
|
||||
tup.serialize_element(&self.native_pixel(x, y))?;
|
||||
}
|
||||
}
|
||||
|
||||
tup.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for VRam {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct VRamVisitor {
|
||||
phantom: PhantomData<VRam>,
|
||||
}
|
||||
|
||||
impl<'de> Visitor<'de> for VRamVisitor {
|
||||
type Value = VRam;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "an array of 1024 * 512 pixels")
|
||||
}
|
||||
|
||||
fn visit_seq<S>(self, mut seq: S) -> std::result::Result<VRam, S::Error>
|
||||
where
|
||||
S: SeqAccess<'de>,
|
||||
{
|
||||
let mut vram = VRam::with_upscale_shift(0);
|
||||
|
||||
for y in 0..512 {
|
||||
for x in 0..1024 {
|
||||
let p = match seq.next_element()? {
|
||||
Some(p) => p,
|
||||
None => return Err(de::Error::invalid_length(y * 1024 + x, &self)),
|
||||
};
|
||||
|
||||
vram.set_native_pixel(x as u16, y as u16, p);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(vram)
|
||||
}
|
||||
}
|
||||
|
||||
let visitor = VRamVisitor {
|
||||
phantom: PhantomData,
|
||||
};
|
||||
deserializer.deserialize_tuple(1024 * 512, visitor)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -227,6 +227,7 @@ pub enum RasterizerOption {
|
|||
DitherForceDisable(bool),
|
||||
Wireframe(bool),
|
||||
DrawPolygons(bool),
|
||||
UpscaleShift(u8),
|
||||
}
|
||||
|
||||
/// Buffer containing one rendered frame
|
||||
|
|
|
@ -4,6 +4,7 @@ pub mod bios;
|
|||
pub mod cd;
|
||||
pub mod cop0;
|
||||
pub mod cpu;
|
||||
#[cfg(feature = "debugger")]
|
||||
pub mod debugger;
|
||||
mod dma;
|
||||
pub mod gpu;
|
||||
|
|
Loading…
Add table
Reference in a new issue