Skip to content

Instantly share code, notes, and snippets.

@whitequark
Created May 28, 2018 16:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save whitequark/20fb9b41600a6abfb4f93007c546fa21 to your computer and use it in GitHub Desktop.
Save whitequark/20fb9b41600a6abfb4f93007c546fa21 to your computer and use it in GitHub Desktop.
[package]
name = "cgb-grabber"
version = "0.1.0"
authors = ["whitequark <whitequark@whitequark.org>"]
[dependencies]
libusb = "0.3"
sdl2 = "0.31"
gif = "0.10"
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