diff python/src/glide/mod.rs @ 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
children
line wrap: on
line diff
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)
+}