Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/processing_core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pub type Result<T> = std::result::Result<T, ProcessingError>;
pub enum ProcessingError {
#[error("App was accessed from multiple threads")]
AppAccess,
#[error("Not initialized yet; call size() first")]
AppNotInitialized,
#[error("Error initializing tracing: {0}")]
Tracing(#[from] tracing::subscriber::SetGlobalDefaultError),
#[error("Surface not found")]
Expand Down
2 changes: 1 addition & 1 deletion crates/processing_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub fn app_mut<T>(cb: impl FnOnce(&mut App) -> error::Result<T>) -> error::Resul
.map_err(|_| error::ProcessingError::AppAccess)?;
let app = app_borrow
.as_mut()
.ok_or(error::ProcessingError::AppAccess)?;
.ok_or(error::ProcessingError::AppNotInitialized)?;
cb(app)
});
match res {
Expand Down
14 changes: 12 additions & 2 deletions crates/processing_pyo3/src/compute.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use bevy::color::ColorToComponents;
use bevy::prelude::Entity;
use processing::prelude::*;
use pyo3::{
Expand Down Expand Up @@ -112,7 +113,7 @@ impl Buffer {
value: &Bound<'_, PyAny>,
) -> PyResult<()> {
if let Ok(i) = index.extract::<isize>() {
let sv = py_to_shader_value(value)?;
let sv = buffer_element(value)?;
self.check_element_type(&sv)?;
let bytes = sv
.to_bytes()
Expand Down Expand Up @@ -246,11 +247,19 @@ fn slice_positions(indices: &PySliceIndices) -> impl Iterator<Item = isize> + us
(0..slicelength as isize).map(move |i| start + i * step)
}

/// A buffer has no color type: a `Color` is stored as its linear RGBA floats.
fn buffer_element(value: &Bound<'_, PyAny>) -> PyResult<ShaderValue> {
Ok(match py_to_shader_value(value)? {
ShaderValue::Color(c) => ShaderValue::Float4(c.to_linear().to_f32_array()),
sv => sv,
})
}

fn shader_values_to_bytes(values: &Bound<'_, PyAny>) -> PyResult<(Vec<u8>, Option<ShaderValue>)> {
let mut bytes = Vec::new();
let mut element_type: Option<ShaderValue> = None;
for item in values.try_iter()? {
let sv = py_to_shader_value(&item?)?;
let sv = buffer_element(&item?)?;
if let Some(ref existing) = element_type
&& std::mem::discriminant(existing) != std::mem::discriminant(&sv)
{
Expand Down Expand Up @@ -288,6 +297,7 @@ fn shader_value_to_py<'py>(py: Python<'py>, sv: &ShaderValue) -> PyResult<Bound<
ShaderValue::UInt3(v) => list(py, v),
ShaderValue::UInt4(v) => list(py, v),
ShaderValue::Mat4(v) => list(py, v),
ShaderValue::Color(c) => Ok(crate::color::PyColor(*c).into_pyobject(py)?.into_any()),
ShaderValue::Texture(_)
| ShaderValue::Buffer(_)
| ShaderValue::MeshAttribute(..)
Expand Down
9 changes: 8 additions & 1 deletion crates/processing_pyo3/src/gltf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use bevy::prelude::Entity;
use processing::prelude::*;
use pyo3::{exceptions::PyRuntimeError, prelude::*};

use crate::graphics::{Geometry, Light};
use crate::graphics::{Geometry, Image, Light};
use crate::material::Material;

#[pyclass(unsendable)]
Expand Down Expand Up @@ -35,6 +35,13 @@ impl Gltf {
Ok(Material { entity })
}

/// The texture on the named mesh, e.g. for `LOOKUP`.
pub fn texture(&self, name: &str) -> PyResult<Image> {
let entity =
gltf_texture(self.entity, name).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
Ok(Image::wrap(entity))
}

pub fn mesh_names(&self) -> PyResult<Vec<String>> {
gltf_mesh_names(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}
Expand Down
108 changes: 79 additions & 29 deletions crates/processing_pyo3/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ use pyo3::{
types::{PyDict, PyList, PyTuple},
};
use shader::Shader;
use std::ffi::{CStr, CString};
use std::ffi::CString;

/// Register a window `Graphics` in the module's `_windows` list so the run loop
/// draws + presents it each frame.
Expand Down Expand Up @@ -349,7 +349,11 @@ fn create_graphics_context(
let env = detect_environment(module)?;

let interactive = env != "script";
let log_level = if interactive { Some("error") } else { None };
let log_level = if interactive {
Some("error")
} else {
Some("warn")
};

let has_existing = module
.getattr("_graphics")
Expand Down Expand Up @@ -475,6 +479,40 @@ pub(crate) fn reset_tracked_globals() {
LAST_GLOBALS.with(|cache| cache.borrow_mut().clear());
}

/// Live reload: re-define the saved file's imports, functions and classes in the running
/// sketch's globals, so state survives and `size()`/`run()` don't run again.
fn reload_definitions(py: Python<'_>, source: &str, globals: &Bound<'_, PyAny>) -> PyResult<()> {
let ast = PyModule::import(py, "ast")?;
let tree = ast.getattr("parse")?.call1((source,))?;
let keep = PyTuple::new(
py,
[
"FunctionDef",
"AsyncFunctionDef",
"ClassDef",
"Import",
"ImportFrom",
]
.iter()
.map(|n| ast.getattr(*n))
.collect::<PyResult<Vec<_>>>()?,
)?;
let body = PyList::empty(py);
for node in tree.getattr("body")?.try_iter()? {
let node = node?;
if node.is_instance(&keep)? {
body.append(node)?;
}
}
tree.setattr("body", body)?;
let builtins = PyModule::import(py, "builtins")?;
let code = builtins
.getattr("compile")?
.call1((tree, "<sketch reload>", "exec"))?;
builtins.getattr("exec")?.call1((code, globals))?;
Ok(())
}

fn get_sketch_info() -> PyResult<(String, String)> {
Python::attach(|py| {
let sys = PyModule::import(py, "sys")?;
Expand All @@ -483,7 +521,12 @@ fn get_sketch_info() -> PyResult<(String, String)> {
let os = PyModule::import(py, "os")?;
let path = os.getattr("path")?;

if filename.is_empty() || !path.getattr("isfile")?.call1((filename,))?.is_truthy()? {
// live reload watches the sketch file; it's opt-in while it's experimental
let live_reload = env::var_os("PROCESSING_LIVE_RELOAD").is_some_and(|v| !v.is_empty());
if !live_reload
|| filename.is_empty()
|| !path.getattr("isfile")?.call1((filename,))?.is_truthy()?
{
let cwd = os.getattr("getcwd")?.call0()?.to_string();
return Ok((cwd, String::new()));
}
Expand Down Expand Up @@ -1254,7 +1297,7 @@ pub mod processing_py {

ensure_graphics(module)?;

let mut globals = if let Some(ref draw) = draw_fn {
let globals = if let Some(ref draw) = draw_fn {
draw.getattr("__globals__")?
} else if let Some(ref setup) = setup_fn {
setup.getattr("__globals__")?
Expand All @@ -1278,6 +1321,7 @@ pub mod processing_py {
.end_draw()?;

loop {
py.check_signals()?;
{
let mut graphics = get_graphics_mut(module)?
.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?;
Expand All @@ -1298,35 +1342,32 @@ pub mod processing_py {
let mut first_frame = true;

loop {
// also catches Ctrl-C while paused, when no Python code runs
py.check_signals()?;
{
let mut graphics = get_graphics_mut(module)?
.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?;

// TODO: this shouldn't be on the graphics object
let sketch = graphics.poll_for_sketch_update()?;
if !sketch.source.is_empty() {
let locals = PyDict::new(py);

let ok = CString::new(sketch.source.as_str()).unwrap();
let cstr: &CStr = ok.as_c_str();
if !graphics.surface.poll_events() {
break;
}
}

match py.run(cstr, None, Some(&locals)) {
Ok(_) => {}
Err(e) => {
eprintln!("sketch reload error: {e}");
// TODO: this shouldn't be on the graphics object
let sketch = get_graphics(module)?
.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?
.poll_for_sketch_update()?;
if !sketch.source.is_empty() {
match reload_definitions(py, &sketch.source, &globals) {
Ok(()) => {
if let Ok(draw) = globals.get_item("draw") {
*draw_fn_ref = draw;
}
// the re-run `import *` rebound the tracked globals
reset_tracked_globals();
first_frame = true;
}

*draw_fn_ref = locals.get_item("draw").unwrap().unwrap();
globals = draw_fn_ref.getattr("__globals__")?;
reset_tracked_globals();
first_frame = true;

dbg!(locals);
}

if !graphics.surface.poll_events() {
break;
Err(e) => eprintln!("sketch reload error: {e}"),
}
}

Expand Down Expand Up @@ -1360,9 +1401,7 @@ pub mod processing_py {
wg.bind(py).borrow().begin_draw()?;
}
sync_globals(module, &globals)?;
draw_fn_ref
.call0()
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
draw_fn_ref.call0()?;
for wg in &windows {
wg.bind(py).borrow().end_draw()?;
}
Expand All @@ -1377,6 +1416,17 @@ pub mod processing_py {
}

Ok(())
})
.or_else(|e: PyErr| {
// Ctrl-C quits the sketch like closing its window, rather than a
// traceback from wherever draw() happened to be
Python::attach(|py| {
if e.is_instance_of::<pyo3::exceptions::PyKeyboardInterrupt>(py) {
Ok(())
} else {
Err(e)
}
})
});

// tear down the app while the thread-local is still alive; the eager
Expand Down
4 changes: 4 additions & 0 deletions crates/processing_pyo3/src/material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ pub(crate) fn py_to_shader_value(value: &Bound<'_, PyAny>) -> PyResult<shader_va
if let Ok(v) = value.extract::<PyRef<PyVec2>>() {
return Ok(shader_value::ShaderValue::Float2(v.0.to_array()));
}
// before the sequence fallbacks: a Color iterates in its own space (h, s, v, a for hsva)
if let Ok(c) = value.extract::<PyRef<PyColor>>() {
return Ok(shader_value::ShaderValue::Color(c.0));
}

if let Ok(buf) = value.extract::<PyRef<Buffer>>() {
return Ok(shader_value::ShaderValue::Buffer(buf.entity));
Expand Down
29 changes: 25 additions & 4 deletions crates/processing_pyo3/src/particles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,26 @@ fn parse_falloff(s: &str) -> PyResult<u32> {
}
}

/// `emit()` data: flat numbers, or one value per particle (`[x, y, z]`, a `Color`, ...)
/// converted like `Buffer.write`.
fn emit_bytes(name: &str, value: &Bound<'_, PyAny>) -> PyResult<Vec<u8>> {
let mut bytes = Vec::new();
for item in value.try_iter()? {
let item = item?;
// a bare int would become an i32 shader value; attributes are floats
if let Ok(f) = item.extract::<f32>() {
bytes.extend_from_slice(&f.to_le_bytes());
continue;
}
let sv = crate::material::py_to_shader_value(&item)?;
let b = sv.to_bytes().ok_or_else(|| {
PyTypeError::new_err(format!("attribute '{name}': unsupported value {sv:?}"))
})?;
bytes.extend_from_slice(&b);
}
Ok(bytes)
}

#[pyclass(unsendable)]
pub struct Grid {
pub(crate) entity: Entity,
Expand Down Expand Up @@ -977,19 +997,20 @@ impl Particles {
for (key, value) in kwargs.iter() {
let name: String = key.extract()?;
let attr_entity = self.resolve_attribute(&key)?;
particles_ensure_attribute(self.entity, attr_entity)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
let (_, fmt) = geometry_attribute_info(attr_entity)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
let fmt = AttributeFormat::from_inner(fmt);
let floats: Vec<f32> = value.extract()?;
let bytes = emit_bytes(&name, &value)?;
let expected = (n as usize) * fmt.float_count();
if floats.len() != expected {
if bytes.len() != expected * 4 {
return Err(PyRuntimeError::new_err(format!(
"attribute '{name}': expected {expected} floats ({} per particle × {n}), got {}",
fmt.float_count(),
floats.len(),
bytes.len() / 4,
)));
}
let bytes: Vec<u8> = floats.iter().flat_map(|f| f.to_le_bytes()).collect();
data.push((attr_entity, bytes));
}
particles_emit(self.entity, n, data).map_err(|e| PyRuntimeError::new_err(format!("{e}")))
Expand Down
15 changes: 14 additions & 1 deletion crates/processing_render/src/compute.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::{BTreeSet, HashMap};
use std::collections::{BTreeSet, HashMap, HashSet};

use bevy::asset::{AssetId, RenderAssetUsages};
use bevy::mesh::MeshVertexAttribute;
Expand All @@ -21,6 +21,7 @@ use bevy::{
};

use bevy_naga_reflect::dynamic_shader::DynamicShader;
use bevy_naga_reflect::reflect::ParameterCategory;

use crate::geometry::{Attribute, Geometry};
use crate::material::custom::Shader;
Expand Down Expand Up @@ -220,6 +221,8 @@ pub struct Compute {
pub pipeline_id: CachedComputePipelineId,
pub bind_group_layout_descriptors: Vec<(u32, BindGroupLayoutDescriptor)>,
pub mesh_bindings: HashMap<String, MeshBindingRef>,
/// Storage arrays bound to a particle system's columns by the last apply or emit.
pub column_bindings: HashSet<String>,
}

fn queue_pipeline(
Expand Down Expand Up @@ -328,6 +331,7 @@ pub fn create_compute(app: &mut App, shader_entity: Entity) -> Result<Entity> {
pipeline_id,
bind_group_layout_descriptors,
mesh_bindings: HashMap::new(),
column_bindings: HashSet::new(),
})
.id());
}
Expand Down Expand Up @@ -370,6 +374,15 @@ pub fn dispatch(
if !ready {
return Err(ProcessingError::ResourceNotReady(name.to_string()));
}
// wgpu treats an incomplete bind group as fatal, so fail here instead
if matches!(param.category(), ParameterCategory::Storage { .. })
&& shader.buffer_handle(name).is_none()
&& !mesh_bindings.iter().any(|(n, _)| n == name)
{
return Err(ProcessingError::InvalidArgument(format!(
"nothing is bound to the shader's storage array `{name}`"
)));
}
}

let mut bind_groups = Vec::new();
Expand Down
Loading
Loading