Skip to content

Commit d02a188

Browse files
committed
Switch to storing gfx inside the python runtime.
1 parent 3e8e10d commit d02a188

File tree

5 files changed

+84
-141
lines changed

5 files changed

+84
-141
lines changed

crates/processing_pyo3/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ crate-type = ["cdylib"]
1414
pyo3 = "0.27.0"
1515
processing = { workspace = true }
1616
bevy = { workspace = true }
17+
glfw = { version = "0.60.0"}
18+
19+
[target.'cfg(target_os = "macos")'.dependencies]
1720
glfw = { version = "0.60.0", features = ["static-link"] }
1821

1922
[target.'cfg(target_os = "linux")'.dependencies]
20-
glfw = { version = "0.60.0", features = ["static-link", "wayland"] }
23+
glfw = { version = "0.60.0", features = ["wayland"] }

crates/processing_pyo3/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ Prototype for python bindings to libprocessing
77
### Install venv and maturin
88
Follow these [installation instructions](https://pyo3.rs/v0.27.2/getting-started.html)
99

10+
#### macOS
11+
```bash
12+
brew install glfw
13+
```
14+
1015
### Running code
1116
```
1217
$ maturin develop

crates/processing_pyo3/src/graphics.rs

Lines changed: 41 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,28 @@ use processing::prelude::*;
33
use pyo3::exceptions::PyRuntimeError;
44
use pyo3::prelude::*;
55
use pyo3::types::PyAny;
6-
use std::cell::RefCell;
76

87
use crate::glfw::GlfwContext;
98

10-
// The global graphics context
11-
// we use thread_local! to ensure that the context is specific to the current thread, particularly
12-
// becausae glfw requires that all window operations happen on the main thread.
13-
// TODO: we'll want to think about how to enable multi-window / multi-threaded usage in the future
14-
thread_local! {
15-
static GRAPHICS_CTX: RefCell<Option<Py<Graphics>>> = const { RefCell::new(None) };
16-
}
17-
189
#[pyclass(unsendable)]
1910
pub struct Graphics {
20-
// this makes our object !Send, hence unsendable above
2111
glfw_ctx: GlfwContext,
2212
surface: Entity,
2313
}
2414

2515
#[pymethods]
2616
impl Graphics {
27-
// TODO: in theory users can create multiple Graphics objects, which they manually manage themselves.
28-
// right now we just support the single global window via the module-level functions.
2917
#[new]
30-
fn new(width: u32, height: u32) -> PyResult<Self> {
18+
pub fn new(width: u32, height: u32) -> PyResult<Self> {
3119
let glfw_ctx = GlfwContext::new(width, height)
32-
.map_err(|e| PyRuntimeError::new_err(format!("Couold not create window {e}")))?;
20+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
3321

34-
init().map_err(|e| PyRuntimeError::new_err(format!("Failed to initialize processing {e}")))?;
22+
init().map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
3523

3624
let window_handle = glfw_ctx.get_window();
3725
let display_handle = glfw_ctx.get_display();
3826
let surface = surface_create(window_handle, display_handle, width, height, 1.0)
39-
.map_err(|e| PyRuntimeError::new_err(format!("Could not create surface {e}")))?;
27+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
4028

4129
Ok(Self { glfw_ctx, surface })
4230
}
@@ -45,73 +33,64 @@ impl Graphics {
4533
let (r, g, b, a) = parse_color(&args)?;
4634
let color = bevy::color::Color::srgba(r, g, b, a);
4735
record_command(self.surface, DrawCommand::BackgroundColor(color))
48-
.map_err(|e| PyRuntimeError::new_err(format!("background failed {e}")))
36+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
4937
}
5038

5139
pub fn fill(&self, args: Vec<f32>) -> PyResult<()> {
5240
let (r, g, b, a) = parse_color(&args)?;
5341
let color = bevy::color::Color::srgba(r, g, b, a);
5442
record_command(self.surface, DrawCommand::Fill(color))
55-
.map_err(|e| PyRuntimeError::new_err(format!("fill failed {e}")))
43+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
5644
}
5745

5846
pub fn no_fill(&self) -> PyResult<()> {
5947
record_command(self.surface, DrawCommand::NoFill)
60-
.map_err(|e| PyRuntimeError::new_err(format!("no_fill failed {e}")))
48+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
6149
}
6250

6351
pub fn stroke(&self, args: Vec<f32>) -> PyResult<()> {
6452
let (r, g, b, a) = parse_color(&args)?;
6553
let color = bevy::color::Color::srgba(r, g, b, a);
6654
record_command(self.surface, DrawCommand::StrokeColor(color))
67-
.map_err(|e| PyRuntimeError::new_err(format!("stroke failed {e}")))
55+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
6856
}
6957

7058
pub fn no_stroke(&self) -> PyResult<()> {
7159
record_command(self.surface, DrawCommand::NoStroke)
72-
.map_err(|e| PyRuntimeError::new_err(format!("no_stroke failed {e}")))
60+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
7361
}
7462

7563
pub fn stroke_weight(&self, weight: f32) -> PyResult<()> {
7664
record_command(self.surface, DrawCommand::StrokeWeight(weight))
77-
.map_err(|e| PyRuntimeError::new_err(format!("stroke_weight failed {e}")))
65+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
7866
}
7967

8068
pub fn rect(&self, x: f32, y: f32, w: f32, h: f32, tl: f32, tr: f32, br: f32, bl: f32) -> PyResult<()> {
8169
record_command(
8270
self.surface,
83-
DrawCommand::Rect {
84-
x,
85-
y,
86-
w,
87-
h,
88-
radii: [tl, tr, br, bl],
89-
},
71+
DrawCommand::Rect { x, y, w, h, radii: [tl, tr, br, bl] },
9072
)
91-
.map_err(|e| PyRuntimeError::new_err(format!("rect failed {e}")))
73+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
9274
}
9375

9476
pub fn run(&mut self, draw_fn: Option<Py<PyAny>>) -> PyResult<()> {
9577
loop {
96-
let running = self.glfw_ctx.poll_events();
97-
if !running {
78+
if !self.glfw_ctx.poll_events() {
9879
break;
9980
}
10081

10182
begin_draw(self.surface)
102-
.map_err(|e| PyRuntimeError::new_err(format!("begin_draw failed {e}")))?;
83+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
10384

10485
if let Some(ref draw) = draw_fn {
10586
Python::attach(|py| {
106-
draw.call0(py)
107-
.map_err(|e| PyRuntimeError::new_err(format!("draw failed {e}")))
87+
draw.call0(py).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
10888
})?;
10989
}
11090

11191
end_draw(self.surface)
112-
.map_err(|e| PyRuntimeError::new_err(format!("end_draw failed {e}")))?;
92+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
11393
}
114-
11594
Ok(())
11695
}
11796
}
@@ -120,68 +99,34 @@ impl Graphics {
12099
// about how to expose different color spaces in an idiomatic pythonic way
121100
fn parse_color(args: &[f32]) -> PyResult<(f32, f32, f32, f32)> {
122101
match args.len() {
123-
4 => Ok((
124-
args[0] / 255.0,
125-
args[1] / 255.0,
126-
args[2] / 255.0,
127-
args[3] / 255.0,
128-
)),
129-
_ => Err(PyRuntimeError::new_err(
130-
"Color requires 4 arguments",
131-
)),
102+
1 => {
103+
let v = args[0] / 255.0;
104+
Ok((v, v, v, 1.0))
105+
}
106+
2 => {
107+
let v = args[0] / 255.0;
108+
Ok((v, v, v, args[1] / 255.0))
109+
}
110+
3 => Ok((args[0] / 255.0, args[1] / 255.0, args[2] / 255.0, 1.0)),
111+
4 => Ok((args[0] / 255.0, args[1] / 255.0, args[2] / 255.0, args[3] / 255.0)),
112+
_ => Err(PyRuntimeError::new_err("color requires 1-4 arguments")),
132113
}
133114
}
134115

135-
/// Run inside the current graphics context
136-
pub fn with_graphics<F, T>(f: F) -> PyResult<T>
137-
where
138-
F: FnOnce(PyRef<'_, Graphics>) -> PyResult<T>,
139-
{
140-
GRAPHICS_CTX.with(|cell| {
141-
let opt = cell.borrow();
142-
match opt.as_ref() {
143-
Some(py_graphics) => Python::attach(|py| {
144-
let graphics = py_graphics.bind(py).borrow();
145-
f(graphics)
146-
}),
147-
None => Err(PyRuntimeError::new_err(
148-
"No graphics context",
149-
)),
150-
}
151-
})
116+
pub fn get_graphics<'py>(module: &Bound<'py, PyModule>) -> PyResult<PyRef<'py, Graphics>> {
117+
module
118+
.getattr("_graphics")?
119+
.cast_into::<Graphics>()
120+
.map_err(|_| PyRuntimeError::new_err("no graphics context"))?
121+
.try_borrow()
122+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
152123
}
153124

154-
/// Run inside the current graphics context with mutable access
155-
pub fn with_graphics_mut<F, T>(f: F) -> PyResult<T>
156-
where
157-
F: FnOnce(PyRefMut<'_, Graphics>) -> PyResult<T>,
158-
{
159-
GRAPHICS_CTX.with(|cell| {
160-
let opt = cell.borrow();
161-
match opt.as_ref() {
162-
Some(py_graphics) => Python::attach(|py| {
163-
let graphics = py_graphics.bind(py).borrow_mut();
164-
f(graphics)
165-
}),
166-
None => Err(PyRuntimeError::new_err(
167-
"No graphics context",
168-
)),
169-
}
170-
})
125+
pub fn get_graphics_mut<'py>(module: &Bound<'py, PyModule>) -> PyResult<PyRefMut<'py, Graphics>> {
126+
module
127+
.getattr("_graphics")?
128+
.cast_into::<Graphics>()
129+
.map_err(|_| PyRuntimeError::new_err("no graphics context"))?
130+
.try_borrow_mut()
131+
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
171132
}
172-
173-
/// Create the module level graphics context
174-
pub fn create_context(width: u32, height: u32) -> PyResult<()> {
175-
let already_exists = GRAPHICS_CTX.with(|cell| cell.borrow().is_some());
176-
if already_exists {
177-
return Err(PyRuntimeError::new_err("A context already exists"));
178-
}
179-
180-
Python::attach(|py| {
181-
let graphics = Py::new(py, Graphics::new(width, height)?)?;
182-
GRAPHICS_CTX.with(|cell| {
183-
*cell.borrow_mut() = Some(graphics);
184-
});
185-
Ok(())
186-
})
187-
}

crates/processing_pyo3/src/lib.rs

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,77 @@
1-
mod graphics;
21
mod glfw;
2+
mod graphics;
33

4+
use graphics::{get_graphics, get_graphics_mut, Graphics};
45
use pyo3::prelude::*;
5-
use crate::graphics::{with_graphics, with_graphics_mut};
6+
use pyo3::types::PyAny;
67

78
#[pymodule]
89
fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> {
9-
m.add_class::<graphics::Graphics>()?;
10-
11-
// settings / lifecycle
10+
m.add_class::<Graphics>()?;
1211
m.add_function(wrap_pyfunction!(size, m)?)?;
1312
m.add_function(wrap_pyfunction!(run, m)?)?;
14-
15-
// draw state
1613
m.add_function(wrap_pyfunction!(background, m)?)?;
1714
m.add_function(wrap_pyfunction!(fill, m)?)?;
1815
m.add_function(wrap_pyfunction!(no_fill, m)?)?;
1916
m.add_function(wrap_pyfunction!(stroke, m)?)?;
2017
m.add_function(wrap_pyfunction!(no_stroke, m)?)?;
2118
m.add_function(wrap_pyfunction!(stroke_weight, m)?)?;
22-
23-
// drawing prims
2419
m.add_function(wrap_pyfunction!(rect, m)?)?;
25-
26-
2720
Ok(())
2821
}
2922

30-
// these are all our module-level functions
31-
//
32-
// in processing4 java, the sketch runs implicitly inside a class that extends PApplet and
33-
// executes main. here we have do a little magic trick to get similar behavior. if we required
34-
// users to create a Graphics object and call its methods, it would be ugly because we lack
35-
// an implicit receiver like 'this' in java. so instead we create a singleton Graphics object
36-
// behind the scenes and have these module-level functions forward to that object.
37-
3823
#[pyfunction]
39-
fn size(width: u32, height: u32) -> PyResult<()> {
40-
graphics::create_context(width, height)
24+
#[pyo3(pass_module)]
25+
fn size(module: &Bound<'_, PyModule>, width: u32, height: u32) -> PyResult<()> {
26+
let graphics = Graphics::new(width, height)?;
27+
module.setattr("_graphics", graphics)?;
28+
Ok(())
4129
}
4230

4331
#[pyfunction]
44-
#[pyo3(signature = (draw_fn=None))]
45-
fn run(draw_fn: Option<Py<PyAny>>) -> PyResult<()> {
46-
with_graphics_mut(|mut g| g.run(draw_fn))
32+
#[pyo3(pass_module, signature = (draw_fn=None))]
33+
fn run(module: &Bound<'_, PyModule>, draw_fn: Option<Py<PyAny>>) -> PyResult<()> {
34+
get_graphics_mut(module)?.run(draw_fn)
4735
}
4836

49-
5037
#[pyfunction]
51-
#[pyo3(signature = (*args))]
52-
fn background(args: Vec<f32>) -> PyResult<()> {
53-
with_graphics(|g| g.background(args.to_vec()))
38+
#[pyo3(pass_module, signature = (*args))]
39+
fn background(module: &Bound<'_, PyModule>, args: Vec<f32>) -> PyResult<()> {
40+
get_graphics(module)?.background(args)
5441
}
5542

5643
#[pyfunction]
57-
#[pyo3(signature = (*args))]
58-
fn fill(args: Vec<f32>) -> PyResult<()> {
59-
with_graphics(|g| g.fill(args.to_vec()))
44+
#[pyo3(pass_module, signature = (*args))]
45+
fn fill(module: &Bound<'_, PyModule>, args: Vec<f32>) -> PyResult<()> {
46+
get_graphics(module)?.fill(args)
6047
}
6148

6249
#[pyfunction]
63-
fn no_fill() -> PyResult<()> {
64-
with_graphics(|g| g.no_fill())
50+
#[pyo3(pass_module)]
51+
fn no_fill(module: &Bound<'_, PyModule>) -> PyResult<()> {
52+
get_graphics(module)?.no_fill()
6553
}
6654

6755
#[pyfunction]
68-
#[pyo3(signature = (*args))]
69-
fn stroke(args: Vec<f32>) -> PyResult<()> {
70-
with_graphics(|g| g.stroke(args.to_vec()))
56+
#[pyo3(pass_module, signature = (*args))]
57+
fn stroke(module: &Bound<'_, PyModule>, args: Vec<f32>) -> PyResult<()> {
58+
get_graphics(module)?.stroke(args)
7159
}
7260

7361
#[pyfunction]
74-
fn no_stroke() -> PyResult<()> {
75-
with_graphics(|g| g.no_stroke())
62+
#[pyo3(pass_module)]
63+
fn no_stroke(module: &Bound<'_, PyModule>) -> PyResult<()> {
64+
get_graphics(module)?.no_stroke()
7665
}
7766

7867
#[pyfunction]
79-
fn stroke_weight(weight: f32) -> PyResult<()> {
80-
with_graphics(|g| g.stroke_weight(weight))
68+
#[pyo3(pass_module)]
69+
fn stroke_weight(module: &Bound<'_, PyModule>, weight: f32) -> PyResult<()> {
70+
get_graphics(module)?.stroke_weight(weight)
8171
}
8272

8373
#[pyfunction]
84-
#[pyo3(signature = (x, y, w, h, tl=0.0, tr=0.0, br=0.0, bl=0.0))]
85-
fn rect(x: f32, y: f32, w: f32, h: f32, tl: f32, tr: f32, br: f32, bl: f32) -> PyResult<()> {
86-
with_graphics(|g| g.rect(x, y, w, h, tl, tr, br, bl))
74+
#[pyo3(pass_module, signature = (x, y, w, h, tl=0.0, tr=0.0, br=0.0, bl=0.0))]
75+
fn rect(module: &Bound<'_, PyModule>, x: f32, y: f32, w: f32, h: f32, tl: f32, tr: f32, br: f32, bl: f32) -> PyResult<()> {
76+
get_graphics(module)?.rect(x, y, w, h, tl, tr, br, bl)
8777
}

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ default:
44
py-build:
55
cd crates/processing_pyo3 && uv run maturin develop
66

7-
py-run file:
7+
py-run file: py-build
88
cd crates/processing_pyo3 && uv run python ../../{{file}}
99

1010
wasm-build:

0 commit comments

Comments
 (0)