changeset 772:7492d384d122 default tip

Rust: Add a Glide renderer (2D only for now) This is an experiment for a Rust renderer, iterating over the Python data using pyo3. It requires --feature=glide to be passed to cargo build, doesn’t support NPOT textures, text rendering, the background, or even msg faces, some of that may come in a future changeset.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Mon, 05 Sep 2022 17:53:36 +0200
parents 79c3f782dd41
children
files python/Cargo.toml python/src/glide/gr.rs python/src/glide/mod.rs python/src/lib.rs pytouhou/options.py pytouhou/ui/glide/__init__.py pytouhou/ui/glide/backend.py pytouhou/ui/glide/sprite.pxd pytouhou/ui/glide/sprite.pyx pytouhou/ui/glide/window.pxd pytouhou/ui/glide/window.pyx
diffstat 11 files changed, 716 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- a/python/Cargo.toml
+++ b/python/Cargo.toml
@@ -15,3 +15,8 @@ name = "touhou"
 [dependencies]
 touhou-formats = "*"
 pyo3 = "0.17"
+image = { version = "0.24", default-features = false, features = ["png"], optional = true }
+
+[features]
+default = []
+glide = ["image"]
new file mode 100644
--- /dev/null
+++ b/python/src/glide/gr.rs
@@ -0,0 +1,312 @@
+use core::ptr::null;
+
+#[link(name = "glide2x")]
+extern "C" {
+    fn grGlideInit();
+    fn grGlideShutdown();
+    fn grSstSelect(sst: u32);
+    fn grSstWinOpen(hwnd: u32, resolution: u32, refresh: u32, color_format: u32, origin_location: u32, num_buf: i32, num_aux_buf: i32);
+    fn grBufferSwap(interval: i32);
+    fn grBufferClear(color: u32, alpha: u8, depth: u16);
+    fn grTexCalcMemRequired(min: Lod, max: Lod, aspect: AspectRatio, format: TextureFormat) -> u32;
+    fn grTexDownloadMipMap(tmu: u32, start: u32, even_odd: EvenOdd, info: *const TexInfo);
+    fn grTexSource(tmu: u32, start: u32, even_odd: EvenOdd, info: &TexInfo);
+    fn grTexMinAddress(tmu: u32) -> u32;
+    fn grTexMaxAddress(tmu: u32) -> u32;
+    fn guAlphaSource(mode: AlphaSource) -> u32;
+    fn guColorCombineFunction(function: ColorCombineFnc) -> u32;
+    fn grTexCombineFunction(tmu: u32, function: TextureCombineFnc) -> u32;
+    fn grDrawTriangle(a: *const Vertex, b: *const Vertex, c: *const Vertex);
+    fn grAlphaBlendFunction(a: Blend, b: Blend, c: Blend, d: Blend);
+}
+
+#[repr(i32)]
+#[derive(Clone, Copy)]
+pub enum Lod {
+    L256x256 = 0,
+    L128x128 = 1,
+    L64x64 = 2,
+    L32x32 = 3,
+    L16x16 = 4,
+    L8x8 = 5,
+    L4x4 = 6,
+    L2x2 = 7,
+    L1x1 = 8,
+}
+
+#[repr(i32)]
+#[derive(Clone, Copy)]
+pub enum AspectRatio {
+    A8x1 = 0,
+    A4x1 = 1,
+    A2x1 = 2,
+    A1x1 = 3,
+    A1x2 = 4,
+    A1x4 = 5,
+    A1x8 = 6,
+}
+
+fn lod_aspect_from_dimensions(dimensions: (u32, u32)) -> (Lod, AspectRatio) {
+    match dimensions {
+        (256, 256) => (Lod::L256x256, AspectRatio::A1x1),
+        (128, 128) => (Lod::L128x128, AspectRatio::A1x1),
+        (64, 64) => (Lod::L64x64, AspectRatio::A1x1),
+        (32, 32) => (Lod::L32x32, AspectRatio::A1x1),
+        (16, 16) => (Lod::L16x16, AspectRatio::A1x1),
+        (8, 8) => (Lod::L8x8, AspectRatio::A1x1),
+        (4, 4) => (Lod::L4x4, AspectRatio::A1x1),
+        (2, 2) => (Lod::L2x2, AspectRatio::A1x1),
+        (1, 1) => (Lod::L1x1, AspectRatio::A1x1),
+        (width, height) => todo!("NPOT texture size {width}×{height}"),
+    }
+}
+
+#[repr(i32)]
+#[derive(Clone, Copy, Debug)]
+pub enum TextureFormat {
+    Rgb332 = 0,
+    Yiq422 = 1,
+    Alpha8 = 2,
+    Intensity8 = 3,
+    AlphaIntensity44 = 4,
+    P8 = 5,
+    Argb8332 = 8,
+    Ayiq8422 = 9,
+    Rgb565 = 10,
+    Argb1555 = 11,
+    Argb4444 = 12,
+    AlphaIntensity88 = 13,
+    Ap88 = 14,
+}
+
+#[repr(C)]
+pub struct TexInfo {
+    pub small_lod: Lod,
+    pub large_lod: Lod,
+    pub aspect: AspectRatio,
+    pub format: TextureFormat,
+    data: *const u8,
+}
+
+impl TexInfo {
+    pub fn new(width: u32, height: u32, format: TextureFormat) -> TexInfo {
+        let (lod, aspect) = lod_aspect_from_dimensions((width, height));
+        TexInfo {
+            small_lod: lod,
+            large_lod: lod,
+            aspect,
+            format,
+            data: null(),
+        }
+    }
+
+    pub fn with_data(width: u32, height: u32, format: TextureFormat, data: &[u8]) -> TexInfo {
+        let (lod, aspect) = lod_aspect_from_dimensions((width, height));
+        TexInfo {
+            small_lod: lod,
+            large_lod: lod,
+            aspect,
+            format,
+            data: data.as_ptr(),
+        }
+    }
+}
+
+#[repr(C)]
+pub struct Vertex {
+    x: f32,
+    y: f32,
+    z: f32,
+    r: f32,
+    g: f32,
+    b: f32,
+    ooz: f32,
+    a: f32,
+    oow: f32,
+    sow0: f32,
+    tow0: f32,
+    oow0: f32,
+    sow1: f32,
+    tow1: f32,
+    oow1: f32,
+}
+
+impl Vertex {
+    pub fn new(x: f32, y: f32, sow: f32, tow: f32, color: u32) -> Vertex {
+        let z = 1.0;
+        let r = ((color >> 24) & 0xff) as f32;
+        let g = ((color >> 16) & 0xff) as f32;
+        let b = ((color >> 8) & 0xff) as f32;
+        let a = (color & 0xff) as f32;
+        let ooz = 1.0;
+        let oow = 1.0;
+        let sow0 = sow;
+        let tow0 = tow;
+        let oow0 = 1.0;
+        let sow1 = sow;
+        let tow1 = tow;
+        let oow1 = 1.0;
+        Vertex {
+            x, y, z,
+            r, g, b,
+            ooz,
+            a,
+            oow,
+            sow0, tow0, oow0,
+            sow1, tow1, oow1,
+        }
+    }
+}
+
+pub fn glide_init() {
+    unsafe { grGlideInit() };
+}
+
+pub fn glide_shutdown() {
+    unsafe { grGlideShutdown() };
+}
+
+pub fn sst_select(sst: u32) {
+    unsafe { grSstSelect(sst) };
+}
+
+pub fn sst_win_open(width: u32, height: u32, refresh: u32) {
+    let resolution = match (width, height) {
+        (320, 200) => 0,
+        (320, 240) => 1,
+        (400, 256) => 2,
+        (512, 384) => 3,
+        (640, 200) => 4,
+        (640, 350) => 5,
+        (640, 400) => 6,
+        (640, 480) => 7,
+        _ => unreachable!("Unknown screen resolution {width}×{height}."),
+    };
+    let refresh = match refresh {
+        60 => 0,
+        70 => 1,
+        72 => 2,
+        75 => 3,
+        80 => 4,
+        90 => 5,
+        100 => 6,
+        85 => 7,
+        120 => 8,
+        _ => unreachable!("Unknown refresh rate {refresh} Hz."),
+    };
+    let color_format = 2; // RGBA
+    let origin_location = 0; // Upper Left
+    unsafe { grSstWinOpen(0, resolution, refresh, color_format, origin_location, 2, 0) };
+}
+
+pub fn buffer_swap(interval: i32) {
+    unsafe { grBufferSwap(interval) };
+}
+
+pub fn buffer_clear(color: u32, alpha: u8, depth: u16) {
+    unsafe { grBufferClear(color, alpha, depth) };
+}
+
+pub fn tex_calc_mem_required(small_lod: Lod, large_lod: Lod, aspect: AspectRatio, format: TextureFormat) -> u32 {
+    unsafe { grTexCalcMemRequired(small_lod, large_lod, aspect, format) }
+}
+
+pub fn tex_download_mip_map(tmu: u32, start: u32, even_odd: EvenOdd, info: &TexInfo) {
+    unsafe { grTexDownloadMipMap(tmu, start, even_odd, info) };
+}
+
+pub fn tex_source(tmu: u32, start: u32, even_odd: EvenOdd, info: &TexInfo) {
+    unsafe { grTexSource(tmu, start, even_odd, info) };
+}
+
+pub fn tex_min_address(tmu: u32) -> u32 {
+    unsafe { grTexMinAddress(tmu) }
+}
+
+pub fn tex_max_address(tmu: u32) -> u32 {
+    unsafe { grTexMaxAddress(tmu) }
+}
+
+pub fn alpha_source(mode: AlphaSource) {
+    unsafe { guAlphaSource(mode) };
+}
+
+pub fn color_combine_function(function: ColorCombineFnc) {
+    unsafe { guColorCombineFunction(function) };
+}
+
+pub fn tex_combine_function(tmu: u32, function: TextureCombineFnc) {
+    unsafe { grTexCombineFunction(tmu, function) };
+}
+
+pub fn alpha_blend_function(a: Blend, b: Blend, c: Blend, d: Blend) {
+    unsafe { grAlphaBlendFunction(a, b, c, d) };
+}
+
+#[repr(i32)]
+pub enum EvenOdd {
+    Even = 0,
+    Odd = 1,
+    Both = 2,
+}
+
+#[repr(i32)]
+pub enum Blend {
+    Zero = 0,
+    SrcAlpha = 1,
+    SrcColor = 2,
+    DstAlpha = 3,
+    One = 4,
+    OneMinusSrcAlpha = 5,
+    OneMinusSrcColor = 6,
+    OneMinusDstAlpha = 7,
+    AlphaSaturate = 15,
+}
+
+#[repr(i32)]
+pub enum AlphaSource {
+    CcAlpha = 0,
+    IteratedAlpha = 1,
+    TextureAlpha = 2,
+    TextureAlphaTimesIteratedAlpha = 3,
+}
+
+#[repr(i32)]
+pub enum ColorCombineFnc {
+    Zero = 0,
+    Ccrgb = 1,
+    Itrgb = 2,
+    ItrgbDelta0 = 3,
+    DecalTexture = 4,
+    TextureTimesCcrgb = 5,
+    TextureTimesItrgb = 6,
+    TextureTimesItrgbDelta0 = 7,
+    TextureTimesItrgbAddAlpha = 8,
+    TextureTimesAlpha = 9,
+    TextureTimesAlphaAddItrgb = 10,
+    TextureAddItrgb = 11,
+    TextureSubItrgb = 12,
+    CcrgbBlendItrgbOnTexalpha = 13,
+    DiffSpecA = 14,
+    DiffSpecB = 15,
+    One = 16,
+}
+
+#[repr(i32)]
+pub enum TextureCombineFnc {
+    Zero = 0,
+    Decal = 1,
+    Other = 2,
+    Add = 3,
+    Multiply = 4,
+    Subtract = 5,
+    Detail = 6,
+    DetailOther = 7,
+    TrilinearOdd = 8,
+    TrilinearEven = 9,
+    One = 10,
+}
+
+pub fn draw_triangle(a: &Vertex, b: &Vertex, c: &Vertex) {
+    unsafe { grDrawTriangle(a, b, c) };
+}
new file mode 100644
--- /dev/null
+++ b/python/src/glide/mod.rs
@@ -0,0 +1,258 @@
+use pyo3::prelude::*;
+use pyo3::types::{PyList, PyDict, PySequence};
+use pyo3::exceptions::PyTypeError;
+use std::collections::{HashMap, BTreeMap};
+use image::GenericImageView;
+
+mod gr;
+
+#[inline(always)]
+fn pixel_to_rgb332(pixel: [u8; 4]) -> [u8; 1] {
+    [(pixel[0] & 0xe0) | ((pixel[1] >> 3) & 0x1c) | (pixel[2] >> 6)]
+}
+
+#[inline(always)]
+fn pixel_to_argb8332(pixel: [u8; 4]) -> [u8; 2] {
+    [(pixel[0] & 0xe0) | ((pixel[1] >> 3) & 0x1c) | (pixel[2] >> 6), pixel[3]]
+}
+
+#[inline(always)]
+fn pixel_to_argb4444(pixel: [u8; 4]) -> [u8; 2] {
+    [(pixel[1] & 0xf0) | (pixel[2] >> 4), (pixel[3] & 0xf0) | (pixel[0] >> 4)]
+}
+
+#[inline(always)]
+fn pixel_to_argb1555(pixel: [u8; 4]) -> [u8; 2] {
+    [((pixel[1] << 2) & 0xe0) | (pixel[2] >> 3), (pixel[3] & 0x80) | ((pixel[0] >> 1) & 0x7c) | (pixel[1] >> 6)]
+}
+
+#[inline(always)]
+fn pixel_to_rgb565(pixel: [u8; 4]) -> [u8; 2] {
+    [((pixel[1] << 3) & 0xe0) | (pixel[2] >> 3), (pixel[0] & 0xf8) | (pixel[1] >> 5)]
+}
+
+fn merge_alpha(rgb: &image::DynamicImage, alpha: &image::DynamicImage) -> Vec<u8> {
+    let alpha = match alpha.grayscale() {
+        image::DynamicImage::ImageLuma8(img) => img,
+        foo => panic!("TODO {:?} {:?}", alpha, foo),
+    };
+    rgb
+        .pixels()
+        .zip(alpha.pixels())
+        .map(|((_x, _y, rgb), alpha)| pixel_to_argb4444([rgb[0], rgb[1], rgb[2], alpha[0]]))
+        .flatten()
+        .collect::<Vec<_>>()
+}
+
+#[derive(Debug)]
+struct TextureManager {
+    tmu: u32,
+    next_tex_location: u32,
+    max_tex_location: u32,
+    textures: BTreeMap<u32, gr::TextureFormat>,
+}
+
+impl TextureManager {
+    fn new(tmu: u32) -> TextureManager {
+        let next_tex_location = gr::tex_min_address(tmu);
+        let max_tex_location = gr::tex_max_address(tmu);
+        let textures = BTreeMap::new();
+        TextureManager {
+            tmu,
+            next_tex_location,
+            max_tex_location,
+            textures,
+        }
+    }
+
+    fn download(&mut self, tex: &gr::TexInfo) -> PyResult<u32> {
+        let location = self.next_tex_location;
+        let size = gr::tex_calc_mem_required(tex.small_lod, tex.large_lod, tex.aspect, tex.format);
+        if location + size > self.max_tex_location {
+            return Err(PyTypeError::new_err("Out of memory"));
+        }
+        gr::tex_download_mip_map(self.tmu, location, gr::EvenOdd::Both, tex);
+        self.next_tex_location += size;
+        self.textures.insert(location, tex.format);
+        Ok(location)
+    }
+
+    fn get(&self, address: u32) -> gr::TexInfo {
+        if let Some(&format) = self.textures.get(&address) {
+            gr::TexInfo::new(256, 256, format)
+        } else {
+            unreachable!("Not uploaded texture at address 0x{:08x}!", address);
+        }
+    }
+}
+
+#[pyclass]
+struct GameRenderer {
+    #[pyo3(get, set)]
+    size: (u32, u32, u32, u32),
+
+    texture_manager: TextureManager,
+}
+
+#[pymethods]
+impl GameRenderer {
+    #[new]
+    fn new() -> GameRenderer {
+        let size = (0, 0, 0, 0);
+        let texture_manager = TextureManager::new(0);
+        GameRenderer {
+            size,
+            texture_manager,
+        }
+    }
+
+    fn start(&self, common: PyObject) {
+        gr::color_combine_function(gr::ColorCombineFnc::TextureTimesItrgb);
+        gr::alpha_blend_function(gr::Blend::SrcAlpha, gr::Blend::OneMinusSrcAlpha, gr::Blend::One, gr::Blend::Zero);
+        gr::alpha_source(gr::AlphaSource::TextureAlphaTimesIteratedAlpha);
+        gr::tex_combine_function(0, gr::TextureCombineFnc::Decal);
+    }
+
+    fn load_textures(&mut self, py: Python, anms: HashMap<String, Vec<PyObject>>) -> PyResult<()> {
+        for (filename, anm) in anms {
+            for anm in anm {
+                let png_rgb: String = anm.getattr(py, "first_name")?.extract(py)?;
+                let png_alpha: Option<String> = anm.getattr(py, "secondary_name")?.extract(py)?;
+                let (_, png_rgb) = png_rgb.rsplit_once('/').unwrap();
+                use std::path::PathBuf;
+                let texture_address = if let Some(png_alpha) = png_alpha {
+                    let (_, png_alpha) = png_alpha.rsplit_once('/').unwrap();
+                    //image::load_from_memory_with_format(b"", image::ImageFormat::Png).unwrap();
+                    let rgb = image::open(["/", "tmp", "touhou", png_rgb].iter().collect::<PathBuf>()).unwrap();
+                    let alpha = image::open(["/", "tmp", "touhou", png_alpha].iter().collect::<PathBuf>()).unwrap();
+                    assert_eq!(rgb.dimensions(), alpha.dimensions());
+                    let (width, height) = rgb.dimensions();
+                    let rgba = merge_alpha(&rgb, &alpha);
+                    let tex = gr::TexInfo::with_data(width, height, gr::TextureFormat::Argb4444, &rgba);
+                    self.texture_manager.download(&tex)?
+                } else {
+                    //image::load_from_memory_with_format(b"", image::ImageFormat::Png).unwrap();
+                    let rgb = image::open(["/", "tmp", "touhou", png_rgb].iter().collect::<PathBuf>()).unwrap();
+                    let (width, height) = rgb.dimensions();
+                    let rgb = rgb.pixels()
+                        .map(|(x, y, rgb)| pixel_to_rgb565([rgb[0], rgb[1], rgb[2], 0xff]))
+                        .flatten()
+                        .collect::<Vec<_>>();
+                    let tex = gr::TexInfo::with_data(width, height, gr::TextureFormat::Rgb565, &rgb);
+                    self.texture_manager.download(&tex)?
+                };
+                anm.setattr(py, "texture", texture_address)?;
+                let texture: u32 = anm.getattr(py, "texture")?.extract(py)?;
+            }
+        }
+        Ok(())
+    }
+
+    fn load_background(&self, background: PyObject) {
+        println!("TODO: GameRenderer::load_background({background})");
+    }
+
+    fn render_elements(&self, py: Python, elements: &PyList, shift: (f32, f32)) -> PyResult<()> {
+        let module = py.import("pytouhou.ui.glide.sprite")?;
+        let get_sprite_rendering_data = module.getattr("get_sprite_rendering_data")?;
+        let mut prev_texture = u32::MAX;
+        for element in elements.iter() {
+            /*
+            // TODO: only for enemies.
+            let visible: bool = element.getattr("visible")?.extract()?;
+            if !visible {
+                continue;
+            }
+            */
+            let x: f32 = element.getattr("x")?.extract()?;
+            let y: f32 = element.getattr("y")?.extract()?;
+            let sprite = element.getattr("sprite")?;
+            if !sprite.is_none() {
+                let (pos, mut texcoords, color): ([f32; 12], [f32; 4], u32) = get_sprite_rendering_data.call1((sprite,))?.extract()?;
+                for coord in &mut texcoords {
+                    *coord *= 256.0;
+                }
+                let anm = sprite.getattr("anm")?;
+                let texture = anm.getattr("texture")?.extract()?;
+                if texture != prev_texture {
+                    let tex = self.texture_manager.get(texture);
+                    gr::tex_source(0, texture, gr::EvenOdd::Both, &tex);
+                    prev_texture = texture;
+                }
+                draw_triangle(x + shift.0, y + shift.1, pos, texcoords, color);
+            }
+        }
+        Ok(())
+    }
+
+    fn render(&self, py: Python, game: PyObject) -> PyResult<()> {
+        gr::buffer_clear(0x000000ff, 0xff, 0xffff);
+        for things in ["enemies", "effects", "players_bullets"/*, "lasers_sprites()"*/, "players"/*, "msg_sprites()"*/, "bullets", "lasers", "cancelled_bullets", "items", "labels"] {
+            let things = game.getattr(py, things)?;
+            let things: &PyList = things.extract(py)?;
+            self.render_elements(py, things, (32.0, 16.0))?;
+        }
+        let interface = game.getattr(py, "interface")?;
+        let boss = game.getattr(py, "boss")?;
+        self.render_interface(py, interface, !boss.is_none(py))?;
+        Ok(())
+    }
+
+    fn render_interface(&self, py: Python, interface: PyObject, boss: bool) -> PyResult<()> {
+        let items = interface.getattr(py, "items")?;
+        let items: &PyList = items.extract(py)?;
+        self.render_elements(py, items, (0.0, 0.0))?;
+        /*
+        // TODO: figure out why this doesn’t render alphanumeric characters.
+        let labels = interface.getattr(py, "labels")?;
+        let labels: &PyDict = labels.extract(py)?;
+        self.render_elements(py, labels.values(), (0.0, 0.0))?;
+        */
+        if boss {
+            let items = interface.getattr(py, "boss_items")?;
+            let items: &PyList = items.extract(py)?;
+            self.render_elements(py, items, (0.0, 0.0))?;
+        }
+        Ok(())
+    }
+}
+
+fn draw_triangle(ox: f32, oy: f32, pos: [f32; 12], texcoords: [f32; 4], color: u32) {
+    let a = gr::Vertex::new(ox + pos[0], oy + pos[4], texcoords[0], texcoords[2], color);
+    let b = gr::Vertex::new(ox + pos[1], oy + pos[5], texcoords[1], texcoords[2], color);
+    let c = gr::Vertex::new(ox + pos[2], oy + pos[6], texcoords[1], texcoords[3], color);
+    let d = gr::Vertex::new(ox + pos[3], oy + pos[7], texcoords[0], texcoords[3], color);
+    gr::draw_triangle(&a, &b, &c);
+    gr::draw_triangle(&a, &c, &d);
+}
+
+#[pyfunction]
+fn init(options: HashMap<String, String>) {
+    gr::glide_init();
+    gr::sst_select(0);
+}
+
+#[pyfunction]
+fn shutdown() {
+    gr::glide_shutdown();
+}
+
+#[pyfunction]
+fn create_window(title: &str, posx: u32, posy: u32, width: u32, height: u32, frameskip: u32) {
+    gr::sst_win_open(640, 480, 60);
+}
+
+#[pyfunction]
+fn buffer_swap() {
+    gr::buffer_swap(1);
+}
+
+pub fn module(py: Python) -> PyResult<&PyModule> {
+    let m = PyModule::new(py, "glide")?;
+    m.add_class::<GameRenderer>()?;
+    m.add_function(wrap_pyfunction!(init, m)?)?;
+    m.add_function(wrap_pyfunction!(shutdown, m)?)?;
+    m.add_function(wrap_pyfunction!(create_window, m)?)?;
+    m.add_function(wrap_pyfunction!(buffer_swap, m)?)?;
+    Ok(&m)
+}
--- a/python/src/lib.rs
+++ b/python/src/lib.rs
@@ -4,6 +4,9 @@ use touhou_formats::th06::pbg3;
 use std::fs::File;
 use std::io::BufReader;
 
+#[cfg(feature = "glide")]
+mod glide;
+
 #[pyclass]
 struct PBG3 {
     inner: pbg3::PBG3<BufReader<File>>,
@@ -35,7 +38,9 @@ impl PBG3 {
 }
 
 #[pymodule]
-fn libtouhou(_py: Python, m: &PyModule) -> PyResult<()> {
+fn libtouhou(py: Python, m: &PyModule) -> PyResult<()> {
     m.add_class::<PBG3>()?;
+    #[cfg(feature = "glide")]
+    m.add_submodule(glide::module(py)?)?;
     Ok(())
 }
--- a/pytouhou/options.py
+++ b/pytouhou/options.py
@@ -141,7 +141,7 @@ def parse_arguments(defaults):
 
     graphics_group = parser.add_argument_group('Graphics options')
     graphics_group.add_argument('--frontend', metavar='FRONTEND', choices=['glfw', 'sdl'], help='Which windowing library to use (glfw or sdl).')
-    graphics_group.add_argument('--backend', metavar='BACKEND', choices=['opengl', 'sdl'], nargs='*', help='Which backend to use (opengl or sdl).')
+    graphics_group.add_argument('--backend', metavar='BACKEND', choices=['opengl', 'glide', 'sdl'], nargs='*', help='Which backend to use (opengl, glide or sdl).')
     graphics_group.add_argument('--fps-limit', metavar='FPS', type=int, help='Set fps limit. A value of 0 disables fps limiting, while a negative value limits to 60 fps if and only if vsync doesn’t work.')
     graphics_group.add_argument('--frameskip', metavar='FRAMESKIP', type=int, help='Set the frameskip, as 1/FRAMESKIP, or disabled if 0.')
     graphics_group.add_argument('--no-background', action='store_false', help='Disable background display (huge performance boost on slow systems).')
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/glide/backend.py
@@ -0,0 +1,9 @@
+from libtouhou import glide
+from .window import Window
+
+def create_window(title, posx, posy, width, height, frameskip):
+    glide.create_window(title, posx, posy, width, height, frameskip)
+    return Window()
+
+init = glide.init
+GameRenderer = glide.GameRenderer
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/glide/sprite.pxd
@@ -0,0 +1,8 @@
+from pytouhou.game.sprite cimport Sprite
+
+cdef struct RenderingData:
+    float pos[12]
+    float left, right, bottom, top
+    unsigned char color[4]
+
+cdef void render_sprite(Sprite sprite) nogil
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/glide/sprite.pyx
@@ -0,0 +1,86 @@
+# -*- encoding: utf-8 -*-
+##
+## Copyright (C) 2011 Thibaut Girka <thib@sitedethib.com>
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 3 only.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+
+
+from libc.stdlib cimport malloc
+from libc.string cimport memcpy
+from libc.math cimport M_PI as pi
+
+from pytouhou.utils.matrix cimport Matrix, scale2d, flip, rotate_x, rotate_y, rotate_z, translate, translate2d
+
+
+cdef Matrix default
+default = Matrix(-.5,   .5,   .5,  -.5,
+                 -.5,  -.5,   .5,   .5,
+                 0,    0,    0,    0,
+                 1,    1,    1,    1)
+
+
+def get_sprite_rendering_data(Sprite sprite):
+    if sprite.changed:
+        render_sprite(sprite)
+    data = <RenderingData*>sprite._rendering_data
+    color = <unsigned int>data.color[0] << 24 | <unsigned int>data.color[1] << 16 | <unsigned int>data.color[2] << 8 | <unsigned int>data.color[3]
+    return (data.pos, [data.left, data.right, data.bottom, data.top], color)
+
+
+cdef void render_sprite(Sprite sprite) nogil:
+    cdef Matrix vertmat
+
+    if sprite._rendering_data == NULL:
+        sprite._rendering_data = malloc(sizeof(RenderingData))
+
+    data = <RenderingData*>sprite._rendering_data
+    memcpy(&vertmat, &default, sizeof(Matrix))
+
+    tx, ty, tw, th = sprite._texcoords[0], sprite._texcoords[1], sprite._texcoords[2], sprite._texcoords[3]
+    sx, sy = sprite._rescale[0], sprite._rescale[1]
+    width = sprite.width_override or (tw * sx)
+    height = sprite.height_override or (th * sy)
+
+    scale2d(&vertmat, width, height)
+    if sprite.mirrored:
+        flip(&vertmat)
+
+    rx, ry, rz = sprite._rotations_3d[0], sprite._rotations_3d[1], sprite._rotations_3d[2]
+    if sprite.automatic_orientation:
+        rz += pi/2. - sprite.angle
+    elif sprite.force_rotation:
+        rz += sprite.angle
+
+    if rx:
+        rotate_x(&vertmat, -rx)
+    if ry:
+        rotate_y(&vertmat, ry)
+    if rz:
+        rotate_z(&vertmat, -rz) #TODO: minus, really?
+    if sprite.allow_dest_offset:
+        translate(&vertmat, sprite._dest_offset)
+    if sprite.corner_relative_placement: # Reposition
+        translate2d(&vertmat, width / 2, height / 2)
+
+    memcpy(data.pos, &vertmat, 12 * sizeof(float))
+
+    x_1 = sprite.anm.size_inv[0]
+    y_1 = sprite.anm.size_inv[1]
+    tox, toy = sprite._texoffsets[0], sprite._texoffsets[1]
+    data.left = tx * x_1 + tox
+    data.right = (tx + tw) * x_1 + tox
+    data.bottom = ty * y_1 + toy
+    data.top = (ty + th) * y_1 + toy
+
+    for i in range(4):
+        data.color[i] = sprite._color[i]
+
+    sprite.changed = False
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/glide/window.pxd
@@ -0,0 +1,9 @@
+cimport pytouhou.lib.gui as gui
+
+cdef class Window(gui.Window):
+    cdef void present(self) nogil
+    cdef void set_window_size(self, int width, int height) nogil
+    cdef void set_swap_interval(self, int interval) except *
+    cdef list get_events(self)
+    cdef int get_keystate(self) nogil
+    cdef void toggle_fullscreen(self) nogil
new file mode 100644
--- /dev/null
+++ b/pytouhou/ui/glide/window.pyx
@@ -0,0 +1,22 @@
+import pytouhou.lib.gui as gui
+import libtouhou
+
+cdef class Window(gui.Window):
+    cdef void present(self) nogil:
+        with gil:
+            libtouhou.glide.buffer_swap()
+
+    cdef void set_window_size(self, int width, int height) nogil:
+        pass
+
+    cdef void set_swap_interval(self, int interval) except *:
+        pass
+
+    cdef list get_events(self):
+        return []
+
+    cdef int get_keystate(self) nogil:
+        return 0
+
+    cdef void toggle_fullscreen(self) nogil:
+        pass