diff --git a/Cargo.lock b/Cargo.lock index fc0899a..2c63133 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,24 +8,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "bytemuck" -version = "1.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" - [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - [[package]] name = "enum_dispatch" version = "0.3.13" @@ -44,7 +32,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "032213946b4eaae09117ec63f020322b78ca7a31d8aa2cf64df3032e1579690f" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", "fasthash-sys", "num-traits", "seahash", @@ -77,9 +65,6 @@ name = "glam" version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0e9b6647e9b41d3a5ef02964c6be01311a7f2472fea40595c635c6d046c259e" -dependencies = [ - "bytemuck", -] [[package]] name = "heck" @@ -99,23 +84,33 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "mechthild_core" version = "0.1.0" dependencies = [ - "bytemuck", "enum_dispatch", "fasthash", "glam", - "range2d", + "ndarray", + "num-traits", ] [[package]] name = "mechthild_py" version = "0.1.0" dependencies = [ - "bytemuck", "mechthild_core", + "numpy", "pyo3", ] @@ -128,6 +123,39 @@ dependencies = [ "autocfg", ] +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -137,6 +165,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "numpy" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29f1dee9aa8d3f6f8e8b9af3803006101bb3653866ef056d530d53ae68587191" +dependencies = [ + "libc", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pyo3", + "pyo3-build-config", + "rustc-hash", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -149,6 +193,15 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -160,11 +213,10 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.24.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" +checksum = "f239d656363bcee73afef85277f1b281e8ac6212a1d42aa90e55b90ed43c47a4" dependencies = [ - "cfg-if 1.0.0", "indoc", "libc", "memoffset", @@ -178,9 +230,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.24.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" +checksum = "755ea671a1c34044fa165247aaf6f419ca39caa6003aee791a0df2713d8f1b6d" dependencies = [ "once_cell", "target-lexicon", @@ -188,9 +240,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.24.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" +checksum = "fc95a2e67091e44791d4ea300ff744be5293f394f1bafd9f78c080814d35956e" dependencies = [ "libc", "pyo3-build-config", @@ -198,9 +250,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.24.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" +checksum = "a179641d1b93920829a62f15e87c0ed791b6c8db2271ba0fd7c2686090510214" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -210,9 +262,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.24.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" +checksum = "9dff85ebcaab8c441b0e3f7ae40a6963ecea8a9f5e74f647e33fcf5ec9a1e89e" dependencies = [ "heck", "proc-macro2", @@ -259,10 +311,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" [[package]] -name = "range2d" -version = "0.2.0" +name = "rawpointer" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7169c4e8549f72214a10fe6a64e24bf5e2d75ca9378d0ac7934afb9acbdfc313" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rdrand" @@ -273,6 +325,12 @@ dependencies = [ "rand_core 0.3.1", ] +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "seahash" version = "3.0.7" diff --git a/core/Cargo.toml b/core/Cargo.toml index b7d2e00..a14379c 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -7,12 +7,11 @@ license.workspace = true repository.workspace = true [dependencies] -bytemuck = { version = "1.22.0", optional = true } enum_dispatch = "0.3.13" fasthash = "0.4.0" glam = "0.30.2" -range2d = "0.2.0" +ndarray = "0.16.1" +num-traits = "0.2.19" [features] fast-math = ["glam/fast-math"] -bytemuck = ["dep:bytemuck", "glam/bytemuck"] diff --git a/core/src/camera.rs b/core/src/camera.rs index 548e610..e11fc8a 100644 --- a/core/src/camera.rs +++ b/core/src/camera.rs @@ -1,8 +1,4 @@ -use crate::{ - color::Rgba, - geometry::{Point, Point2, Ray, Transform, Transform2, Vector, point2, ray, vec3}, - sampling::PixelSample, -}; +use crate::geometry::{Point, Point2, Ray, Transform, Vector, ray, vec3}; #[derive(Debug, Clone)] pub struct Camera { @@ -27,45 +23,3 @@ impl Camera { ) } } - -pub struct Film { - width: u32, - height: u32, - transform: Transform2, - data: Box<[Rgba]>, -} - -impl Film { - pub fn new(width: u32, height: u32) -> Self { - let size = width as usize * height as usize; - - let data = vec![Rgba::BLACK; size].into_boxed_slice(); - - let w = width as f32; - let h = height as f32; - let ar = h / w; - - let transform = Transform2::translate(-1.0, ar) * Transform2::scale(2.0 / w, -2.0 / w); - - Self { - width, - height, - transform, - data, - } - } - - pub fn add_sample(&mut self, x: u32, y: u32, color: Rgba, weight: f32) { - let i = y as usize * self.width as usize + x as usize; - self.data[i] += color * weight; - } - - pub fn get_camera_sample(&self, x: u32, y: u32, sampler: &mut PixelSample) -> Point2 { - let p = point2(x as f32, y as f32) + sampler.get_2d().into(); - self.transform * p - } - - pub fn into_data(self) -> Box<[Rgba]> { - self.data - } -} diff --git a/core/src/color.rs b/core/src/color.rs index 44a62ef..7ba756d 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -1,75 +1,99 @@ -use std::{ - fmt, - ops::{Add, AddAssign, Mul}, -}; +use std::fmt; +use std::ops::{Add, AddAssign, Div, DivAssign, Index, Mul, MulAssign}; + +use glam::Vec3A; + +use crate::Vector; #[derive(Clone, Copy)] -#[repr(C)] -pub struct Rgba { - r: f32, - g: f32, - b: f32, - a: f32, +pub struct Rgb(Vec3A); + +impl Rgb { + pub const BLACK: Self = rgb(0.0, 0.0, 0.0); } -pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Rgba { - Rgba { r, g, b, a } +impl num_traits::Zero for Rgb { + fn zero() -> Self { + Self(Vec3A::ZERO) + } + + fn is_zero(&self) -> bool { + self.0 == Vec3A::ZERO + } } -impl Rgba { - pub const BLACK: Self = rgba(0.0, 0.0, 0.0, 1.0); +pub const fn rgb(r: f32, g: f32, b: f32) -> Rgb { + Rgb(glam::vec3a(r, g, b)) } -impl fmt::Debug for Rgba { +impl From for Rgb { + fn from(v: Vector) -> Self { + Self(v.into()) + } +} + +impl fmt::Debug for Rgb { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple(stringify!(Rgba)) - .field(&self.r) - .field(&self.g) - .field(&self.b) - .field(&self.a) + f.debug_tuple(stringify!(Rgb)) + .field(&self.0.x) + .field(&self.0.y) + .field(&self.0.z) .finish() } } -impl Add for Rgba { - type Output = Self; +impl Index for Rgb { + type Output = f32; - fn add(self, Self { r, g, b, a }: Self) -> Self::Output { - Self { - r: self.r + r, - g: self.g + g, - b: self.b + b, - a: self.a + a, + #[inline] + fn index(&self, index: usize) -> &Self::Output { + match index { + 0 => &self.0.x, + 1 => &self.0.y, + 2 => &self.0.z, + _ => panic!("index out of bounds"), } } } -impl AddAssign for Rgba { - fn add_assign(&mut self, Self { r, g, b, a }: Self) { - self.r += r; - self.g += g; - self.b += b; - self.a += a; +impl Add for Rgb { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) } } -impl Mul for Rgba { +impl AddAssign for Rgb { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0 + } +} + +impl Mul for Rgb { type Output = Self; fn mul(self, rhs: f32) -> Self::Output { - Self { - r: self.r * rhs, - g: self.g * rhs, - b: self.b * rhs, - a: self.a * rhs, - } + Self(self.0 * rhs) } } -impl Mul for f32 { - type Output = Rgba; - - fn mul(self, rhs: Rgba) -> Self::Output { - rhs * self +impl MulAssign for Rgb { + fn mul_assign(&mut self, rhs: f32) { + self.0 *= rhs + } +} + +impl Div for Rgb { + type Output = Self; + + fn div(self, rhs: f32) -> Self::Output { + Self(self.0 / rhs) + } +} + +impl DivAssign for Rgb { + fn div_assign(&mut self, rhs: f32) { + self.0 /= rhs } } diff --git a/core/src/film.rs b/core/src/film.rs new file mode 100644 index 0000000..4507d0c --- /dev/null +++ b/core/src/film.rs @@ -0,0 +1,37 @@ +use ndarray::{Array3, Ix2, iter::LanesMut}; + +use crate::{geometry::Transform2, sampling::Sampler}; + +pub struct Film { + pub width: u32, + pub height: u32, + pub transform: Transform2, + pub data: Array3, +} + +impl Film { + pub fn new(width: u32, height: u32) -> Self { + let w = width as f32; + let h = height as f32; + let ar = h / w; + + let transform = Transform2::translate(-1.0, ar) * Transform2::scale(2.0 / w, -2.0 / w); + + let data = Array3::zeros((height as usize, width as usize, 3)); + + Self { + width, + height, + transform, + data, + } + } + + pub fn sampler(&self, samples: u32, seed: u32) -> Sampler { + Sampler::new(self.width, self.height, samples, seed) + } + + pub fn lanes_mut(&mut self) -> LanesMut<'_, f32, Ix2> { + self.data.lanes_mut(ndarray::Axis(2)) + } +} diff --git a/core/src/geometry.rs b/core/src/geometry.rs index 0c719fc..697d25f 100644 --- a/core/src/geometry.rs +++ b/core/src/geometry.rs @@ -21,6 +21,10 @@ impl Vector { pub const J: Self = Self(glam::Vec3A::Y); pub const K: Self = Self(glam::Vec3A::Z); + pub fn splat(v: f32) -> Self { + Self(glam::Vec3A::splat(v)) + } + pub fn length(self) -> f32 { self.0.length() } @@ -80,6 +84,12 @@ impl Deref for Vector { } } +impl From for glam::Vec3A { + fn from(value: Vector) -> Self { + value.0 + } +} + impl From for glam::Vec3 { fn from(value: Vector) -> Self { value.0.into() diff --git a/core/src/impl_bytemuck.rs b/core/src/impl_bytemuck.rs deleted file mode 100644 index 87dfaa0..0000000 --- a/core/src/impl_bytemuck.rs +++ /dev/null @@ -1,6 +0,0 @@ -use bytemuck::{Pod, Zeroable}; - -use crate::color::Rgba; - -unsafe impl Pod for Rgba {} -unsafe impl Zeroable for Rgba {} diff --git a/core/src/lib.rs b/core/src/lib.rs index 2aef395..b3663d8 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,66 +1,129 @@ -use range2d::Range2D; - -#[cfg(feature = "bytemuck")] -pub mod impl_bytemuck; - -pub mod color; +use std::{ + sync::mpsc::{self, Receiver, Sender, channel}, + thread::{self, JoinHandle}, +}; mod camera; +mod color; +mod film; mod geometry; mod sampling; +mod scene; pub use camera::Camera; +pub use film::Film; pub use geometry::{Point, Shape, Transform, Vector}; +pub use scene::Scene; -use camera::Film; -use color::{Rgba, rgba}; -use geometry::{Hit, Hittable, Ray, vec3}; -use sampling::Sampler; +use color::Rgb; +use geometry::{Point2, Transform2, point2}; +use sampling::PixelSample; -pub struct Scene { +pub struct RenderSession { + thread: RenderThread, +} + +impl RenderSession { + pub fn new(scene: Scene, camera: Camera, film: Film, samples: u32, seed: u32) -> Self { + Self { + thread: RenderThread::spawn(scene, camera, film, samples, seed), + } + } + + pub fn stop(self) -> thread::Result { + self.thread.stop() + } + + pub fn is_finished(&self) -> bool { + self.thread.is_finished() + } + + pub fn join(self) -> thread::Result { + self.thread.join() + } +} + +pub enum RenderState { + Stopped(Film), + Finished(Film), +} + +struct RenderThread { + handle: JoinHandle, + shutdown: Sender<()>, +} + +impl RenderThread { + fn spawn(scene: Scene, camera: Camera, film: Film, samples: u32, seed: u32) -> Self { + let (tx, rx) = channel::<()>(); + let handle = thread::spawn(move || render_thread(scene, camera, film, samples, seed, rx)); + + Self { + handle, + shutdown: tx, + } + } + + fn is_finished(&self) -> bool { + self.handle.is_finished() + } + + fn join(self) -> thread::Result { + self.handle.join() + } + + fn stop(self) -> thread::Result { + let _ = self.shutdown.send(()); + self.handle.join() + } +} + +fn render_thread( + scene: Scene, camera: Camera, - objects: Vec, -} + mut film: Film, + samples: u32, + seed: u32, + shutdown: Receiver<()>, +) -> RenderState { + let mut sampler = film.sampler(samples, seed); + let t = film.transform; + let w = film.width as usize; -impl Scene { - pub fn new(camera: Camera, objects: Vec) -> Self { - Self { camera, objects } - } + for (i, mut c) in film.lanes_mut().into_iter().enumerate() { + let mut rgb = Rgb::BLACK; - pub fn render(&self, width: u32, height: u32, samples: u32, seed: u32) -> Box<[Rgba]> { - let mut sampler = Sampler::new(width, height, samples, seed); - let mut film = Film::new(width, height); + let x = i % w; + let y = i / w; - for (y, x) in Range2D::new(0..height, 0..width) { - let mut rgb = Vector::ZERO; + for mut pixel_sample in sampler.pixel(x as u32, y as u32) { + let p_film = get_camera_sample(x as u32, y as u32, &mut pixel_sample, t); + let ray = camera.ray(p_film); - for mut pixel_sample in sampler.pixel(x, y) { - let p_film = film.get_camera_sample(x, y, &mut pixel_sample); - let ray = self.camera.ray(p_film); - - if let Some(h) = self.intersect(ray) { - rgb += h.normal.normalize() * 0.5 + vec3(0.5, 0.5, 0.5); - } + if let Some(h) = scene.intersect(ray) { + rgb += dir_color(h.normal); } - rgb /= samples as f32; - - film.add_sample(x, y, rgba(rgb.x, rgb.y, rgb.z, 0.0), 1.0); - } - - film.into_data() - } - - fn intersect(&self, r: Ray) -> Option { - let mut hit: Option = None; - - for obj in &self.objects { - let t_max = hit.as_ref().map(|h| h.t).unwrap_or(f32::INFINITY); - if let Some(h) = obj.intersect(&r, t_max) { - hit = Some(h) + use mpsc::TryRecvError as E; + match shutdown.try_recv() { + Ok(_) | Err(E::Disconnected) => return RenderState::Stopped(film), + Err(E::Empty) => (), } } - hit + rgb /= samples as f32; + for i in 0..3 { + c[i] = rgb[i]; + } } + + RenderState::Finished(film) +} + +fn get_camera_sample(x: u32, y: u32, sampler: &mut PixelSample, transform: Transform2) -> Point2 { + transform * (point2(x as f32, y as f32) + sampler.get_2d().into()) +} + +fn dir_color(v: Vector) -> Rgb { + (v.normalize() * 0.5 + Vector::splat(0.5)).into() } diff --git a/core/src/sampling.rs b/core/src/sampling.rs index fa68689..a88cd41 100644 --- a/core/src/sampling.rs +++ b/core/src/sampling.rs @@ -20,6 +20,7 @@ const PERMUTATIONS: [[u8; 4]; 24] = [ [3, 0, 1, 2], [3, 0, 2, 1], [3, 1, 0, 2], [3, 1, 2, 0], [3, 2, 0, 1], [3, 2, 1, 0], ]; +#[derive(Clone)] pub struct Sampler { seed: u32, samples: u32, diff --git a/core/src/scene.rs b/core/src/scene.rs new file mode 100644 index 0000000..219d69c --- /dev/null +++ b/core/src/scene.rs @@ -0,0 +1,26 @@ +use crate::{ + Shape, + geometry::{Hit, Hittable, Ray}, +}; + +#[derive(Debug, Clone)] +pub struct Scene(Vec); + +impl Scene { + pub fn new(objects: Vec) -> Self { + Self(objects) + } + + pub fn intersect(&self, r: Ray) -> Option { + let mut hit: Option = None; + + for obj in &self.0 { + let t_max = hit.as_ref().map(|h| h.t).unwrap_or(f32::INFINITY); + if let Some(h) = obj.intersect(&r, t_max) { + hit = Some(h) + } + } + + hit + } +} diff --git a/examples/basic.py b/examples/basic.py index 4810bb0..6bbae16 100755 --- a/examples/basic.py +++ b/examples/basic.py @@ -1,10 +1,14 @@ #!python +import argparse from PIL import Image -from mechthild import Scene, Shape, Camera +from mechthild import Session, Shape, Camera -WIDTH = 800 -HEIGHT = 600 -SAMPLES = 32 +parser = argparse.ArgumentParser() +parser.add_argument('width', type=int, default=400, nargs='?') +parser.add_argument('height', type=int, default=400, nargs='?') +parser.add_argument('samples', type=int, default=32, nargs='?') + +args = parser.parse_args() s1 = Shape.sphere((0.75, 1.0, 1.0), 1.0) s2 = Shape.sphere((-1.5, 2.0, -2.0), 2.0) @@ -12,10 +16,10 @@ ground = Shape.plane((0.0, 1.0, 0.0), 0.0) camera = Camera((0.0, 1.5, 5.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), 80.0) -scene = Scene(camera, [s1, s2, ground]) +session = Session([s1, s2, ground], camera, args.width, args.height, args.samples) -render = scene.render(WIDTH, HEIGHT, SAMPLES) -rgba = bytes([int(v * 255) for v in render]) +film = (session.get_output() * 255).astype('uint8') -image = Image.frombuffer('RGBA', (WIDTH, HEIGHT), rgba) +image = Image.fromarray(film) image.save("test.png") + diff --git a/py/Cargo.toml b/py/Cargo.toml index b3f8da0..c4eafaf 100644 --- a/py/Cargo.toml +++ b/py/Cargo.toml @@ -6,12 +6,11 @@ authors.workspace = true license.workspace = true repository.workspace = true -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] name = "mechthild" crate-type = ["cdylib"] [dependencies] -pyo3 = "0.24.0" -mechthild_core = { path = "../core", features = ["bytemuck"] } -bytemuck = { version = "1.22.0", features = ["extern_crate_alloc"] } +pyo3 = { version = "0.25.0", features = ["extension-module"]} +numpy = "0.25.0" +mechthild_core = { path = "../core" } diff --git a/py/src/lib.rs b/py/src/lib.rs index aad00b3..c0504c2 100644 --- a/py/src/lib.rs +++ b/py/src/lib.rs @@ -1,5 +1,7 @@ -use bytemuck::allocation::cast_slice_box; -use mechthild_core::{Camera, Scene, Shape, color::Rgba}; +use std::{mem, panic}; + +use mechthild_core::{Camera, Film, RenderSession, RenderState, Scene, Shape}; +use numpy::{Ix3, PyArray}; use pyo3::prelude::*; #[pyclass(name = "Camera")] @@ -44,22 +46,86 @@ impl PyShape { } } -#[pyclass(name = "Scene")] -struct PyScene(Scene); +#[pyclass(name = "Session")] +struct PySession(Session); -#[pymethods] -impl PyScene { - #[new] - pub fn new(camera: &PyCamera, contents: Vec) -> Self { - let contents = contents.into_iter().map(|s| s.0).collect(); - Self(Scene::new(camera.0.clone(), contents)) +enum Session { + Running(RenderSession), + Finished(Film), + Stopped(Film), + // i hate this + Empty, +} + +impl Default for Session { + fn default() -> Self { + Self::Empty + } +} + +impl Session { + fn new(scene: Scene, camera: Camera, film: Film, samples: u32, seed: u32) -> Self { + Self::Running(RenderSession::new(scene, camera, film, samples, seed)) } - #[pyo3(signature = (width, height, samples, seed = 0))] - pub fn render(&self, width: u32, height: u32, samples: u32, seed: u32) -> Vec { - let result = self.0.render(width, height, samples, seed); + fn take(&mut self) -> Option { + let session = mem::take(self); - cast_slice_box::(result).into_vec() + if let Self::Running(s) = session { + Some(s) + } else { + *self = session; + None + } + } +} + +#[pymethods] +impl PySession { + #[new] + #[pyo3(signature = (objects, camera, width, height, samples, seed = 0))] + pub fn new( + objects: Vec, + camera: &PyCamera, + width: u32, + height: u32, + samples: u32, + seed: u32, + ) -> Self { + let film = Film::new(width, height); + let scene = Scene::new(objects.into_iter().map(|s| s.0).collect()); + + Self(Session::new(scene, camera.0.clone(), film, samples, seed)) + } + + pub fn wait(&mut self) { + if let Some(session) = self.0.take() { + *self = match session.join() { + Ok(RenderState::Finished(f)) => Self(Session::Finished(f)), + Ok(RenderState::Stopped(f)) => Self(Session::Stopped(f)), + Err(e) => panic::resume_unwind(e), + } + } + } + + pub fn stop(&mut self) { + if let Some(session) = self.0.take() { + *self = match session.stop() { + Ok(RenderState::Finished(f)) => Self(Session::Finished(f)), + Ok(RenderState::Stopped(f)) => Self(Session::Stopped(f)), + Err(e) => panic::resume_unwind(e), + } + } + } + + pub fn get_output<'py>(&mut self, py: Python<'py>) -> Bound<'py, PyArray> { + self.wait(); + + match &self.0 { + Session::Finished(f) => PyArray::from_array(py, &f.data), + Session::Stopped(f) => PyArray::from_array(py, &f.data), + _ => unreachable!(), + } } } @@ -67,6 +133,6 @@ impl PyScene { fn mechthild(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - m.add_class::()?; + m.add_class::()?; Ok(()) }