-
-
Save whitequark/20fb9b41600a6abfb4f93007c546fa21 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "cgb-grabber" | |
version = "0.1.0" | |
authors = ["whitequark <whitequark@whitequark.org>"] | |
[dependencies] | |
libusb = "0.3" | |
sdl2 = "0.31" | |
gif = "0.10" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extern crate libusb; | |
extern crate sdl2; | |
extern crate gif; | |
use std::slice; | |
use std::time::Duration; | |
use std::io::{self, Read, BufReader}; | |
use std::fs::File; | |
use std::thread; | |
use std::sync::mpsc::{channel, Receiver}; | |
struct Glasgow(Receiver<Vec<u8>>); | |
impl Glasgow { | |
fn new(context: libusb::Context) -> Glasgow { | |
let (sender, receiver) = channel(); | |
thread::spawn(move || { | |
let mut handle = context.open_device_with_vid_pid(0x20b7, 0x9db1) | |
.expect("cannot open device"); | |
handle.write_control(0x40, 0x14, 0x00, 0x03, &[0xe4, 0x0c], Default::default()) | |
.expect("cannot set voltage"); | |
handle.set_active_configuration(1) | |
.expect("cannot set configuration"); | |
handle.detach_kernel_driver(0) | |
.unwrap_or(/* ok if it didn't work */()); | |
handle.claim_interface(0) | |
.expect("cannot claim interface"); | |
loop { | |
let mut buf = Vec::new(); | |
buf.resize(512, 0); | |
handle.read_bulk(0x86, &mut buf[..], Duration::from_millis(1000)) | |
.expect("cannot read buffer"); | |
sender.send(buf) | |
.expect("cannot send buffer"); | |
} | |
}); | |
Glasgow(receiver) | |
} | |
} | |
impl Read for Glasgow { | |
fn read(&mut self, dst_buf: &mut [u8]) -> io::Result<usize> { | |
let src_buf = self.0.recv().expect("cannot receive buffer"); | |
assert!(dst_buf.len() >= src_buf.len()); | |
dst_buf[..src_buf.len()].copy_from_slice(&src_buf[..]); | |
Ok(src_buf.len()) | |
} | |
} | |
const WIDTH: usize = 160; | |
const HEIGHT: usize = 144; | |
const PITCH: usize = 3 * WIDTH; | |
const FACTOR: usize = 4; | |
struct VideoStream<R: Read> { | |
reader: R, | |
sync_byte: Option<u8>, | |
} | |
struct Header { | |
overflow: bool, | |
n_frame: usize, | |
n_row: usize | |
} | |
struct Scanline { | |
header: Header, | |
data: [u8; PITCH] /* RGB */ | |
} | |
impl<R: Read> VideoStream<R> { | |
fn new(reader: R) -> VideoStream<R> { | |
VideoStream { reader, sync_byte: None } | |
} | |
fn read_byte(&mut self) -> u8 { | |
if let Some(byte) = self.sync_byte.take() { | |
return byte | |
} | |
let mut byte = 0u8; | |
self.reader.read(slice::from_mut(&mut byte)).expect("cannot read"); | |
byte | |
} | |
fn read_data_byte(&mut self) -> Result<u8, ()> { | |
let byte = self.read_byte(); | |
if byte & 0x80 == 0 { | |
Ok(byte) | |
} else { | |
self.sync_byte = Some(byte); | |
Err(()) | |
} | |
} | |
fn read_header(&mut self) -> Result<Header, ()> { | |
let mut sync = 0u8; | |
while sync & 0x80 == 0 { | |
sync = self.read_byte(); | |
} | |
let overflow = (sync & 0x70) >> 7; | |
let n_frame = (sync & 0x3e) >> 1; | |
let n_row = (sync & 0x01) << 7 | self.read_data_byte()?; | |
Ok(Header { | |
overflow: overflow != 0, | |
n_frame: n_frame as usize, | |
n_row: n_row as usize | |
}) | |
} | |
fn read_scanline(&mut self) -> Result<Scanline, ()> { | |
let header = self.read_header()?; | |
let mut data = [0; PITCH]; | |
for pixel in data.chunks_mut(3) { | |
// let x = self.read_data_byte()?; | |
// if x != 0x00 { panic!("fuck {:02x}", x) } | |
pixel[0] = self.read_data_byte()? << 3; | |
pixel[1] = self.read_data_byte()? << 3; | |
pixel[2] = self.read_data_byte()? << 3; | |
} | |
Ok(Scanline { header, data }) | |
} | |
} | |
use sdl2::event::Event; | |
use sdl2::pixels::{Color, PixelFormatEnum}; | |
fn write_gif(n_frame: usize, framebuffer: &[u8]) { | |
let frame = gif::Frame::from_rgb(WIDTH as u16, HEIGHT as u16, | |
&framebuffer[..]); | |
let mut image = File::create(format!("frames/{:06}.gif", n_frame)) | |
.expect("cannot open file"); | |
let mut encoder = gif::Encoder::new(&mut image, frame.width, frame.height, &[]) | |
.expect("cannot create encoder"); | |
encoder.write_frame(&frame) | |
.expect("cannot write frame"); | |
} | |
fn main() { | |
let context = libusb::Context::new().unwrap(); | |
let device = Glasgow::new(context); | |
let mut reader = VideoStream::new(BufReader::with_capacity(512, device)); | |
let sdl_context = sdl2::init().expect("cannot initialize SDL"); | |
let video_subsystem = sdl_context | |
.video() | |
.expect("cannot initialize SDL video"); | |
let window = video_subsystem | |
.window("Game Boy Color", (WIDTH * FACTOR) as u32, (HEIGHT * FACTOR) as u32) | |
.build() | |
.expect("cannot create SDL window"); | |
let mut canvas = window | |
.into_canvas() | |
.build() | |
.expect("cannot create SDL canvas"); | |
let texture_creator = canvas | |
.texture_creator(); | |
let mut texture = texture_creator | |
.create_texture_streaming(PixelFormatEnum::RGB24, WIDTH as u32, HEIGHT as u32) | |
.expect("cannot create RGB555 SDL texture"); | |
let mut event_pump = sdl_context | |
.event_pump() | |
.expect("cannot create SDL event pump"); | |
canvas.set_draw_color(Color::RGB(0, 0, 0)); | |
canvas.clear(); | |
canvas.present(); | |
let mut absolute_frame = 0; | |
let mut current_n_frame = 0; | |
let mut current_n_row = 0; | |
let mut framebuffer = [0u8; PITCH * HEIGHT]; | |
let mut skip_frame = false; | |
'run: loop { | |
match reader.read_scanline() { | |
Ok(Scanline { header: Header { overflow, n_frame, n_row }, data }) => { | |
if overflow { | |
print!("hardware reported FIFO overflow\n"); | |
} | |
// LCDC outputs a 145th row and it's always white. | |
// No idea what's up... | |
if n_row == 144 { continue 'run } | |
if n_row != (current_n_row + 1) % HEIGHT { | |
print!("expected row {} got {}\n", (current_n_row + 1) % HEIGHT, n_row); | |
skip_frame = true; | |
} | |
current_n_row = n_row; | |
if n_frame != current_n_frame { | |
if skip_frame { | |
skip_frame = false; | |
} else { | |
thread::spawn(move || write_gif(absolute_frame, &framebuffer[..])); | |
texture.update(None, &framebuffer, PITCH) | |
.expect("cannot update texture"); | |
canvas.copy(&texture, None, None) | |
.expect("cannot draw texture"); | |
canvas.present(); | |
} | |
absolute_frame += 1; | |
} | |
current_n_frame = n_frame; | |
framebuffer[n_row * PITCH..(n_row + 1) * PITCH].copy_from_slice(&data[..]); | |
} | |
Err(()) => { | |
print!("stream synchronization lost\n"); | |
current_n_row = HEIGHT - 1; | |
} | |
} | |
for event in event_pump.poll_iter() { | |
match event { | |
Event::Quit {..} => break 'run, | |
_ => () | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment