From 30d42c2f2a1f374aa84279288ad8e790f9006344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 28 Sep 2026 13:21:23 -0700 Subject: [PATCH 01/20] Compute perf. --- crates/processing_render/src/compute.rs | 37 +++++++-- crates/processing_render/src/lib.rs | 82 ++++++++++++++----- .../processing_render/src/particles/grid.rs | 10 +-- .../processing_render/src/particles/scan.rs | 8 +- 4 files changed, 99 insertions(+), 38 deletions(-) diff --git a/crates/processing_render/src/compute.rs b/crates/processing_render/src/compute.rs index 4b56fe6a..9e593ed9 100644 --- a/crates/processing_render/src/compute.rs +++ b/crates/processing_render/src/compute.rs @@ -112,17 +112,40 @@ pub fn create_buffer_with_data( .id() } -pub fn write_buffer_cpu( - In((handle, offset, data)): In<(Handle, u64, Vec)>, +/// Writes to the GPU buffer, false if it isn't prepared yet. +pub fn write_buffer_gpu( + InRef((handle, offset, data)): InRef<(Handle, u64, Vec)>, + gpu_buffers: Res>, + render_queue: Res, +) -> bool { + let Some(gpu_buffer) = gpu_buffers.get(handle) else { + return false; + }; + render_queue.write_buffer(&gpu_buffer.buffer, *offset, data); + true +} + +/// Writes to the buffer's asset. +pub fn write_buffer_asset( + In(((handle, offset, data), tracked)): In<((Handle, u64, Vec), bool)>, mut buffers: ResMut>, ) -> Result<()> { - let mut asset = buffers - .get_mut(&handle) - .ok_or(ProcessingError::BufferNotFound)?; + let patched = if tracked { + buffers + .get_mut(&handle) + .map(|mut asset| patch_asset(&mut asset, offset, &data)) + } else { + buffers + .get_mut_untracked(handle.id()) + .map(|asset| patch_asset(asset, offset, &data)) + }; + patched.ok_or(ProcessingError::BufferNotFound)? +} + +fn patch_asset(asset: &mut ShaderBuffer, offset: u64, data: &[u8]) -> Result<()> { let dst = asset.data.as_mut().ok_or(ProcessingError::BufferNotFound)?; let start = offset as usize; - let end = start + data.len(); - dst[start..end].copy_from_slice(&data); + dst[start..start + data.len()].copy_from_slice(data); Ok(()) } diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 5b0cc9b2..67280944 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -2432,9 +2432,23 @@ fn buffer_write_range( data.len() ))); } - ensure_buffer_synced(app, entity)?; + let write = (handle, offset, data); + let on_gpu = app + .sub_app_mut(bevy::render::RenderApp) + .world_mut() + .run_system_cached_with(compute::write_buffer_gpu, &write) + .unwrap(); + let synced = app + .world() + .get::(entity) + .ok_or(error::ProcessingError::BufferNotFound)? + .synced; + // next read will refresh + if on_gpu && !synced { + return Ok(()); + } app.world_mut() - .run_system_cached_with(compute::write_buffer_cpu, (handle, offset, data)) + .run_system_cached_with(compute::write_buffer_asset, (write, !on_gpu)) .unwrap() }) } @@ -2527,30 +2541,54 @@ pub fn compute_set( pub fn compute_dispatch(entity: Entity, x: u32, y: u32, z: u32) -> error::Result<()> { app_mut(|app| { app.update(); + dispatch_inner(app, entity, x, y, z) + }) +} - let args = { - let world = app.world(); - let c = world - .get::(entity) - .ok_or(error::ProcessingError::ComputeNotFound)?; - let mesh_bindings = compute::resolve_mesh_bindings(world, c)?; - ( - c.pipeline_id, - c.bind_group_layout_descriptors.clone(), - c.shader.clone(), - mesh_bindings, - x, - y, - z, - ) - }; - app.sub_app_mut(bevy::render::RenderApp) - .world_mut() - .run_system_cached_with(compute::dispatch, args) - .unwrap() +/// Dispatch without an `app.update()` first; only updates if the pipeline isn't compiled yet. +pub(crate) fn compute_dispatch_no_update( + entity: Entity, + x: u32, + y: u32, + z: u32, +) -> error::Result<()> { + app_mut(|app| match dispatch_inner(app, entity, x, y, z) { + Err(error::ProcessingError::PipelineNotReady(_)) => { + app.update(); + dispatch_inner(app, entity, x, y, z) + } + r => r, }) } +fn dispatch_inner(app: &mut App, entity: Entity, x: u32, y: u32, z: u32) -> error::Result<()> { + let args = { + let world = app.world(); + let c = world + .get::(entity) + .ok_or(error::ProcessingError::ComputeNotFound)?; + let mesh_bindings = compute::resolve_mesh_bindings(world, c)?; + ( + c.pipeline_id, + c.bind_group_layout_descriptors.clone(), + c.shader.clone(), + mesh_bindings, + x, + y, + z, + ) + }; + app.sub_app_mut(bevy::render::RenderApp) + .world_mut() + .run_system_cached_with(compute::dispatch, args) + .unwrap()?; + // The kernel may have written its read-write buffers, so their assets are no longer synced. + app.world_mut() + .run_system_cached(compute::invalidate_rw_buffers) + .unwrap(); + Ok(()) +} + pub fn compute_destroy(entity: Entity) -> error::Result<()> { app_mut(|app| { app.world_mut() diff --git a/crates/processing_render/src/particles/grid.rs b/crates/processing_render/src/particles/grid.rs index 0a31f069..0e778111 100644 --- a/crates/processing_render/src/particles/grid.rs +++ b/crates/processing_render/src/particles/grid.rs @@ -6,7 +6,7 @@ use processing_core::error::Result; use crate::particles::scan::prefix_sum_u32; use crate::shader_value::ShaderValue; -use crate::{buffer_create, compute_create, compute_dispatch, compute_set, shader_load}; +use crate::{buffer_create, compute_create, compute_dispatch_no_update, compute_set, shader_load}; const CLEAR_SHADER: &str = "embedded://processing_render/particles/kernels/grid_clear.wgsl"; const COUNT_SHADER: &str = "embedded://processing_render/particles/kernels/grid_count.wgsl"; @@ -89,24 +89,24 @@ pub fn grid_build(grid: &Grid, position: Entity) -> Result<()> { let num_cells = grid.params.num_cells(); compute_set(clear, "counts", ShaderValue::Buffer(grid.offsets))?; - compute_dispatch(clear, (num_cells + 1).div_ceil(CLEAR_WG), 1, 1)?; + compute_dispatch_no_update(clear, (num_cells + 1).div_ceil(CLEAR_WG), 1, 1)?; compute_set(count, "position", ShaderValue::Buffer(position))?; compute_set(count, "counts", ShaderValue::Buffer(grid.offsets))?; set_domain(count, &grid.params)?; - compute_dispatch(count, grid.capacity.div_ceil(PARTICLE_WG), 1, 1)?; + compute_dispatch_no_update(count, grid.capacity.div_ceil(PARTICLE_WG), 1, 1)?; prefix_sum_u32(grid.offsets)?; compute_set(copy, "starts", ShaderValue::Buffer(grid.offsets))?; compute_set(copy, "cursor", ShaderValue::Buffer(grid.cursor))?; - compute_dispatch(copy, num_cells.div_ceil(COPY_WG), 1, 1)?; + compute_dispatch_no_update(copy, num_cells.div_ceil(COPY_WG), 1, 1)?; compute_set(scatter, "position", ShaderValue::Buffer(position))?; compute_set(scatter, "cursor", ShaderValue::Buffer(grid.cursor))?; compute_set(scatter, "sorted", ShaderValue::Buffer(grid.sorted))?; set_domain(scatter, &grid.params)?; - compute_dispatch(scatter, grid.capacity.div_ceil(PARTICLE_WG), 1, 1)?; + compute_dispatch_no_update(scatter, grid.capacity.div_ceil(PARTICLE_WG), 1, 1)?; Ok(()) } diff --git a/crates/processing_render/src/particles/scan.rs b/crates/processing_render/src/particles/scan.rs index bab519e7..e433f683 100644 --- a/crates/processing_render/src/particles/scan.rs +++ b/crates/processing_render/src/particles/scan.rs @@ -6,8 +6,8 @@ use processing_core::error::Result; use crate::shader_value::ShaderValue; use crate::{ - buffer_create, buffer_destroy, buffer_size, compute_create, compute_dispatch, compute_set, - shader_load, + buffer_create, buffer_destroy, buffer_size, compute_create, compute_dispatch_no_update, + compute_set, shader_load, }; const BLOCK: u64 = 256; @@ -67,7 +67,7 @@ pub fn prefix_sum_u32(buffer: Entity) -> Result<()> { compute_set(block, "data", ShaderValue::Buffer(level_bufs[lvl]))?; compute_set(block, "block_sums", ShaderValue::Buffer(sums))?; - compute_dispatch(block, num_blocks as u32, 1, 1)?; + compute_dispatch_no_update(block, num_blocks as u32, 1, 1)?; level_bufs.push(sums); level_ns.push(num_blocks); @@ -82,7 +82,7 @@ pub fn prefix_sum_u32(buffer: Entity) -> Result<()> { let num_blocks = level_ns[k].div_ceil(BLOCK).max(1); compute_set(add, "data", ShaderValue::Buffer(level_bufs[k]))?; compute_set(add, "block_sums", ShaderValue::Buffer(level_bufs[k + 1]))?; - compute_dispatch(add, num_blocks as u32, 1, 1)?; + compute_dispatch_no_update(add, num_blocks as u32, 1, 1)?; } Ok(()) From 2767eae21db59d741ba5ef46dc46e61e65c3487b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 28 Sep 2026 21:30:46 -0700 Subject: [PATCH 02/20] Grid entities. --- assets/shaders/plexus_link.wesl | 62 +++++-------- crates/processing_core/src/error.rs | 2 + crates/processing_ffi/src/lib.rs | 66 ++++++++++++++ .../examples/particles_lissajous.py | 2 +- .../examples/particles_plexus.py | 2 +- crates/processing_pyo3/src/compute.rs | 5 +- crates/processing_pyo3/src/material.rs | 3 + crates/processing_pyo3/src/particles.rs | 36 +++++--- .../shaders/processing/particles.wesl | 7 ++ crates/processing_render/src/lib.rs | 4 +- .../processing_render/src/material/custom.rs | 7 +- .../processing_render/src/particles/emit.rs | 10 +-- .../processing_render/src/particles/grid.rs | 52 +++++++---- .../src/particles/kernels/flock.wgsl | 33 +++---- .../src/particles/kernels/neighbor.wgsl | 41 ++++----- crates/processing_render/src/particles/mod.rs | 2 +- .../processing_render/src/shader_property.rs | 86 +++++++++++++++---- crates/processing_render/src/shader_value.rs | 11 ++- 18 files changed, 280 insertions(+), 151 deletions(-) diff --git a/assets/shaders/plexus_link.wesl b/assets/shaders/plexus_link.wesl index 9b375f0b..e38366a2 100644 --- a/assets/shaders/plexus_link.wesl +++ b/assets/shaders/plexus_link.wesl @@ -1,11 +1,4 @@ -struct GridParams { - grid_min: vec3, - cell_size: f32, - dims_x: u32, - dims_y: u32, - dims_z: u32, - _pad: u32, -} +import processing::particles::{Grid, cell_coords, cell_index}; struct Params { connection_radius: f32, @@ -15,30 +8,17 @@ struct Params { } // Auto-bound by apply(): names must be position/color. -@group(0) @binding(0) var position: array; -@group(0) @binding(1) var color: array; -@group(0) @binding(2) var edge_pos: array; -@group(0) @binding(3) var edge_col: array; -@group(0) @binding(4) var indices: array; -@group(0) @binding(5) var draw_args: array>; -// Bound by grid.bind(). -@group(0) @binding(6) var offsets: array; -@group(0) @binding(7) var sorted: array; -@group(0) @binding(8) var params: Params; -@group(0) @binding(9) var gp: GridParams; - -fn cell_coords(p: vec3, grid_min: vec3, cell_size: f32, dims: vec3) -> vec3 { - let rel = (p - grid_min) / cell_size; - return vec3( - clamp(i32(floor(rel.x)), 0, i32(dims.x) - 1), - clamp(i32(floor(rel.y)), 0, i32(dims.y) - 1), - clamp(i32(floor(rel.z)), 0, i32(dims.z) - 1), - ); -} - -fn cell_index(c: vec3, dims: vec3) -> u32 { - return c.x + c.y * dims.x + c.z * dims.x * dims.y; -} +@group(0) @binding(0) var position: array; +@group(0) @binding(1) var color: array; +@group(0) @binding(2) var edge_pos: array; +@group(0) @binding(3) var edge_col: array; +@group(0) @binding(4) var indices: array; +@group(0) @binding(5) var draw_args: array>; +// set with link.set(grid=grid) +@group(0) @binding(6) var grid_offsets: array; +@group(0) @binding(7) var grid_sorted: array; +@group(0) @binding(8) var params: Params; +@group(0) @binding(9) var grid: Grid; fn load_pos(i: u32) -> vec3 { return vec3(position[i * 3u], position[i * 3u + 1u], position[i * 3u + 2u]); @@ -70,26 +50,26 @@ fn main(@builtin(global_invocation_id) gid: vec3) { let rgb = load_rgb(i); let radius = params.connection_radius; let r2 = radius * radius; - let dims = vec3(gp.dims_x, gp.dims_y, gp.dims_z); - let base = cell_coords(pos, gp.grid_min, gp.cell_size, dims); + let dims = grid.dims; + let base = cell_coords(pos, grid.origin, grid.cell_size, dims); - let reach = max(1, i32(ceil(radius / gp.cell_size))); + let reach = max(1, i32(ceil(radius / grid.cell_size))); let z0 = max(base.z - reach, 0); - let z1 = min(base.z + reach, i32(gp.dims_z) - 1); + let z1 = min(base.z + reach, i32(grid.dims.z) - 1); let y0 = max(base.y - reach, 0); - let y1 = min(base.y + reach, i32(gp.dims_y) - 1); + let y1 = min(base.y + reach, i32(grid.dims.y) - 1); let x0 = max(base.x - reach, 0); - let x1 = min(base.x + reach, i32(gp.dims_x) - 1); + let x1 = min(base.x + reach, i32(grid.dims.x) - 1); var emitted = 0u; for (var cz = z0; cz <= z1; cz++) { for (var cy = y0; cy <= y1; cy++) { for (var cx = x0; cx <= x1; cx++) { let cell = cell_index(vec3(u32(cx), u32(cy), u32(cz)), dims); - let start = offsets[cell]; - let end = offsets[cell + 1u]; + let start = grid_offsets[cell]; + let end = grid_offsets[cell + 1u]; for (var s = start; s < end; s++) { - let j = sorted[s]; + let j = grid_sorted[s]; if j <= i { continue; } // one direction per edge let pj = load_pos(j); let diff = pos - pj; diff --git a/crates/processing_core/src/error.rs b/crates/processing_core/src/error.rs index 41510f34..e4dbdad4 100644 --- a/crates/processing_core/src/error.rs +++ b/crates/processing_core/src/error.rs @@ -60,6 +60,8 @@ pub enum ProcessingError { PipelineNotReady(u32), #[error("Particles not found")] ParticlesNotFound, + #[error("Grid not found")] + GridNotFound, #[error("Font not found")] FontNotFound, #[error("Font load error: {0}")] diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index c81d3c6f..2e4b49fa 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -3442,6 +3442,27 @@ pub unsafe extern "C" fn processing_shader_set_buffer( }); } +/// Binds `{name}_offsets`, `{name}_sorted` and the `{name}` uniform. +/// +/// # Safety +/// - `name` must be non-null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn processing_shader_set_grid( + entity: u64, + name: *const std::ffi::c_char, + grid_id: u64, +) { + error::clear_error(); + error::check(|| { + let name = unsafe { cstr_to_str(name) }?; + shader_set( + Entity::from_bits(entity), + name, + ShaderValue::Grid(Entity::from_bits(grid_id)), + ) + }); +} + #[unsafe(no_mangle)] pub extern "C" fn processing_compute_dispatch(compute_id: u64, x: u32, y: u32, z: u32) { error::clear_error(); @@ -3826,6 +3847,51 @@ pub extern "C" fn processing_particles_apply(particles_id: u64, compute_id: u64) }); } +/// Returns the grid id, or 0 on error. +#[unsafe(no_mangle)] +pub extern "C" fn processing_particles_grid_create( + particles_id: u64, + min_x: f32, + min_y: f32, + min_z: f32, + cell_size: f32, + dims_x: u32, + dims_y: u32, + dims_z: u32, +) -> u64 { + error::clear_error(); + error::check(|| { + let capacity = particles_capacity(Entity::from_bits(particles_id))?; + grid_create( + GridParams { + min: [min_x, min_y, min_z], + cell_size, + dims: [dims_x, dims_y, dims_z], + }, + capacity, + ) + }) + .map(|e| e.to_bits()) + .unwrap_or(0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_particles_grid_build(grid_id: u64, position_buf_id: u64) { + error::clear_error(); + error::check(|| { + grid_build( + Entity::from_bits(grid_id), + Entity::from_bits(position_buf_id), + ) + }); +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_particles_grid_destroy(grid_id: u64) { + error::clear_error(); + error::check(|| grid_destroy(Entity::from_bits(grid_id))); +} + #[unsafe(no_mangle)] pub extern "C" fn processing_particles_draw(graphics_id: u64, particles_id: u64, geometry_id: u64) { error::clear_error(); diff --git a/crates/processing_pyo3/examples/particles_lissajous.py b/crates/processing_pyo3/examples/particles_lissajous.py index ae8e4220..886c3408 100644 --- a/crates/processing_pyo3/examples/particles_lissajous.py +++ b/crates/processing_pyo3/examples/particles_lissajous.py @@ -73,7 +73,7 @@ def draw(): line_alpha=LINE_ALPHA, max_links=MAX_LINKS, ) - grid.bind(link) + link.set(grid=grid) p.apply(link) blend_mode(ALPHA_OVER) diff --git a/crates/processing_pyo3/examples/particles_plexus.py b/crates/processing_pyo3/examples/particles_plexus.py index 00ad6fd9..027d3712 100644 --- a/crates/processing_pyo3/examples/particles_plexus.py +++ b/crates/processing_pyo3/examples/particles_plexus.py @@ -65,7 +65,7 @@ def draw(): grid.build(p.buffer("position")) p.reset_indices() link.set(indices=idx, draw_args=args, link_distance=LINK_DIST, max_links=MAX_LINKS) - grid.bind(link) + link.set(grid=grid) p.apply(link) particles(p, topology="lines") diff --git a/crates/processing_pyo3/src/compute.rs b/crates/processing_pyo3/src/compute.rs index bac55d3b..9f7208fb 100644 --- a/crates/processing_pyo3/src/compute.rs +++ b/crates/processing_pyo3/src/compute.rs @@ -291,8 +291,9 @@ fn shader_value_to_py<'py>(py: Python<'py>, sv: &ShaderValue) -> PyResult Err(PyRuntimeError::new_err( - "cannot convert Texture/Buffer/Mesh* to Python value", + | ShaderValue::MeshIndex(_) + | ShaderValue::Grid(_) => Err(PyRuntimeError::new_err( + "cannot convert Texture/Buffer/Mesh*/Grid to Python value", )), } } diff --git a/crates/processing_pyo3/src/material.rs b/crates/processing_pyo3/src/material.rs index e0d4c8b1..95cf57a9 100644 --- a/crates/processing_pyo3/src/material.rs +++ b/crates/processing_pyo3/src/material.rs @@ -43,6 +43,9 @@ pub(crate) fn py_to_shader_value(value: &Bound<'_, PyAny>) -> PyResult>() { return Ok(shader_value::ShaderValue::Buffer(buf.entity)); } + if let Ok(grid) = value.extract::>() { + return Ok(shader_value::ShaderValue::Grid(grid.entity)); + } if let Ok(v) = value.extract::<[f32; 4]>() { return Ok(shader_value::ShaderValue::Float4(v)); diff --git a/crates/processing_pyo3/src/particles.rs b/crates/processing_pyo3/src/particles.rs index bc169d06..4676c1cc 100644 --- a/crates/processing_pyo3/src/particles.rs +++ b/crates/processing_pyo3/src/particles.rs @@ -117,23 +117,33 @@ fn parse_falloff(s: &str) -> PyResult { #[pyclass(unsendable)] pub struct Grid { - pub(crate) inner: processing_render::particles::grid::Grid, + pub(crate) entity: Entity, } #[pymethods] impl Grid { + /// Opaque id for this object. + pub fn id(&self) -> u64 { + self.entity.to_bits() + } + pub fn build(&self, position: &Buffer) -> PyResult<()> { - grid_build(&self.inner, position.entity) + grid_build(self.entity, position.entity) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } - pub fn bind(&self, compute: &Compute) -> PyResult<()> { - grid_bind(&self.inner, compute.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + #[getter] + pub fn cell_size(&self) -> PyResult { + Ok(grid_get(self.entity) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))? + .params + .cell_size) } +} - #[getter] - pub fn cell_size(&self) -> f32 { - self.inner.params.cell_size +impl Drop for Grid { + fn drop(&mut self) { + let _ = grid_destroy(self.entity); } } @@ -673,7 +683,7 @@ impl Particles { Some(v) => parse_falloff(&v.extract::()?)?, None => FALLOFF_SMOOTHSTEP, }; - let cell = grid.inner.params.cell_size; + let cell = grid.cell_size()?; let radius = kw_f32(kwargs, "radius", cell)?.min(cell); let (out, out_comp) = self.operand(kwargs, "out")?; @@ -704,7 +714,7 @@ impl Particles { }; particles_gather( self.entity, - &grid.inner, + grid.entity, a, out, op, @@ -944,9 +954,9 @@ impl Particles { cell_size, dims, }; - let inner = + let entity = grid_create(params, capacity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; - Ok(Grid { inner }) + Ok(Grid { entity }) } #[pyo3(signature = (grid, **kwargs))] @@ -955,7 +965,7 @@ impl Particles { if let Some(kwargs) = kwargs { crate::compute::set_compute_kwargs(flock, kwargs)?; } - let cell = grid.inner.params.cell_size; + let cell = grid.cell_size()?; let neighbor_distance = kw_f32(kwargs, "neighbor_distance", cell)?.min(cell); compute_set( flock, @@ -963,7 +973,7 @@ impl Particles { shader_value::ShaderValue::Float(neighbor_distance), ) .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; - particles_flock(self.entity, flock, &grid.inner) + particles_flock(self.entity, flock, grid.entity) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } diff --git a/crates/processing_render/shaders/processing/particles.wesl b/crates/processing_render/shaders/processing/particles.wesl index 9545b2b3..66984c73 100644 --- a/crates/processing_render/shaders/processing/particles.wesl +++ b/crates/processing_render/shaders/processing/particles.wesl @@ -10,6 +10,13 @@ fn falloff(d: f32, radius: f32, mode: u32) -> f32 { } } +// a spatial hash grid, bound by `set(grid=...)` as `grid_offsets`, `grid_sorted` and `grid` +struct Grid { + origin: vec3, + cell_size: f32, + dims: vec3, +} + fn cell_coords(p: vec3, grid_min: vec3, cell_size: f32, dims: vec3) -> vec3 { let rel = (p - grid_min) / cell_size; return vec3( diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 67280944..d20e26d4 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -28,7 +28,7 @@ pub use particles::algebra::{ generate, lookup, map, mix, pack, reduce_components, }; pub use particles::compact::compact; -pub use particles::grid::{Grid, GridParams, grid_bind, grid_build, grid_create}; +pub use particles::grid::{Grid, GridParams, grid_build, grid_create, grid_destroy, grid_get}; pub use particles::reduce::{REDUCE_OP_MAX, REDUCE_OP_MIN, REDUCE_OP_SUM, reduce}; pub use particles::sort::bitonic_sort_by_key; pub use particles::{ @@ -2443,7 +2443,7 @@ fn buffer_write_range( .get::(entity) .ok_or(error::ProcessingError::BufferNotFound)? .synced; - // next read will refresh + // next read will refresh if on_gpu && !synced { return Ok(()); } diff --git a/crates/processing_render/src/material/custom.rs b/crates/processing_render/src/material/custom.rs index f39de9d7..7563ece1 100644 --- a/crates/processing_render/src/material/custom.rs +++ b/crates/processing_render/src/material/custom.rs @@ -335,7 +335,7 @@ pub(crate) fn apply_reflect_field( Err(ProcessingError::UnknownShaderProperty(name.to_string())) } -fn apply_field_coerced(field: &mut dyn PartialReflect, value: &dyn PartialReflect) { +pub(crate) fn apply_field_coerced(field: &mut dyn PartialReflect, value: &dyn PartialReflect) { if let Some(coerced) = coerce_numeric(field, value) { field.apply(coerced.as_ref()); } else { @@ -495,9 +495,10 @@ pub(crate) fn shader_value_to_reflect(value: &ShaderValue) -> Result { + | ShaderValue::MeshIndex(_) + | ShaderValue::Grid(_) => { return Err(ProcessingError::InvalidArgument( - "Texture/Buffer/Mesh* must be bound via set_property, not as a uniform value" + "Texture/Buffer/Mesh*/Grid must be bound via set_property, not as a uniform value" .to_string(), )); } diff --git a/crates/processing_render/src/particles/emit.rs b/crates/processing_render/src/particles/emit.rs index f31fb5aa..16e3ca27 100644 --- a/crates/processing_render/src/particles/emit.rs +++ b/crates/processing_render/src/particles/emit.rs @@ -6,7 +6,7 @@ use processing_core::app_mut; use processing_core::error; use crate::geometry; -use crate::particles::grid::{Grid, grid_bind, grid_build}; +use crate::particles::grid::grid_build; use crate::particles::kernels::KernelRequires; use crate::particles::{Particles, particles_ensure_attribute}; use crate::shader_value::ShaderValue; @@ -146,7 +146,7 @@ pub fn particles_emit( pub fn particles_flock( particles_entity: Entity, flock_entity: Entity, - grid: &Grid, + grid: Entity, ) -> error::Result<()> { let position = app_mut(|app| { let world = app.world(); @@ -167,7 +167,7 @@ pub fn particles_flock( })?; grid_build(grid, position)?; - grid_bind(grid, flock_entity)?; + compute_set(flock_entity, "grid", ShaderValue::Grid(grid))?; particles_apply(particles_entity, flock_entity) } @@ -186,7 +186,7 @@ fn neighbor_compute() -> error::Result { pub fn particles_gather( particles_entity: Entity, - grid: &Grid, + grid: Entity, source: Entity, out: Entity, op: u32, @@ -222,7 +222,7 @@ pub fn particles_gather( grid_build(grid, position)?; let neighbor = neighbor_compute()?; - grid_bind(grid, neighbor)?; + compute_set(neighbor, "grid", ShaderValue::Grid(grid))?; compute_set(neighbor, "position", ShaderValue::Buffer(position))?; compute_set(neighbor, "source", ShaderValue::Buffer(source))?; compute_set(neighbor, "out", ShaderValue::Buffer(out))?; diff --git a/crates/processing_render/src/particles/grid.rs b/crates/processing_render/src/particles/grid.rs index 0e778111..cedc5de7 100644 --- a/crates/processing_render/src/particles/grid.rs +++ b/crates/processing_render/src/particles/grid.rs @@ -1,12 +1,16 @@ use std::sync::Mutex; -use bevy::prelude::Entity; +use bevy::prelude::{Component, Entity}; -use processing_core::error::Result; +use processing_core::app_mut; +use processing_core::error::{ProcessingError, Result}; use crate::particles::scan::prefix_sum_u32; use crate::shader_value::ShaderValue; -use crate::{buffer_create, compute_create, compute_dispatch_no_update, compute_set, shader_load}; +use crate::{ + buffer_create, buffer_destroy, compute_create, compute_dispatch_no_update, compute_set, + shader_load, +}; const CLEAR_SHADER: &str = "embedded://processing_render/particles/kernels/grid_clear.wgsl"; const COUNT_SHADER: &str = "embedded://processing_render/particles/kernels/grid_count.wgsl"; @@ -41,7 +45,7 @@ impl GridParams { } } -#[derive(Clone, Copy)] +#[derive(Component, Clone, Copy)] pub struct Grid { pub offsets: Entity, pub cursor: Entity, @@ -54,25 +58,36 @@ const CLEAR_WG: u32 = 256; const PARTICLE_WG: u32 = 64; const COPY_WG: u32 = 256; -pub fn grid_create(params: GridParams, capacity: u32) -> Result { +pub fn grid_create(params: GridParams, capacity: u32) -> Result { let num_cells = params.num_cells(); - let offsets = buffer_create(((num_cells + 1) as u64) * 4)?; - let cursor = buffer_create((num_cells as u64) * 4)?; - let sorted = buffer_create((capacity.max(1) as u64) * 4)?; - Ok(Grid { - offsets, - cursor, - sorted, + let grid = Grid { + offsets: buffer_create(((num_cells + 1) as u64) * 4)?, + cursor: buffer_create((num_cells as u64) * 4)?, + sorted: buffer_create((capacity.max(1) as u64) * 4)?, params, capacity, + }; + app_mut(|app| Ok(app.world_mut().spawn(grid).id())) +} + +pub fn grid_get(entity: Entity) -> Result { + app_mut(|app| { + app.world() + .get::(entity) + .copied() + .ok_or(ProcessingError::GridNotFound) }) } -pub fn grid_bind(grid: &Grid, compute: Entity) -> Result<()> { - compute_set(compute, "offsets", ShaderValue::Buffer(grid.offsets))?; - compute_set(compute, "sorted", ShaderValue::Buffer(grid.sorted))?; - set_domain(compute, &grid.params)?; - Ok(()) +pub fn grid_destroy(entity: Entity) -> Result<()> { + let grid = grid_get(entity)?; + buffer_destroy(grid.offsets)?; + buffer_destroy(grid.cursor)?; + buffer_destroy(grid.sorted)?; + app_mut(|app| { + app.world_mut().despawn(entity); + Ok(()) + }) } fn set_domain(compute: Entity, params: &GridParams) -> Result<()> { @@ -84,7 +99,8 @@ fn set_domain(compute: Entity, params: &GridParams) -> Result<()> { Ok(()) } -pub fn grid_build(grid: &Grid, position: Entity) -> Result<()> { +pub fn grid_build(entity: Entity, position: Entity) -> Result<()> { + let grid = grid_get(entity)?; let (clear, count, copy, scatter) = grid_computes()?; let num_cells = grid.params.num_cells(); diff --git a/crates/processing_render/src/particles/kernels/flock.wgsl b/crates/processing_render/src/particles/kernels/flock.wgsl index 56040c17..b1e75ec3 100644 --- a/crates/processing_render/src/particles/kernels/flock.wgsl +++ b/crates/processing_render/src/particles/kernels/flock.wgsl @@ -1,4 +1,4 @@ -import processing::particles::{cell_coords, cell_index}; +import processing::particles::{Grid, cell_coords, cell_index}; struct FlockParams { sep_distance: f32, @@ -11,21 +11,12 @@ struct FlockParams { min_speed: f32, } -struct GridParams { - grid_min: vec3, - cell_size: f32, - dims_x: u32, - dims_y: u32, - dims_z: u32, - _pad: u32, -} - @group(0) @binding(0) var position: array; @group(0) @binding(1) var velocity: array; -@group(0) @binding(2) var offsets: array; -@group(0) @binding(3) var sorted: array; +@group(0) @binding(2) var grid_offsets: array; +@group(0) @binding(3) var grid_sorted: array; @group(0) @binding(4) var fp: FlockParams; -@group(0) @binding(5) var gp: GridParams; +@group(0) @binding(5) var grid: Grid; fn limit_mag(v: vec3, m: f32) -> vec3 { let len2 = dot(v, v); @@ -59,8 +50,8 @@ fn main(@builtin(global_invocation_id) gid: vec3) { let sep_d2 = fp.sep_distance * fp.sep_distance; let neighbor_d2 = fp.neighbor_distance * fp.neighbor_distance; - let dims = vec3(gp.dims_x, gp.dims_y, gp.dims_z); - let base = cell_coords(pos, gp.grid_min, gp.cell_size, dims); + let dims = grid.dims; + let base = cell_coords(pos, grid.origin, grid.cell_size, dims); let bx = base.x; let by = base.y; let bz = base.z; @@ -73,19 +64,19 @@ fn main(@builtin(global_invocation_id) gid: vec3) { for (var dz = -1; dz <= 1; dz++) { let cz = bz + dz; - if cz < 0 || cz >= i32(gp.dims_z) { continue; } + if cz < 0 || cz >= i32(grid.dims.z) { continue; } for (var dy = -1; dy <= 1; dy++) { let cy = by + dy; - if cy < 0 || cy >= i32(gp.dims_y) { continue; } + if cy < 0 || cy >= i32(grid.dims.y) { continue; } for (var dx = -1; dx <= 1; dx++) { let cx = bx + dx; - if cx < 0 || cx >= i32(gp.dims_x) { continue; } + if cx < 0 || cx >= i32(grid.dims.x) { continue; } let cell = cell_index(vec3(u32(cx), u32(cy), u32(cz)), dims); - let start = offsets[cell]; - let end = offsets[cell + 1u]; + let start = grid_offsets[cell]; + let end = grid_offsets[cell + 1u]; for (var s = start; s < end; s++) { - let j = sorted[s]; + let j = grid_sorted[s]; if j == i { continue; } let diff = pos - load_pos(j); diff --git a/crates/processing_render/src/particles/kernels/neighbor.wgsl b/crates/processing_render/src/particles/kernels/neighbor.wgsl index 966b412f..723c68e1 100644 --- a/crates/processing_render/src/particles/kernels/neighbor.wgsl +++ b/crates/processing_render/src/particles/kernels/neighbor.wgsl @@ -1,13 +1,4 @@ -import processing::particles::{cell_coords, cell_index, falloff}; - -struct GridParams { - grid_min: vec3, - cell_size: f32, - dims_x: u32, - dims_y: u32, - dims_z: u32, - _pad: u32, -} +import processing::particles::{Grid, cell_coords, cell_index, falloff}; struct Params { radius: f32, @@ -20,13 +11,13 @@ const OP_SUM: u32 = 0u; const OP_MEAN: u32 = 1u; const OP_COUNT: u32 = 2u; -@group(0) @binding(0) var position: array; -@group(0) @binding(1) var source: array; -@group(0) @binding(2) var out: array; -@group(0) @binding(3) var offsets: array; -@group(0) @binding(4) var sorted: array; -@group(0) @binding(5) var params: Params; -@group(0) @binding(6) var gp: GridParams; +@group(0) @binding(0) var position: array; +@group(0) @binding(1) var source: array; +@group(0) @binding(2) var out: array; +@group(0) @binding(3) var grid_offsets: array; +@group(0) @binding(4) var grid_sorted: array; +@group(0) @binding(5) var params: Params; +@group(0) @binding(6) var grid: Grid; fn load_pos(i: u32) -> vec3 { return vec3(position[i * 3u], position[i * 3u + 1u], position[i * 3u + 2u]); @@ -42,27 +33,27 @@ fn main(@builtin(global_invocation_id) gid: vec3) { let r2 = params.radius * params.radius; let comps = params.components; - let dims = vec3(gp.dims_x, gp.dims_y, gp.dims_z); - let base = cell_coords(pos, gp.grid_min, gp.cell_size, dims); + let dims = grid.dims; + let base = cell_coords(pos, grid.origin, grid.cell_size, dims); var value = array(0.0, 0.0, 0.0, 0.0); var weight_sum = 0.0; for (var dz = -1; dz <= 1; dz++) { let cz = base.z + dz; - if cz < 0 || cz >= i32(gp.dims_z) { continue; } + if cz < 0 || cz >= i32(grid.dims.z) { continue; } for (var dy = -1; dy <= 1; dy++) { let cy = base.y + dy; - if cy < 0 || cy >= i32(gp.dims_y) { continue; } + if cy < 0 || cy >= i32(grid.dims.y) { continue; } for (var dx = -1; dx <= 1; dx++) { let cx = base.x + dx; - if cx < 0 || cx >= i32(gp.dims_x) { continue; } + if cx < 0 || cx >= i32(grid.dims.x) { continue; } let cell = cell_index(vec3(u32(cx), u32(cy), u32(cz)), dims); - let start = offsets[cell]; - let end = offsets[cell + 1u]; + let start = grid_offsets[cell]; + let end = grid_offsets[cell + 1u]; for (var s = start; s < end; s++) { - let j = sorted[s]; + let j = grid_sorted[s]; let diff = pos - load_pos(j); let d2 = dot(diff, diff); if d2 <= r2 { diff --git a/crates/processing_render/src/particles/mod.rs b/crates/processing_render/src/particles/mod.rs index c415c9b4..5088726d 100644 --- a/crates/processing_render/src/particles/mod.rs +++ b/crates/processing_render/src/particles/mod.rs @@ -23,7 +23,7 @@ pub use compact::compact; pub use emit::{ particles_apply, particles_emit, particles_emit_gpu, particles_flock, particles_gather, }; -pub use grid::{Grid, GridParams, grid_bind, grid_build, grid_create}; +pub use grid::{Grid, GridParams, grid_build, grid_create, grid_destroy, grid_get}; pub use kernels::{ BOUNDS_CLAMP, BOUNDS_REFLECT, BOUNDS_SOFT, BOUNDS_WRAP, COMBINE_ADD, COMBINE_DIV, COMBINE_MAX, COMBINE_MIN, COMBINE_MUL, COMBINE_POW, COMBINE_SUB, FALLOFF_CONST, FALLOFF_CUBIC, diff --git a/crates/processing_render/src/shader_property.rs b/crates/processing_render/src/shader_property.rs index 7d76b73c..a48d1f51 100644 --- a/crates/processing_render/src/shader_property.rs +++ b/crates/processing_render/src/shader_property.rs @@ -1,10 +1,12 @@ use bevy::prelude::*; +use bevy::reflect::ReflectMut; use bevy_naga_reflect::dynamic_shader::DynamicShader; use bevy_naga_reflect::reflect::ParameterCategory; use crate::compute::{Buffer, Compute, MeshBindingRef}; use crate::image::Image as PImage; -use crate::material::custom::{apply_reflect_field, shader_value_to_reflect}; +use crate::material::custom::{apply_field_coerced, apply_reflect_field, shader_value_to_reflect}; +use crate::particles::grid::Grid; use crate::render::filter::Filter; use crate::shader_value::ShaderValue; use processing_core::error::{ProcessingError, Result}; @@ -104,27 +106,81 @@ pub(crate) fn apply_shader_value( } } +/// Binds `{name}_{suffix}` buffers and the fields of the `{name}` uniform struct. +fn bind_struct( + shader: &mut DynamicShader, + name: &str, + type_name: &str, + buffers: &[(&str, Entity)], + values: &[(&str, ShaderValue)], + p_buffers: &mut Query<&mut Buffer>, + p_images: &Query<&PImage>, +) -> Result<()> { + for (suffix, buffer) in buffers { + let binding = format!("{name}_{suffix}"); + apply_shader_value( + shader, + &binding, + ShaderValue::Buffer(*buffer), + p_buffers, + p_images, + )?; + } + let wrong_type = || { + ProcessingError::InvalidArgument(format!( + "`{name}` must be a `particles::{type_name}` uniform" + )) + }; + let param = shader + .field_mut(name) + .ok_or_else(|| ProcessingError::UnknownShaderProperty(name.to_string()))?; + let ReflectMut::Struct(fields) = param.reflect_mut() else { + return Err(wrong_type()); + }; + for (field, value) in values { + let target = fields.field_mut(field).ok_or_else(wrong_type)?; + apply_field_coerced(target, &*shader_value_to_reflect(value)?); + } + Ok(()) +} + pub fn set_property( In((entity, name, value)): In<(Entity, String, ShaderValue)>, mut computes: Query<&mut Compute>, mut filters: Query<&mut Filter>, mut p_buffers: Query<&mut Buffer>, p_images: Query<&PImage>, + grids: Query<&Grid>, ) -> Result { - if let Ok(mut compute) = computes.get_mut(entity) { - match value { - ShaderValue::MeshAttribute(..) | ShaderValue::MeshIndex(..) => { - bind_compute_mesh(&mut compute, name, value)?; - } - other => { - apply_shader_value(&mut compute.shader, &name, other, &mut p_buffers, &p_images)?; - } + let shader = if let Ok(compute) = computes.get_mut(entity) { + if let ShaderValue::MeshAttribute(..) | ShaderValue::MeshIndex(..) = value { + bind_compute_mesh(compute.into_inner(), name, value)?; + return Ok(true); } - return Ok(true); - } - if let Ok(mut filter) = filters.get_mut(entity) { - apply_shader_value(&mut filter.shader, &name, value, &mut p_buffers, &p_images)?; - return Ok(true); + &mut compute.into_inner().shader + } else if let Ok(filter) = filters.get_mut(entity) { + &mut filter.into_inner().shader + } else { + return Ok(false); + }; + match value { + ShaderValue::Grid(grid) => { + let grid = grids.get(grid).map_err(|_| ProcessingError::GridNotFound)?; + bind_struct( + shader, + &name, + "Grid", + &[("offsets", grid.offsets), ("sorted", grid.sorted)], + &[ + ("origin", ShaderValue::Float3(grid.params.min)), + ("cell_size", ShaderValue::Float(grid.params.cell_size)), + ("dims", ShaderValue::UInt3(grid.params.dims)), + ], + &mut p_buffers, + &p_images, + )?; + } + other => apply_shader_value(shader, &name, other, &mut p_buffers, &p_images)?, } - Ok(false) + Ok(true) } diff --git a/crates/processing_render/src/shader_value.rs b/crates/processing_render/src/shader_value.rs index 2f1d4049..5e1f7ce8 100644 --- a/crates/processing_render/src/shader_value.rs +++ b/crates/processing_render/src/shader_value.rs @@ -19,6 +19,8 @@ pub enum ShaderValue { Buffer(Entity), MeshAttribute(Entity, Entity), MeshIndex(Entity), + /// binds `{name}_offsets`, `{name}_sorted` and the `{name}` uniform + Grid(Entity), } impl ShaderValue { @@ -40,7 +42,8 @@ impl ShaderValue { ShaderValue::Texture(_) | ShaderValue::Buffer(_) | ShaderValue::MeshAttribute(..) - | ShaderValue::MeshIndex(_) => None, + | ShaderValue::MeshIndex(_) + | ShaderValue::Grid(_) => None, } } @@ -54,7 +57,8 @@ impl ShaderValue { ShaderValue::Texture(_) | ShaderValue::Buffer(_) | ShaderValue::MeshAttribute(..) - | ShaderValue::MeshIndex(_) => None, + | ShaderValue::MeshIndex(_) + | ShaderValue::Grid(_) => None, } } @@ -103,7 +107,8 @@ impl ShaderValue { ShaderValue::Texture(_) | ShaderValue::Buffer(_) | ShaderValue::MeshAttribute(..) - | ShaderValue::MeshIndex(_) => None, + | ShaderValue::MeshIndex(_) + | ShaderValue::Grid(_) => None, } } } From f92d516eed0342475cf8202c26f1ae893a67f755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 28 Sep 2026 21:56:39 -0700 Subject: [PATCH 03/20] Dispatch and uniform fixes. --- crates/processing_core/src/error.rs | 2 + crates/processing_render/src/compute.rs | 12 +++++ crates/processing_render/src/lib.rs | 5 +- .../processing_render/src/material/custom.rs | 47 ++++++++++++++----- .../src/particles/algebra.rs | 12 ++--- 5 files changed, 58 insertions(+), 20 deletions(-) diff --git a/crates/processing_core/src/error.rs b/crates/processing_core/src/error.rs index e4dbdad4..59265767 100644 --- a/crates/processing_core/src/error.rs +++ b/crates/processing_core/src/error.rs @@ -58,6 +58,8 @@ pub enum ProcessingError { PipelineCompileError(String), #[error("Pipeline not ready after {0} frames")] PipelineNotReady(u32), + #[error("`{0}` isn't on the GPU yet")] + ResourceNotReady(String), #[error("Particles not found")] ParticlesNotFound, #[error("Grid not found")] diff --git a/crates/processing_render/src/compute.rs b/crates/processing_render/src/compute.rs index 9e593ed9..e11a2810 100644 --- a/crates/processing_render/src/compute.rs +++ b/crates/processing_render/src/compute.rs @@ -359,6 +359,18 @@ pub fn dispatch( .clone(); let reflection = shader.reflection(); + // a missing GPU resource drops its binding, and wgpu panics on an incomplete bind group + for param in reflection.parameters() { + let Some(name) = param.name() else { continue }; + let ready = match (shader.buffer_handle(name), shader.image_handle(name)) { + (Some(handle), _) => gpu_buffers.get(handle).is_some(), + (_, Some(handle)) => gpu_images.get(handle).is_some(), + _ => true, + }; + if !ready { + return Err(ProcessingError::ResourceNotReady(name.to_string())); + } + } let mut bind_groups = Vec::new(); for (group, desc) in &layout_descriptors { diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index d20e26d4..31b22b3d 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -2553,7 +2553,10 @@ pub(crate) fn compute_dispatch_no_update( z: u32, ) -> error::Result<()> { app_mut(|app| match dispatch_inner(app, entity, x, y, z) { - Err(error::ProcessingError::PipelineNotReady(_)) => { + Err( + error::ProcessingError::PipelineNotReady(_) + | error::ProcessingError::ResourceNotReady(_), + ) => { app.update(); dispatch_inner(app, entity, x, y, z) } diff --git a/crates/processing_render/src/material/custom.rs b/crates/processing_render/src/material/custom.rs index 7563ece1..bc9535cb 100644 --- a/crates/processing_render/src/material/custom.rs +++ b/crates/processing_render/src/material/custom.rs @@ -322,11 +322,27 @@ pub(crate) fn apply_reflect_field( return Ok(()); } - let param_name = find_param_containing_field(shader, name); - if let Some(param_name) = param_name - && let Some(param) = shader.field_mut(¶m_name) + // `struct.field` picks a struct explicitly + let (param_name, field_name) = match name.split_once('.') { + Some((param, field)) => (param.to_string(), field), + None => { + let params = params_containing_field(shader, name); + if params.len() > 1 { + return Err(ProcessingError::InvalidArgument(format!( + "`{name}` is a field of {}; name one, e.g. `{}.{name}`", + params.join(" and "), + params[0], + ))); + } + let Some(param) = params.into_iter().next() else { + return Err(ProcessingError::UnknownShaderProperty(name.to_string())); + }; + (param, name) + } + }; + if let Some(param) = shader.field_mut(¶m_name) && let ReflectMut::Struct(s) = param.reflect_mut() - && let Some(field) = s.field_mut(name) + && let Some(field) = s.field_mut(field_name) { apply_field_coerced(field, value); return Ok(()); @@ -509,15 +525,20 @@ pub(crate) fn find_param_containing_field( shader: &DynamicShader, field_name: &str, ) -> Option { - for i in 0..shader.field_len() { - if let Some(field) = shader.field_at(i) - && let ReflectRef::Struct(s) = field.reflect_ref() - && s.field(field_name).is_some() - { - return shader.name_at(i).map(|s: &str| s.to_string()); - } - } - None + params_containing_field(shader, field_name) + .into_iter() + .next() +} + +fn params_containing_field(shader: &DynamicShader, field_name: &str) -> Vec { + (0..shader.field_len()) + .filter(|&i| { + shader.field_at(i).is_some_and(|field| { + matches!(field.reflect_ref(), ReflectRef::Struct(s) if s.field(field_name).is_some()) + }) + }) + .filter_map(|i| shader.name_at(i).map(str::to_string)) + .collect() } pub struct CustomMaterialPlugin; diff --git a/crates/processing_render/src/particles/algebra.rs b/crates/processing_render/src/particles/algebra.rs index 7500040b..313ff98a 100644 --- a/crates/processing_render/src/particles/algebra.rs +++ b/crates/processing_render/src/particles/algebra.rs @@ -5,7 +5,7 @@ use bevy::prelude::Entity; use processing_core::error::{ProcessingError, Result}; use crate::shader_value::ShaderValue; -use crate::{buffer_size, compute_dispatch, compute_set, shader_create_with_features}; +use crate::{buffer_size, compute_dispatch_no_update, compute_set, shader_create_with_features}; const WG: u32 = 64; @@ -96,7 +96,7 @@ static MAP: Mutex> = Mutex::new(None); fn dispatch_particles(compute: Entity, floats: u64, components: u32) -> Result<()> { let n = (floats / components as u64) as u32; - compute_dispatch(compute, n.div_ceil(WG), 1, 1) + compute_dispatch_no_update(compute, n.div_ceil(WG), 1, 1) } fn check_components(verb: &str, components: u32) -> Result<()> { @@ -373,7 +373,7 @@ pub fn lookup( compute_set(c, "tex", ShaderValue::Texture(tex))?; compute_set(c, "samp", ShaderValue::Texture(tex))?; let n = (buffer_size(dst)? / 16) as u32; - compute_dispatch(c, n.div_ceil(WG), 1, 1) + compute_dispatch_no_update(c, n.div_ceil(WG), 1, 1) } pub const REDUCE_LENGTH: u32 = 0; @@ -430,7 +430,7 @@ pub fn reduce_components(dst: Entity, src: Entity, components: u32, op: u32) -> compute_set(c, "components", ShaderValue::UInt(components))?; compute_set(c, "op", ShaderValue::UInt(op))?; let n = (buffer_size(dst)? / 4) as u32; - compute_dispatch(c, n.div_ceil(WG), 1, 1) + compute_dispatch_no_update(c, n.div_ceil(WG), 1, 1) } const EXTRACT_SRC: &str = r#" @@ -465,7 +465,7 @@ pub fn extract(dst: Entity, src: Entity, components: u32, index: u32) -> Result< compute_set(c, "components", ShaderValue::UInt(components))?; compute_set(c, "index", ShaderValue::UInt(index))?; let n = (buffer_size(dst)? / 4) as u32; - compute_dispatch(c, n.div_ceil(WG), 1, 1) + compute_dispatch_no_update(c, n.div_ceil(WG), 1, 1) } const PACK_SRC: &str = r#" @@ -534,7 +534,7 @@ pub fn pack(dst: Entity, sources: &[Entity]) -> Result<()> { compute_set(c, "dst", ShaderValue::Buffer(dst))?; compute_set(c, "components", ShaderValue::UInt(components))?; let n = (buffer_size(sources[0])? / 4) as u32; - compute_dispatch(c, n.div_ceil(WG), 1, 1) + compute_dispatch_no_update(c, n.div_ceil(WG), 1, 1) } pub const GEN_UNIFORM: u32 = 0; From a78b3d873f505934fb3939800c19e1f9a7e1a3db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 28 Sep 2026 21:56:39 -0700 Subject: [PATCH 04/20] Normalize, limit, broadcasting. --- crates/processing_core/src/constants.rs | 2 + crates/processing_pyo3/src/constants.rs | 2 +- crates/processing_pyo3/src/particles.rs | 15 ++++++-- crates/processing_render/src/lib.rs | 6 +-- .../src/particles/algebra.rs | 37 +++++++++++++++++-- crates/processing_render/src/particles/mod.rs | 6 +-- 6 files changed, 55 insertions(+), 13 deletions(-) diff --git a/crates/processing_core/src/constants.rs b/crates/processing_core/src/constants.rs index d155448b..bb616946 100644 --- a/crates/processing_core/src/constants.rs +++ b/crates/processing_core/src/constants.rs @@ -87,6 +87,8 @@ pub const AFFINE: &str = "affine"; pub const ABS: &str = "abs"; pub const NEGATE: &str = "negate"; pub const FLOOR: &str = "floor"; +pub const NORMALIZE: &str = "normalize"; +pub const LIMIT: &str = "limit"; pub const SQRT: &str = "sqrt"; pub const GREATER: &str = "greater"; pub const LESS: &str = "less"; diff --git a/crates/processing_pyo3/src/constants.rs b/crates/processing_pyo3/src/constants.rs index 999a1904..bd1a7436 100644 --- a/crates/processing_pyo3/src/constants.rs +++ b/crates/processing_pyo3/src/constants.rs @@ -62,7 +62,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { add!( m, MAP, COMBINE, MIX, LOOKUP, REDUCE, EXTRACT, PACK, GENERATE ); - add!(m, AFFINE, ABS, NEGATE, FLOOR, SQRT); + add!(m, AFFINE, ABS, NEGATE, FLOOR, SQRT, NORMALIZE, LIMIT); add!(m, GREATER, LESS, GEQ, LEQ, EQ, NEQ); add!(m, SUB, MUL, DIV, POW); add!(m, LENGTH, SUM, SUMSQ, MEAN, MIN, MAX); diff --git a/crates/processing_pyo3/src/particles.rs b/crates/processing_pyo3/src/particles.rs index 4676c1cc..ae1497fe 100644 --- a/crates/processing_pyo3/src/particles.rs +++ b/crates/processing_pyo3/src/particles.rs @@ -25,7 +25,7 @@ use processing_render::{ use processing_render::{GEN_GAUSSIAN, GEN_SIGNED, GEN_UNIFORM}; use processing_render::{ MAP_ABS, MAP_AFFINE, MAP_CLAMP, MAP_EQ, MAP_FLOOR, MAP_GEQ, MAP_GREATER, MAP_LEQ, MAP_LESS, - MAP_NEGATE, MAP_NEQ, MAP_SQRT, MAP_SQUARE, + MAP_LIMIT, MAP_NEGATE, MAP_NEQ, MAP_NORMALIZE, MAP_SQRT, MAP_SQUARE, }; use processing_render::{ REDUCE_LENGTH, REDUCE_MAX, REDUCE_MEAN, REDUCE_MIN, REDUCE_SUM, REDUCE_SUMSQ, @@ -46,6 +46,8 @@ fn parse_map_op(s: &str) -> PyResult { _ if s.eq_ignore_ascii_case(c::LEQ) => Ok(MAP_LEQ), _ if s.eq_ignore_ascii_case(c::EQ) => Ok(MAP_EQ), _ if s.eq_ignore_ascii_case(c::NEQ) => Ok(MAP_NEQ), + _ if s.eq_ignore_ascii_case(c::NORMALIZE) => Ok(MAP_NORMALIZE), + _ if s.eq_ignore_ascii_case(c::LIMIT) => Ok(MAP_LIMIT), _ => Err(PyValueError::new_err(format!("map: unknown op {s:?}"))), } } @@ -239,6 +241,11 @@ fn map_params(kwargs: Option<&Bound<'_, PyDict>>, op: u32) -> PyResult<(f32, f32 kw_f32(kwargs, "offset", 0.0)?, ), MAP_CLAMP => (kw_f32(kwargs, "lo", 0.0)?, kw_f32(kwargs, "hi", 1.0)?), + MAP_NORMALIZE => (kw_f32(kwargs, "length", 1.0)?, 0.0), + MAP_LIMIT => ( + kw_f32(kwargs, "min_length", 0.0)?, + kw_f32(kwargs, "max_length", f32::MAX)?, + ), MAP_GREATER | MAP_LESS | MAP_GEQ | MAP_LEQ | MAP_EQ | MAP_NEQ => ( kw_f32(kwargs, "threshold", 0.0)?, kw_f32(kwargs, "epsilon", 1.0e-6)?, @@ -251,6 +258,8 @@ fn map_param_keys(op: u32) -> &'static [&'static str] { match op { MAP_AFFINE => &["scale", "offset"], MAP_CLAMP => &["lo", "hi"], + MAP_NORMALIZE => &["length"], + MAP_LIMIT => &["min_length", "max_length"], MAP_GREATER | MAP_LESS | MAP_GEQ | MAP_LEQ | MAP_EQ | MAP_NEQ => &["threshold", "epsilon"], _ => &[], } @@ -567,9 +576,9 @@ impl Particles { reject_unknown_kwargs(kwargs, &["a", "b", "out", "op", "b_scale", "b_offset"])?; let (a, comp) = self.operand(kwargs, "a")?; let (b, b_comp) = self.operand(kwargs, "b")?; - if b_comp != comp { + if b_comp != comp && b_comp != 1 { return Err(PyValueError::new_err(format!( - "apply(combine): `a` has {comp} components but `b` has {b_comp} (must match)" + "apply(combine): the first operand has {comp} components, so the second needs {comp} or 1, not {b_comp}" ))); } let out = self.dest(kwargs, a)?; diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 31b22b3d..10c65cbd 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -23,9 +23,9 @@ pub mod transform; pub use particles::algebra::{ GEN_GAUSSIAN, GEN_SIGNED, GEN_UNIFORM, MAP_ABS, MAP_AFFINE, MAP_CLAMP, MAP_EQ, MAP_FLOOR, - MAP_GEQ, MAP_GREATER, MAP_LEQ, MAP_LESS, MAP_NEGATE, MAP_NEQ, MAP_SQRT, MAP_SQUARE, - REDUCE_LENGTH, REDUCE_MAX, REDUCE_MEAN, REDUCE_MIN, REDUCE_SUM, REDUCE_SUMSQ, combine, extract, - generate, lookup, map, mix, pack, reduce_components, + MAP_GEQ, MAP_GREATER, MAP_LEQ, MAP_LESS, MAP_LIMIT, MAP_NEGATE, MAP_NEQ, MAP_NORMALIZE, + MAP_SQRT, MAP_SQUARE, REDUCE_LENGTH, REDUCE_MAX, REDUCE_MEAN, REDUCE_MIN, REDUCE_SUM, + REDUCE_SUMSQ, combine, extract, generate, lookup, map, mix, pack, reduce_components, }; pub use particles::compact::compact; pub use particles::grid::{Grid, GridParams, grid_build, grid_create, grid_destroy, grid_get}; diff --git a/crates/processing_render/src/particles/algebra.rs b/crates/processing_render/src/particles/algebra.rs index 313ff98a..f2d3829f 100644 --- a/crates/processing_render/src/particles/algebra.rs +++ b/crates/processing_render/src/particles/algebra.rs @@ -22,6 +22,8 @@ pub const MAP_GEQ: u32 = 9; pub const MAP_LEQ: u32 = 10; pub const MAP_EQ: u32 = 11; pub const MAP_NEQ: u32 = 12; +pub const MAP_NORMALIZE: u32 = 13; +pub const MAP_LIMIT: u32 = 14; const MAP_SRC: &str = r#" struct Params { @@ -60,10 +62,25 @@ fn main(@builtin(global_invocation_id) gid: vec3) { let i = gid.x; let n = arrayLength(&a) / params.components; if i >= n { return; } + // normalize and limit scale the whole vector by its length + var len2 = 0.0; + for (var c = 0u; c < params.components; c = c + 1u) { + let x = a[i * params.components + c]; + len2 += x * x; + } + let len = sqrt(len2); + var k = 1.0; + if params.op == 13u && len > 0.0 { + k = params.p0 / len; + } else if params.op == 14u && len > 0.0 { + k = clamp(len, params.p0, params.p1) / len; + } for (var c = 0u; c < params.components; c = c + 1u) { let idx = i * params.components + c; - @if(in_place) { a[idx] = apply_op(a[idx]); } - @if(!in_place) { dst[idx] = apply_op(a[idx]); } + var r = a[idx] * k; + if params.op < 13u { r = apply_op(a[idx]); } + @if(in_place) { a[idx] = r; } + @if(!in_place) { dst[idx] = r; } } } "#; @@ -149,6 +166,8 @@ struct Params { op: u32, b_scale: f32, b_offset: f32, + // 1 = one `b` per particle, applied to every component + b_components: u32, } @if(in_place) @group(0) @binding(0) var a: array; @@ -177,7 +196,8 @@ fn main(@builtin(global_invocation_id) gid: vec3) { if i >= n { return; } for (var c = 0u; c < params.components; c = c + 1u) { let idx = i * params.components + c; - let r = combine_op(a[idx], b[idx] * params.b_scale + params.b_offset); + let b_idx = select(idx, i, params.b_components == 1u); + let r = combine_op(a[idx], b[b_idx] * params.b_scale + params.b_offset); @if(in_place) { a[idx] = r; } @if(!in_place) { dst[idx] = r; } } @@ -198,6 +218,16 @@ pub fn combine( check_components("combine", components)?; let v = variants(&COMBINE, COMBINE_SRC)?; let floats = buffer_size(a)? / 4; + let b_floats = buffer_size(b)? / 4; + let b_components = if b_floats == floats { + components + } else if b_floats == floats / components as u64 { + 1 + } else { + return Err(ProcessingError::InvalidArgument(format!( + "combine: `b` needs {components} or 1 components per particle" + ))); + }; let c = if dst == a { ensure_no_alias("combine", a, &[b])?; @@ -216,6 +246,7 @@ pub fn combine( compute_set(c, "op", ShaderValue::UInt(op))?; compute_set(c, "b_scale", ShaderValue::Float(b_scale))?; compute_set(c, "b_offset", ShaderValue::Float(b_offset))?; + compute_set(c, "b_components", ShaderValue::UInt(b_components))?; dispatch_particles(c, floats, components) } diff --git a/crates/processing_render/src/particles/mod.rs b/crates/processing_render/src/particles/mod.rs index 5088726d..ddc33a17 100644 --- a/crates/processing_render/src/particles/mod.rs +++ b/crates/processing_render/src/particles/mod.rs @@ -15,9 +15,9 @@ pub mod sort; pub use algebra::{ GEN_GAUSSIAN, GEN_SIGNED, GEN_UNIFORM, MAP_ABS, MAP_AFFINE, MAP_CLAMP, MAP_EQ, MAP_FLOOR, - MAP_GEQ, MAP_GREATER, MAP_LEQ, MAP_LESS, MAP_NEGATE, MAP_NEQ, MAP_SQRT, MAP_SQUARE, - REDUCE_LENGTH, REDUCE_MAX, REDUCE_MEAN, REDUCE_MIN, REDUCE_SUM, REDUCE_SUMSQ, combine, extract, - generate, lookup, map, mix, pack, reduce_components, + MAP_GEQ, MAP_GREATER, MAP_LEQ, MAP_LESS, MAP_LIMIT, MAP_NEGATE, MAP_NEQ, MAP_NORMALIZE, + MAP_SQRT, MAP_SQUARE, REDUCE_LENGTH, REDUCE_MAX, REDUCE_MEAN, REDUCE_MIN, REDUCE_SUM, + REDUCE_SUMSQ, combine, extract, generate, lookup, map, mix, pack, reduce_components, }; pub use compact::compact; pub use emit::{ From b97cee5e5bad4878be73bbc8f98e0ba230b0fef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 28 Sep 2026 21:56:39 -0700 Subject: [PATCH 05/20] Python particle API. --- .../processing_pyo3/examples/flocking_duck.py | 2 +- .../processing_pyo3/examples/flocking_gpu.py | 2 +- .../examples/particles_animated.py | 2 +- .../examples/particles_basic.py | 2 +- .../examples/particles_density.py | 2 +- .../examples/particles_emit.py | 2 +- .../examples/particles_emit_gpu.py | 2 +- .../examples/particles_from_mesh.py | 2 +- .../examples/particles_gpu_surface.py | 2 +- .../examples/particles_gpu_surface_lit.py | 2 +- .../examples/particles_lifecycle.py | 2 +- .../examples/particles_lines.py | 2 +- .../examples/particles_lissajous.py | 4 +- .../examples/particles_noise.py | 2 +- .../examples/particles_plexus.py | 2 +- .../examples/particles_points.py | 2 +- .../examples/particles_scatter_volume.py | 4 +- .../examples/particles_sphere.py | 4 +- .../examples/particles_stress.py | 2 +- .../examples/particles_surface.py | 2 +- crates/processing_pyo3/src/graphics.rs | 3 +- crates/processing_pyo3/src/lib.rs | 13 +- crates/processing_pyo3/src/particles.rs | 228 +++++++++++++----- .../src/geometry/attribute.rs | 13 + crates/processing_render/src/lib.rs | 10 + 25 files changed, 217 insertions(+), 96 deletions(-) diff --git a/crates/processing_pyo3/examples/flocking_duck.py b/crates/processing_pyo3/examples/flocking_duck.py index 3af396ab..e60ff4f4 100644 --- a/crates/processing_pyo3/examples/flocking_duck.py +++ b/crates/processing_pyo3/examples/flocking_duck.py @@ -62,7 +62,7 @@ def setup(): duck = gltf.geometry("LOD3spShape") p = create_particles( - geometry=duck, + duck, attributes=[ Attribute.position(), Attribute.rotation(), diff --git a/crates/processing_pyo3/examples/flocking_gpu.py b/crates/processing_pyo3/examples/flocking_gpu.py index d3119830..ab8ec579 100644 --- a/crates/processing_pyo3/examples/flocking_gpu.py +++ b/crates/processing_pyo3/examples/flocking_gpu.py @@ -61,7 +61,7 @@ def setup(): directional_light((0.95, 0.9, 0.85), 800.0) p = create_particles( - capacity=BOID_COUNT, + BOID_COUNT, attributes=[ Attribute.position(), Attribute.rotation(), diff --git a/crates/processing_pyo3/examples/particles_animated.py b/crates/processing_pyo3/examples/particles_animated.py index b80c9335..035df999 100644 --- a/crates/processing_pyo3/examples/particles_animated.py +++ b/crates/processing_pyo3/examples/particles_animated.py @@ -23,7 +23,7 @@ def setup(): for z in range(10): positions.extend([x - 4.5, y - 4.5, z - 4.5]) - p = create_particles(capacity=capacity, attributes=[Attribute.position()]) + p = create_particles(capacity, attributes=[Attribute.position()]) pos_buf = p.buffer("position") pos_buf.write(positions) diff --git a/crates/processing_pyo3/examples/particles_basic.py b/crates/processing_pyo3/examples/particles_basic.py index ae897b85..d94b64ec 100644 --- a/crates/processing_pyo3/examples/particles_basic.py +++ b/crates/processing_pyo3/examples/particles_basic.py @@ -15,7 +15,7 @@ def setup(): source = Geometry.sphere(5.0, 32, 24) p = create_particles( - geometry=source, + source, attributes=[Attribute.position(), Attribute.uv(), Attribute.color()], ) diff --git a/crates/processing_pyo3/examples/particles_density.py b/crates/processing_pyo3/examples/particles_density.py index 1bbe6463..f98af0d6 100644 --- a/crates/processing_pyo3/examples/particles_density.py +++ b/crates/processing_pyo3/examples/particles_density.py @@ -18,7 +18,7 @@ def setup(): mode_3d() p = create_particles( - capacity=N, + N, attributes=[ Attribute.position(), Attribute.color(), diff --git a/crates/processing_pyo3/examples/particles_emit.py b/crates/processing_pyo3/examples/particles_emit.py index 4a3db9dc..7f868b85 100644 --- a/crates/processing_pyo3/examples/particles_emit.py +++ b/crates/processing_pyo3/examples/particles_emit.py @@ -17,7 +17,7 @@ def setup(): capacity = 2000 p = create_particles( - capacity=capacity, + capacity, attributes=[Attribute.position(), Attribute.color()], ) diff --git a/crates/processing_pyo3/examples/particles_emit_gpu.py b/crates/processing_pyo3/examples/particles_emit_gpu.py index 22093ad3..d32eb7e8 100644 --- a/crates/processing_pyo3/examples/particles_emit_gpu.py +++ b/crates/processing_pyo3/examples/particles_emit_gpu.py @@ -29,7 +29,7 @@ def setup(): # so declare them (all built-in here). `velocity`/`age` are built-ins now, so # no custom `Attribute(...)` is needed. p = create_particles( - capacity=CAPACITY, + CAPACITY, attributes=[ Attribute.position(), Attribute.velocity(), diff --git a/crates/processing_pyo3/examples/particles_from_mesh.py b/crates/processing_pyo3/examples/particles_from_mesh.py index f7e5d64a..d577f41e 100644 --- a/crates/processing_pyo3/examples/particles_from_mesh.py +++ b/crates/processing_pyo3/examples/particles_from_mesh.py @@ -15,7 +15,7 @@ def setup(): source = Geometry.sphere(5.0, 32, 24) p = create_particles( - geometry=source, + source, attributes=[Attribute.position(), Attribute.uv(), Attribute.color()], ) diff --git a/crates/processing_pyo3/examples/particles_gpu_surface.py b/crates/processing_pyo3/examples/particles_gpu_surface.py index e0e4af69..4d77b967 100644 --- a/crates/processing_pyo3/examples/particles_gpu_surface.py +++ b/crates/processing_pyo3/examples/particles_gpu_surface.py @@ -14,7 +14,7 @@ def setup(): window_title("gpu index") mode_3d() - p = create_particles(capacity=NX * NY, attributes=[Attribute.position()]) + p = create_particles(NX * NY, attributes=[Attribute.position()]) idx = p.index_buffer((NX - 1) * (NY - 1) * 6) gen = create_compute(load_shader("shaders/gen_surface.wesl")) gen.set(indices=idx) diff --git a/crates/processing_pyo3/examples/particles_gpu_surface_lit.py b/crates/processing_pyo3/examples/particles_gpu_surface_lit.py index d737d1bd..a1db5871 100644 --- a/crates/processing_pyo3/examples/particles_gpu_surface_lit.py +++ b/crates/processing_pyo3/examples/particles_gpu_surface_lit.py @@ -15,7 +15,7 @@ def setup(): mode_3d() p = create_particles( - capacity=NX * NY, + NX * NY, attributes=[Attribute.position(), Attribute.color(), Attribute.normal()], ) idx = p.index_buffer((NX - 1) * (NY - 1) * 6) diff --git a/crates/processing_pyo3/examples/particles_lifecycle.py b/crates/processing_pyo3/examples/particles_lifecycle.py index d6ecdd2e..8301fd81 100644 --- a/crates/processing_pyo3/examples/particles_lifecycle.py +++ b/crates/processing_pyo3/examples/particles_lifecycle.py @@ -22,7 +22,7 @@ def setup(): capacity = 800 p = create_particles( - capacity=capacity, + capacity, attributes=[ Attribute.position(), Attribute.color(), diff --git a/crates/processing_pyo3/examples/particles_lines.py b/crates/processing_pyo3/examples/particles_lines.py index 3137ae11..20083fa2 100644 --- a/crates/processing_pyo3/examples/particles_lines.py +++ b/crates/processing_pyo3/examples/particles_lines.py @@ -23,7 +23,7 @@ def setup(): mode_3d() p = create_particles( - capacity=N, + N, attributes=[Attribute.position()], ) p.buffer("position").write([list(knot(i / N * tau, 0.0)) for i in range(N)]) diff --git a/crates/processing_pyo3/examples/particles_lissajous.py b/crates/processing_pyo3/examples/particles_lissajous.py index 886c3408..190bee92 100644 --- a/crates/processing_pyo3/examples/particles_lissajous.py +++ b/crates/processing_pyo3/examples/particles_lissajous.py @@ -34,10 +34,10 @@ def setup(): window_title(f"Lissajous — all points connected — {N:,} pts") mode_3d() - p = create_particles(capacity=N, attributes=[Attribute.position(), Attribute.color()]) + p = create_particles(N, attributes=[Attribute.position(), Attribute.color()]) # One line = 2 vertices; up to N*MAX_LINKS lines. edges = create_particles( - capacity=N * MAX_LINKS * 2, attributes=[Attribute.position(), Attribute.color()] + N * MAX_LINKS * 2, attributes=[Attribute.position(), Attribute.color()] ) idx = edges.index_buffer(N * MAX_LINKS * 2) # link fills indices + the dynamic count diff --git a/crates/processing_pyo3/examples/particles_noise.py b/crates/processing_pyo3/examples/particles_noise.py index ba2aa993..1d558e98 100644 --- a/crates/processing_pyo3/examples/particles_noise.py +++ b/crates/processing_pyo3/examples/particles_noise.py @@ -16,7 +16,7 @@ def setup(): source = Geometry.sphere(5.0, 32, 24) p = create_particles( - geometry=source, + source, attributes=[Attribute.position(), Attribute.uv(), Attribute.color()], ) diff --git a/crates/processing_pyo3/examples/particles_plexus.py b/crates/processing_pyo3/examples/particles_plexus.py index 027d3712..5c740427 100644 --- a/crates/processing_pyo3/examples/particles_plexus.py +++ b/crates/processing_pyo3/examples/particles_plexus.py @@ -25,7 +25,7 @@ def setup(): mode_3d() p = create_particles( - capacity=N, + N, attributes=[Attribute.position(), Attribute.velocity(), Attribute.color()], ) diff --git a/crates/processing_pyo3/examples/particles_points.py b/crates/processing_pyo3/examples/particles_points.py index 2c745d27..f1b0befe 100644 --- a/crates/processing_pyo3/examples/particles_points.py +++ b/crates/processing_pyo3/examples/particles_points.py @@ -15,7 +15,7 @@ def setup(): mode_3d() p = create_particles( - capacity=COUNT, + COUNT, attributes=[Attribute.position()], ) diff --git a/crates/processing_pyo3/examples/particles_scatter_volume.py b/crates/processing_pyo3/examples/particles_scatter_volume.py index af3d251d..4ff5ad93 100644 --- a/crates/processing_pyo3/examples/particles_scatter_volume.py +++ b/crates/processing_pyo3/examples/particles_scatter_volume.py @@ -26,7 +26,7 @@ def setup(): particle = Geometry.sphere(0.15, 4, 3) p = create_particles( - capacity=CAPACITY, + CAPACITY, attributes=[ Attribute.position(), Attribute.scale(), @@ -45,7 +45,7 @@ def draw(): seed = (int(elapsed_time * 1000.0) ^ 0xC0FFEE) & 0xFFFFFFFF scatter.set(seed=seed) p.emit_gpu(BURST, scatter) - p.apply(MAP, a="scale", op=AFFINE, scale=0.985, offset=0.0) + p.apply(MAP, "scale", op=AFFINE, scale=0.985, offset=0.0) run() diff --git a/crates/processing_pyo3/examples/particles_sphere.py b/crates/processing_pyo3/examples/particles_sphere.py index e8bd6927..c515171e 100644 --- a/crates/processing_pyo3/examples/particles_sphere.py +++ b/crates/processing_pyo3/examples/particles_sphere.py @@ -16,13 +16,13 @@ def setup(): rest = Attribute("rest", AttributeFormat.Float3) sphere = Geometry.sphere(1.5, 96, 64) p = create_particles( - geometry=sphere, + sphere, attributes=[ Attribute.position(), rest, ], ) - p.apply(MAP, a=Attribute.position(), out=rest, op=AFFINE, scale=1.0, offset=0.0) + p.apply(MAP, Attribute.position(), out=rest, op=AFFINE, scale=1.0, offset=0.0) def draw(): diff --git a/crates/processing_pyo3/examples/particles_stress.py b/crates/processing_pyo3/examples/particles_stress.py index b86a3ec4..68d7a8ff 100644 --- a/crates/processing_pyo3/examples/particles_stress.py +++ b/crates/processing_pyo3/examples/particles_stress.py @@ -25,7 +25,7 @@ def setup(): directional_light((0.0, 0.0, 1.0), 1000.0, position=Vec3.Z, look_at=Vec3.ZERO) p = create_particles( - geometry=Geometry.grid(GRID, GRID, GRID, SPACING), + Geometry.grid(GRID, GRID, GRID, SPACING), attributes=[Attribute.position(), Attribute.uv(), Attribute.color()], ) diff --git a/crates/processing_pyo3/examples/particles_surface.py b/crates/processing_pyo3/examples/particles_surface.py index 07afe584..7a33cec0 100644 --- a/crates/processing_pyo3/examples/particles_surface.py +++ b/crates/processing_pyo3/examples/particles_surface.py @@ -30,7 +30,7 @@ def setup(): window_title(f"Particle surface — {N:,}-vertex Mobius strip") mode_3d() - p = create_particles(capacity=N, attributes=[Attribute.position()]) + p = create_particles(N, attributes=[Attribute.position()]) def draw(): diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index e4474170..591fd911 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -800,8 +800,7 @@ impl Geometry { Ok(Self { entity }) } - /// lattice centered at the origin; topology is `POINTS`, intended as a - /// position source for `Particles(geometry=...)` rather than rasterized. + /// A point lattice centered at the origin, to seed `create_particles(geometry)`. #[staticmethod] #[pyo3(signature = (nx, ny, nz, spacing=1.0))] pub fn grid(nx: u32, ny: u32, nz: u32, spacing: f32) -> PyResult { diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 97deda89..01012978 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -1626,18 +1626,15 @@ pub mod mewnala { Ok(window) } - /// Creates a GPU particle system (Processing `createParticles`). `attributes` - /// defaults to `position`; other built-in attributes (`velocity`, `color`, - /// `scale`, `life`, `age`, ...) and declared custom ones materialize on - /// demand when you call `buffer("name")`. + /// Creates a GPU particle system (Processing `createParticles`) from a count or a `Geometry`. + /// Attributes are created on first use. #[pyfunction] - #[pyo3(signature = (capacity=None, attributes=None, geometry=None))] + #[pyo3(signature = (source, attributes=None))] fn create_particles( - capacity: Option, + source: &Bound<'_, PyAny>, attributes: Option>>, - geometry: Option<&Geometry>, ) -> PyResult { - super::particles::Particles::create(capacity, attributes, geometry) + super::particles::Particles::create(source, attributes) } /// Creates a compute pass from a shader (Processing-style `createCompute`). diff --git a/crates/processing_pyo3/src/particles.rs b/crates/processing_pyo3/src/particles.rs index ae1497fe..eeb54e57 100644 --- a/crates/processing_pyo3/src/particles.rs +++ b/crates/processing_pyo3/src/particles.rs @@ -1,7 +1,7 @@ use bevy::prelude::Entity; use processing::prelude::*; use processing_render::geometry; -use pyo3::types::PyDict; +use pyo3::types::{PyDict, PyTuple}; use pyo3::{ exceptions::{PyRuntimeError, PyTypeError, PyValueError}, prelude::*, @@ -265,6 +265,54 @@ fn map_param_keys(op: u32) -> &'static [&'static str] { } } +fn operand_names(verb: &str) -> &'static [&'static str] { + let is = |c: &str| verb.eq_ignore_ascii_case(c); + if is(c::MAP) || is(c::REDUCE) || is(c::EXTRACT) || is(c::LOOKUP) || is(c::NEIGHBOR) { + &["a"] + } else if is(c::COMBINE) { + &["a", "b"] + } else if is(c::MIX) { + &["a", "b", "t"] + } else { + &[] + } +} + +fn with_operands<'py>( + verb: &str, + operands: &Bound<'py, PyTuple>, + kwargs: Option<&Bound<'py, PyDict>>, +) -> PyResult>> { + let py = operands.py(); + let merged = match kwargs { + Some(k) => k.copy()?, + None => PyDict::new(py), + }; + let packs = verb.eq_ignore_ascii_case(c::PACK); + let names = operand_names(verb); + for name in names.iter().chain(packs.then_some(&"sources")) { + if merged.contains(*name)? { + return Err(PyTypeError::new_err(format!( + "apply({verb}): pass operands positionally, not as `{name}=`" + ))); + } + } + if packs { + merged.set_item("sources", operands.to_list())?; + } else if operands.len() > names.len() { + return Err(PyTypeError::new_err(format!( + "apply({verb}): takes at most {} operand(s), got {}", + names.len(), + operands.len() + ))); + } else { + for (name, operand) in names.iter().zip(operands.iter()) { + merged.set_item(*name, operand)?; + } + } + Ok(Some(merged)) +} + fn reject_unknown_kwargs(kwargs: Option<&Bound<'_, PyDict>>, valid: &[&str]) -> PyResult<()> { let Some(kwargs) = kwargs else { return Ok(()); @@ -455,9 +503,8 @@ impl Particles { }) } - /// Resolve a `buffer()` argument (an attribute name string or an `Attribute`) - /// to its attribute entity. Built-in names map to their factory; other names - /// must have been declared as custom attributes at construction. + /// Resolve an attribute name or `Attribute` to its entity. Unknown names must have been + /// written first, e.g. via `out=`. fn resolve_attribute(&self, attribute: &Bound<'_, PyAny>) -> PyResult { if let Ok(attr) = attribute.extract::() { return Ok(attr.entity); @@ -469,8 +516,13 @@ impl Particles { if let Some((entity, _)) = self.name_to_attr.get(&name) { return Ok(*entity); } + if let Some(entity) = geometry_attribute_find(&name) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))? + { + return Ok(entity); + } return Err(PyValueError::new_err(format!( - "\"{name}\" is not a built-in attribute; pass its Attribute to buffer()" + "no attribute \"{name}\" yet; write to it first, e.g. out=\"{name}\"" ))); } Err(PyTypeError::new_err( @@ -503,24 +555,66 @@ impl Particles { } fn operand(&self, kwargs: Option<&Bound<'_, PyDict>>, key: &str) -> PyResult<(Entity, u32)> { - let val = kw(kwargs, key) - .ok_or_else(|| PyRuntimeError::new_err(format!("apply(): missing operand '{key}'")))?; + let val = kw(kwargs, key).ok_or_else(|| { + PyTypeError::new_err(match key { + "out" => "apply(): missing `out=`", + _ => "apply(): missing an operand", + }) + })?; self.resolve_operand(&val) } - fn dest(&self, kwargs: Option<&Bound<'_, PyDict>>, in_place: Entity) -> PyResult { + /// Resolve `out=`, creating it with `components` if new. An existing `out` must match. + fn output(&self, kwargs: Option<&Bound<'_, PyDict>>, components: u32) -> PyResult { + let rt = |e: error::ProcessingError| PyRuntimeError::new_err(format!("{e}")); + let val = + kw(kwargs, "out").ok_or_else(|| PyTypeError::new_err("apply(): missing `out=`"))?; + if let Ok(name) = val.extract::() + && Self::builtin_attribute(&name).is_none() + && !self.name_to_attr.contains_key(&name) + && geometry_attribute_find(&name).map_err(rt)?.is_none() + { + use processing_render::geometry::AttributeFormat as F; + let format = match components { + 1 => F::Float, + 2 => F::Float2, + 3 => F::Float3, + 4 => F::Float4, + n => { + return Err(PyValueError::new_err(format!( + "apply(): can't create \"{name}\" with {n} components" + ))); + } + }; + let attr = geometry_attribute_create(name, format).map_err(rt)?; + return particles_ensure_attribute(self.entity, attr).map_err(rt); + } + let (out, comp) = self.resolve_operand(&val)?; + if comp != components { + return Err(PyValueError::new_err(format!( + "apply(): `out` has {comp} components but this writes {components}" + ))); + } + Ok(out) + } + + /// `out=` if given, otherwise in place. + fn dest( + &self, + kwargs: Option<&Bound<'_, PyDict>>, + in_place: Entity, + components: u32, + ) -> PyResult { match kw(kwargs, "out") { - Some(v) => Ok(self.resolve_operand(&v)?.0), + Some(_) => self.output(kwargs, components), None => Ok(in_place), } } - /// Build a particle system (backs `create_particles`). Attributes default to - /// `position`; the rest (built-in or declared custom) materialize on demand. + /// Build a particle system (backs `create_particles`) from a count or a `Geometry`. pub(crate) fn create( - capacity: Option, + source: &Bound<'_, PyAny>, attributes: Option>>, - geometry: Option<&Geometry>, ) -> PyResult { let attrs: Vec = match attributes { Some(list) => list.iter().map(|a| (**a).clone()).collect(), @@ -528,22 +622,16 @@ impl Particles { }; let attr_entities: Vec = attrs.iter().map(|a| a.entity).collect(); - let entity = match (capacity, geometry) { - (Some(cap), None) => particles_create(cap, attr_entities) - .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?, - (None, Some(g)) => particles_create_from_geometry(g.entity, attr_entities) - .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?, - (None, None) => { - return Err(PyRuntimeError::new_err( - "create_particles() requires either capacity or geometry", - )); - } - (Some(_), Some(_)) => { - return Err(PyRuntimeError::new_err( - "create_particles() accepts capacity or geometry, not both", - )); - } - }; + let entity = if let Ok(geometry) = source.extract::>() { + particles_create_from_geometry(geometry.entity, attr_entities) + } else if let Ok(capacity) = source.extract::() { + particles_create(capacity, attr_entities) + } else { + return Err(PyTypeError::new_err( + "create_particles(): pass a particle count or a Geometry", + )); + } + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; Ok(Self { entity, @@ -565,7 +653,7 @@ impl Particles { if name.eq_ignore_ascii_case(c::MAP) { let (a, comp) = self.operand(kwargs, "a")?; - let out = self.dest(kwargs, a)?; + let out = self.dest(kwargs, a, comp)?; let op = kw_op(kwargs, MAP_AFFINE, parse_map_op)?; let mut valid = vec!["a", "out", "op"]; valid.extend_from_slice(map_param_keys(op)); @@ -581,7 +669,7 @@ impl Particles { "apply(combine): the first operand has {comp} components, so the second needs {comp} or 1, not {b_comp}" ))); } - let out = self.dest(kwargs, a)?; + let out = self.dest(kwargs, a, comp)?; let op = kw_op(kwargs, COMBINE_ADD, parse_combine_op)?; let b_scale = kw_f32(kwargs, "b_scale", 1.0)?; let b_offset = kw_f32(kwargs, "b_offset", 0.0)?; @@ -596,15 +684,15 @@ impl Particles { let (t, t_comp) = self.operand(kwargs, "t")?; if b_comp != comp { return Err(PyValueError::new_err(format!( - "apply(mix): `a` has {comp} components but `b` has {b_comp} (must match)" + "apply(mix): the first operand has {comp} components but the second has {b_comp}" ))); } if t_comp != 1 { return Err(PyValueError::new_err(format!( - "apply(mix): `t` must be a per-particle scalar (1 component), got {t_comp}" + "apply(mix): the third operand must be one value per particle, not {t_comp} components" ))); } - let out = self.dest(kwargs, a)?; + let out = self.dest(kwargs, a, comp)?; let t_scale = kw_f32(kwargs, "t_scale", 1.0)?; let t_offset = kw_f32(kwargs, "t_offset", 0.0)?; let t_clamp = kw_bool(kwargs, "t_clamp", true)?; @@ -624,7 +712,7 @@ impl Particles { ], )?; let (a, in_comp) = self.operand(kwargs, "a")?; - let out = self.operand(kwargs, "out")?.0; + let out = self.output(kwargs, 4)?; let tex = kw(kwargs, "tex") .ok_or_else(|| PyRuntimeError::new_err("apply(lookup): missing 'tex' Image"))? .extract::>() @@ -650,29 +738,47 @@ impl Particles { } else if name.eq_ignore_ascii_case(c::REDUCE) { reject_unknown_kwargs(kwargs, &["a", "out", "op"])?; let (a, comp) = self.operand(kwargs, "a")?; - let out = self.operand(kwargs, "out")?.0; + let out = self.output(kwargs, 1)?; let op = kw_op(kwargs, REDUCE_LENGTH, parse_reduce_op)?; algebra_reduce(out, a, comp, op).map_err(rt) } else if name.eq_ignore_ascii_case(c::EXTRACT) { reject_unknown_kwargs(kwargs, &["a", "out", "index"])?; let (a, comp) = self.operand(kwargs, "a")?; - let out = self.operand(kwargs, "out")?.0; + let out = self.output(kwargs, 1)?; let index = kw_u32(kwargs, "index", 0)?; algebra_extract(out, a, comp, index).map_err(rt) } else if name.eq_ignore_ascii_case(c::PACK) { reject_unknown_kwargs(kwargs, &["out", "sources"])?; - let out = self.operand(kwargs, "out")?.0; let sources = kw(kwargs, "sources") .ok_or_else(|| PyRuntimeError::new_err("apply(pack): missing 'sources' list"))?; let items: Vec> = sources.extract()?; + let out = self.output(kwargs, items.len() as u32)?; let mut entities = Vec::with_capacity(items.len()); for item in &items { entities.push(self.resolve_operand(item)?.0); } algebra_pack(out, &entities).map_err(rt) } else if name.eq_ignore_ascii_case(c::GENERATE) { - reject_unknown_kwargs(kwargs, &["out", "mode", "seed", "scale", "offset"])?; - let (out, comp) = self.operand(kwargs, "out")?; + reject_unknown_kwargs( + kwargs, + &["out", "mode", "seed", "scale", "offset", "components"], + )?; + // nothing to infer from, so a new `out` needs `components=` + let (out, comp) = match kw(kwargs, "components") { + Some(c) => { + let comp = c.extract::()?; + (self.output(kwargs, comp)?, comp) + } + None => match kw(kwargs, "out").map(|v| self.resolve_operand(&v)) { + Some(Ok(resolved)) => resolved, + Some(Err(_)) => { + return Err(PyValueError::new_err( + "apply(generate): pass components= to create a new `out`", + )); + } + None => return Err(PyTypeError::new_err("apply(): missing `out=`")), + }, + }; let mode = match kw(kwargs, "mode") { Some(v) => parse_generate_mode(&v.extract::()?)?, None => GEN_UNIFORM, @@ -695,13 +801,7 @@ impl Particles { let cell = grid.cell_size()?; let radius = kw_f32(kwargs, "radius", cell)?.min(cell); - let (out, out_comp) = self.operand(kwargs, "out")?; let (a, components) = if op == NEIGHBOR_COUNT { - if out_comp != 1 { - return Err(PyValueError::new_err( - "apply(neighbor, op=count/density): `out` must be a scalar (1 component)", - )); - } let a = match kw(kwargs, "a") { Some(_) => self.operand(kwargs, "a")?.0, None => { @@ -713,14 +813,9 @@ impl Particles { }; (a, 1u32) } else { - let (a, in_comp) = self.operand(kwargs, "a")?; - if in_comp != out_comp { - return Err(PyValueError::new_err(format!( - "apply(neighbor): source has {in_comp} components but out has {out_comp}" - ))); - } - (a, in_comp) + self.operand(kwargs, "a")? }; + let out = self.output(kwargs, components)?; particles_gather( self.entity, grid.entity, @@ -772,8 +867,8 @@ impl Particles { /// The GPU buffer for an attribute, materialized on demand. `attribute` is a /// built-in name (`"position"`, `"velocity"`, `"color"`, `"scale"`, `"life"`, - /// `"age"`, `"normal"`, `"uv"`, `"rotation"`), a declared custom attribute's - /// name, or an `Attribute`. + /// `"age"`, `"normal"`, `"uv"`, `"rotation"`), the name of any created + /// attribute, or an `Attribute`. pub fn buffer(&self, attribute: &Bound<'_, PyAny>) -> PyResult { let attr_entity = self.resolve_attribute(attribute)?; let buf = particles_ensure_attribute(self.entity, attr_entity) @@ -811,13 +906,21 @@ impl Particles { particles_reset_indices(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } - #[pyo3(signature = (kind, **kwargs))] + /// Run an operation: operands positionally, `out=` and options as keywords, e.g. + /// `p.apply(COMBINE, "velocity", "force", op="add")`. + #[pyo3(signature = (kind, *operands, **kwargs))] pub fn apply( &self, kind: &Bound<'_, PyAny>, + operands: &Bound<'_, PyTuple>, kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult<()> { if let Ok(compute) = kind.extract::>() { + if !operands.is_empty() { + return Err(PyTypeError::new_err( + "apply(compute): pass kernel parameters as keywords", + )); + } if let Some(kwargs) = kwargs { compute.set(Some(kwargs))?; } @@ -829,7 +932,8 @@ impl Particles { "apply(): first argument must be an operation constant or a Compute", ) })?; - self.apply_named(&name, kwargs) + let kwargs = with_operands(&name, operands, kwargs)?; + self.apply_named(&name, kwargs.as_ref()) } #[pyo3(signature = (n, **kwargs))] @@ -841,12 +945,10 @@ impl Particles { let mut data: Vec<(Entity, Vec)> = Vec::new(); for (key, value) in kwargs.iter() { let name: String = key.extract()?; - let (attr_entity, fmt) = self.name_to_attr.get(&name).copied().ok_or_else(|| { - PyRuntimeError::new_err(format!( - "no attribute named '{name}' (registered: {:?})", - self.name_to_attr.keys().collect::>() - )) - })?; + let attr_entity = self.resolve_attribute(&key)?; + let (_, fmt) = geometry_attribute_info(attr_entity) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + let fmt = AttributeFormat::from_inner(fmt); let floats: Vec = value.extract()?; let expected = (n as usize) * fmt.float_count(); if floats.len() != expected { diff --git a/crates/processing_render/src/geometry/attribute.rs b/crates/processing_render/src/geometry/attribute.rs index ca5f7764..4c674ef8 100644 --- a/crates/processing_render/src/geometry/attribute.rs +++ b/crates/processing_render/src/geometry/attribute.rs @@ -346,6 +346,19 @@ pub fn create( Ok(entity) } +pub fn find( + In(name): In, + builtins: Res, + registry: Res, +) -> Option { + builtins.by_name(&name).or_else(|| { + registry + .by_name + .get(&hash_attr_name(&name)) + .map(|(entity, _)| *entity) + }) +} + pub fn destroy(In(entity): In, mut commands: Commands) -> Result<()> { commands.entity(entity).despawn(); Ok(()) diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 10c65cbd..499f3a38 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -1462,6 +1462,16 @@ pub fn geometry_attribute_create( }) } +/// The attribute named `name`, if one has been created. +pub fn geometry_attribute_find(name: impl Into) -> error::Result> { + app_mut(|app| { + Ok(app + .world_mut() + .run_system_cached_with(geometry::attribute::find, name.into()) + .unwrap()) + }) +} + pub fn geometry_attribute_position() -> Entity { app_mut(|app| { Ok(app From b324ea19a76f8a0e07a61df16885959b9d2fdfc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 28 Sep 2026 21:56:39 -0700 Subject: [PATCH 06/20] Neighbor lists. --- crates/processing_core/src/constants.rs | 2 + crates/processing_ffi/src/lib.rs | 19 +++ crates/processing_pyo3/src/constants.rs | 12 +- crates/processing_pyo3/src/particles.rs | 77 +++++++--- .../shaders/processing/particles.wesl | 1 + crates/processing_render/src/lib.rs | 21 +-- .../processing_render/src/particles/emit.rs | 6 +- .../src/particles/kernels/mod.rs | 3 + .../particles/kernels/neighbors_build.wgsl | 76 ++++++++++ .../particles/kernels/neighbors_reduce.wgsl | 73 ++++++++++ crates/processing_render/src/particles/mod.rs | 34 ++++- .../src/particles/neighbors.rs | 136 ++++++++++++++++++ 12 files changed, 424 insertions(+), 36 deletions(-) create mode 100644 crates/processing_render/src/particles/kernels/neighbors_build.wgsl create mode 100644 crates/processing_render/src/particles/kernels/neighbors_reduce.wgsl create mode 100644 crates/processing_render/src/particles/neighbors.rs diff --git a/crates/processing_core/src/constants.rs b/crates/processing_core/src/constants.rs index bb616946..3fb7ba0c 100644 --- a/crates/processing_core/src/constants.rs +++ b/crates/processing_core/src/constants.rs @@ -73,6 +73,7 @@ pub const EXTRACT: &str = "extract"; pub const PACK: &str = "pack"; pub const GENERATE: &str = "generate"; pub const NEIGHBOR: &str = "neighbor"; +pub const FIND_NEIGHBORS: &str = "find_neighbors"; pub const COUNT: &str = "count"; pub const DENSITY: &str = "density"; @@ -82,6 +83,7 @@ pub const SMOOTHSTEP: &str = "smoothstep"; pub const QUADRATIC: &str = "quadratic"; pub const CUBIC: &str = "cubic"; pub const INVERSE: &str = "inverse"; +pub const INVERSE_SQUARE: &str = "inverse_square"; pub const AFFINE: &str = "affine"; pub const ABS: &str = "abs"; diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index 2e4b49fa..6eec696a 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -3892,6 +3892,25 @@ pub extern "C" fn processing_particles_grid_destroy(grid_id: u64) { error::check(|| grid_destroy(Entity::from_bits(grid_id))); } +/// Writes each particle's `neighbors` and `neighbor_count`, rebuilding the grid first. +#[unsafe(no_mangle)] +pub extern "C" fn processing_particles_find_neighbors( + particles_id: u64, + grid_id: u64, + radius: f32, + max: u32, +) { + error::clear_error(); + error::check(|| { + particles_find_neighbors( + Entity::from_bits(particles_id), + Entity::from_bits(grid_id), + radius, + max, + ) + }); +} + #[unsafe(no_mangle)] pub extern "C" fn processing_particles_draw(graphics_id: u64, particles_id: u64, geometry_id: u64) { error::clear_error(); diff --git a/crates/processing_pyo3/src/constants.rs b/crates/processing_pyo3/src/constants.rs index bd1a7436..99486f82 100644 --- a/crates/processing_pyo3/src/constants.rs +++ b/crates/processing_pyo3/src/constants.rs @@ -67,9 +67,17 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { add!(m, SUB, MUL, DIV, POW); add!(m, LENGTH, SUM, SUMSQ, MEAN, MIN, MAX); add!(m, UNIFORM, SIGNED, GAUSSIAN); - add!(m, NEIGHBOR); + add!(m, NEIGHBOR, FIND_NEIGHBORS); add!(m, COUNT, DENSITY); - add!(m, CONSTANT, SMOOTHSTEP, QUADRATIC, CUBIC, INVERSE); + add!( + m, + CONSTANT, + SMOOTHSTEP, + QUADRATIC, + CUBIC, + INVERSE, + INVERSE_SQUARE + ); add!( m, NOISE, diff --git a/crates/processing_pyo3/src/particles.rs b/crates/processing_pyo3/src/particles.rs index eeb54e57..ea270ede 100644 --- a/crates/processing_pyo3/src/particles.rs +++ b/crates/processing_pyo3/src/particles.rs @@ -111,6 +111,7 @@ fn parse_falloff(s: &str) -> PyResult { _ if s.eq_ignore_ascii_case(c::QUADRATIC) => Ok(FALLOFF_QUADRATIC), _ if s.eq_ignore_ascii_case(c::CUBIC) => Ok(FALLOFF_CUBIC), _ if s.eq_ignore_ascii_case(c::INVERSE) => Ok(FALLOFF_INVERSE), + _ if s.eq_ignore_ascii_case(c::INVERSE_SQUARE) => Ok(FALLOFF_INVERSE_SQUARE), _ => Err(PyValueError::new_err(format!( "neighbor: unknown falloff {s:?}" ))), @@ -787,19 +788,56 @@ impl Particles { let scale = kw_f32(kwargs, "scale", 1.0)?; let offset = kw_f32(kwargs, "offset", 0.0)?; algebra_generate(out, comp, mode, seed, scale, offset).map_err(rt) - } else if name.eq_ignore_ascii_case(c::NEIGHBOR) { - reject_unknown_kwargs(kwargs, &["a", "out", "grid", "op", "radius", "falloff"])?; + } else if name.eq_ignore_ascii_case(c::FIND_NEIGHBORS) { + reject_unknown_kwargs(kwargs, &["grid", "radius", "max"])?; let grid = kw(kwargs, "grid") - .ok_or_else(|| PyRuntimeError::new_err("apply(neighbor): missing 'grid'"))? + .ok_or_else(|| PyTypeError::new_err("apply(find_neighbors): missing `grid=`"))? .extract::>() - .map_err(|_| PyRuntimeError::new_err("apply(neighbor): 'grid' must be a Grid"))?; + .map_err(|_| PyTypeError::new_err("apply(find_neighbors): `grid` must be a Grid"))? + .entity; + let cell = grid_get(grid).map_err(rt)?.params.cell_size; + let radius = kw_f32(kwargs, "radius", cell)?; + let max = kw_u32(kwargs, "max", 64)?; + particles_find_neighbors(self.entity, grid, radius, max).map_err(rt) + } else if name.eq_ignore_ascii_case(c::NEIGHBOR) { + reject_unknown_kwargs( + kwargs, + &["a", "out", "grid", "op", "radius", "falloff", "relative"], + )?; let op = kw_op(kwargs, NEIGHBOR_MEAN, parse_neighbor_op)?; + let relative = match kw(kwargs, "relative") { + Some(v) => v.extract::()?, + None => false, + }; + // `grid=` searches exactly; otherwise read the lists from find_neighbors + let grid = match kw(kwargs, "grid") { + Some(g) => { + if relative { + return Err(PyValueError::new_err( + "apply(neighbor): relative=True reads the neighbor lists, so drop grid=", + )); + } + let g = g.extract::>().map_err(|_| { + PyTypeError::new_err("apply(neighbor): `grid` must be a Grid") + })?; + Some(g.entity) + } + None => None, + }; + let (falloff_default, radius) = match grid { + Some(g) => { + let cell = grid_get(g).map_err(rt)?.params.cell_size; + ( + FALLOFF_SMOOTHSTEP, + kw_f32(kwargs, "radius", cell)?.min(cell), + ) + } + None => (FALLOFF_CONST, kw_f32(kwargs, "radius", f32::MAX)?), + }; let falloff = match kw(kwargs, "falloff") { Some(v) => parse_falloff(&v.extract::()?)?, - None => FALLOFF_SMOOTHSTEP, + None => falloff_default, }; - let cell = grid.cell_size()?; - let radius = kw_f32(kwargs, "radius", cell)?.min(cell); let (a, components) = if op == NEIGHBOR_COUNT { let a = match kw(kwargs, "a") { @@ -816,16 +854,21 @@ impl Particles { self.operand(kwargs, "a")? }; let out = self.output(kwargs, components)?; - particles_gather( - self.entity, - grid.entity, - a, - out, - op, - radius, - falloff, - components, - ) + match grid { + Some(grid) => { + particles_gather(self.entity, grid, a, out, op, radius, falloff, components) + } + None => particles_neighbor_reduce( + self.entity, + a, + out, + op, + radius, + falloff, + components, + relative, + ), + } .map_err(rt) } else { Err(PyValueError::new_err(format!( diff --git a/crates/processing_render/shaders/processing/particles.wesl b/crates/processing_render/shaders/processing/particles.wesl index 66984c73..331ffaed 100644 --- a/crates/processing_render/shaders/processing/particles.wesl +++ b/crates/processing_render/shaders/processing/particles.wesl @@ -6,6 +6,7 @@ fn falloff(d: f32, radius: f32, mode: u32) -> f32 { case 3u: { return n * n; } case 4u: { return n * n * n; } case 5u: { return radius / (d + radius); } + case 6u: { return 1.0 / max(d * d, 1e-6); } default: { return 1.0; } } } diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 499f3a38..d215ea84 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -29,20 +29,23 @@ pub use particles::algebra::{ }; pub use particles::compact::compact; pub use particles::grid::{Grid, GridParams, grid_build, grid_create, grid_destroy, grid_get}; +pub use particles::neighbors::{ + particles_find_neighbors, particles_neighbor_lists, particles_neighbor_reduce, +}; pub use particles::reduce::{REDUCE_OP_MAX, REDUCE_OP_MIN, REDUCE_OP_SUM, reduce}; pub use particles::sort::bitonic_sort_by_key; pub use particles::{ BOUNDS_CLAMP, BOUNDS_REFLECT, BOUNDS_SOFT, BOUNDS_WRAP, COMBINE_ADD, COMBINE_DIV, COMBINE_MAX, COMBINE_MIN, COMBINE_MUL, COMBINE_POW, COMBINE_SUB, FALLOFF_CONST, FALLOFF_CUBIC, - FALLOFF_INVERSE, FALLOFF_LINEAR, FALLOFF_QUADRATIC, FALLOFF_SMOOTHSTEP, particles_apply, - particles_attribute_add, particles_attributes, particles_buffer, particles_capacity, - particles_connectivity_indirect, particles_create, particles_create_from_geometry, - particles_destroy, particles_emit, particles_emit_gpu, particles_ensure_attribute, - particles_flock, particles_gather, particles_kernel_age, particles_kernel_attr_combine, - particles_kernel_attr_linear, particles_kernel_attr_lookup1d, particles_kernel_attr_lookup2d, - particles_kernel_attr_mix, particles_kernel_attract, particles_kernel_bounds_box, - particles_kernel_bounds_geometry, particles_kernel_bounds_sphere, particles_kernel_drag, - particles_kernel_field, particles_kernel_flock, particles_kernel_force, + FALLOFF_INVERSE, FALLOFF_INVERSE_SQUARE, FALLOFF_LINEAR, FALLOFF_QUADRATIC, FALLOFF_SMOOTHSTEP, + particles_apply, particles_attribute_add, particles_attributes, particles_buffer, + particles_capacity, particles_connectivity_indirect, particles_create, + particles_create_from_geometry, particles_destroy, particles_emit, particles_emit_gpu, + particles_ensure_attribute, particles_flock, particles_gather, particles_kernel_age, + particles_kernel_attr_combine, particles_kernel_attr_linear, particles_kernel_attr_lookup1d, + particles_kernel_attr_lookup2d, particles_kernel_attr_mix, particles_kernel_attract, + particles_kernel_bounds_box, particles_kernel_bounds_geometry, particles_kernel_bounds_sphere, + particles_kernel_drag, particles_kernel_field, particles_kernel_flock, particles_kernel_force, particles_kernel_impulse, particles_kernel_integrate, particles_kernel_noise, particles_kernel_orient, particles_kernel_transform, particles_kernel_vortex, particles_reset_indices, particles_scatter_create, particles_scatter_volume_create, diff --git a/crates/processing_render/src/particles/emit.rs b/crates/processing_render/src/particles/emit.rs index 16e3ca27..fb5f30e1 100644 --- a/crates/processing_render/src/particles/emit.rs +++ b/crates/processing_render/src/particles/emit.rs @@ -252,13 +252,17 @@ pub fn particles_apply(particles_entity: Entity, compute_entity: Entity) -> erro let field = world .get::(particles_entity) .ok_or(error::ProcessingError::ParticlesNotFound)?; - let mut buffers: Vec<(String, Entity)> = Vec::with_capacity(field.buffers.len()); + let mut buffers: Vec<(String, Entity)> = Vec::with_capacity(field.buffers.len() + 2); for (&attr_entity, &buf_entity) in &field.buffers { let attr = world .get::(attr_entity) .ok_or(error::ProcessingError::InvalidEntity)?; buffers.push((attr.name.to_string(), buf_entity)); } + if let Some(lists) = field.neighbor_lists { + buffers.push(("neighbors".to_string(), lists.neighbors)); + buffers.push(("neighbor_count".to_string(), lists.count)); + } Ok((field.capacity, buffers)) })?; diff --git a/crates/processing_render/src/particles/kernels/mod.rs b/crates/processing_render/src/particles/kernels/mod.rs index a552af51..df9199fd 100644 --- a/crates/processing_render/src/particles/kernels/mod.rs +++ b/crates/processing_render/src/particles/kernels/mod.rs @@ -63,6 +63,8 @@ impl Plugin for ParticlesKernelsPlugin { embedded_asset!(app, "compact_scatter.wgsl"); embedded_asset!(app, "reduce.wgsl"); embedded_asset!(app, "neighbor.wgsl"); + embedded_asset!(app, "neighbors_build.wgsl"); + embedded_asset!(app, "neighbors_reduce.wgsl"); } } @@ -72,6 +74,7 @@ pub const FALLOFF_SMOOTHSTEP: u32 = 2; pub const FALLOFF_QUADRATIC: u32 = 3; pub const FALLOFF_CUBIC: u32 = 4; pub const FALLOFF_INVERSE: u32 = 5; +pub const FALLOFF_INVERSE_SQUARE: u32 = 6; pub const BOUNDS_CLAMP: u32 = 0; pub const BOUNDS_REFLECT: u32 = 1; diff --git a/crates/processing_render/src/particles/kernels/neighbors_build.wgsl b/crates/processing_render/src/particles/kernels/neighbors_build.wgsl new file mode 100644 index 00000000..ab3dae3c --- /dev/null +++ b/crates/processing_render/src/particles/kernels/neighbors_build.wgsl @@ -0,0 +1,76 @@ +import processing::particles::{Grid, cell_coords, cell_index}; + +// with radius == cell size, ~1 in 6.5 candidates is a neighbor; rounded up +const CANDIDATES_PER_NEIGHBOR: f32 = 7.0; + +struct Params { + radius: f32, + max_neighbors: u32, +} + +@group(0) @binding(0) var position: array; +@group(0) @binding(1) var grid_offsets: array; +@group(0) @binding(2) var grid_sorted: array; +@group(0) @binding(3) var grid: Grid; +@group(0) @binding(4) var neighbors: array; +@group(0) @binding(5) var neighbor_count: array; +@group(0) @binding(6) var params: Params; + +fn load_pos(i: u32) -> vec3 { + return vec3(position[i * 3u], position[i * 3u + 1u], position[i * 3u + 2u]); +} + +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) gid: vec3) { + let i = gid.x; + if i >= arrayLength(&neighbor_count) { return; } + + let pos = load_pos(i); + let r2 = params.radius * params.radius; + let dims = grid.dims; + let base = cell_coords(pos, grid.origin, grid.cell_size, dims); + let reach = max(1, i32(ceil(params.radius / grid.cell_size))); + let lo = max(base - vec3(reach), vec3(0)); + let hi = min(base + vec3(reach), vec3(dims) - vec3(1)); + + // past the limit, sample every cell evenly so lists aren't biased toward the first cells + var total = 0u; + for (var cz = lo.z; cz <= hi.z; cz++) { + for (var cy = lo.y; cy <= hi.y; cy++) { + for (var cx = lo.x; cx <= hi.x; cx++) { + let cell = cell_index(vec3(u32(cx), u32(cy), u32(cz)), dims); + total += grid_offsets[cell + 1u] - grid_offsets[cell]; + } + } + } + let rate = min(1.0, f32(params.max_neighbors) * CANDIDATES_PER_NEIGHBOR / f32(max(total, 1u))); + // per-particle offset so particles in a cell don't all sample the same members + let phase = i * 2654435761u; + + let first = i * params.max_neighbors; + var found = 0u; + for (var cz = lo.z; cz <= hi.z && found < params.max_neighbors; cz++) { + for (var cy = lo.y; cy <= hi.y && found < params.max_neighbors; cy++) { + for (var cx = lo.x; cx <= hi.x && found < params.max_neighbors; cx++) { + let cell = cell_index(vec3(u32(cx), u32(cy), u32(cz)), dims); + let start = grid_offsets[cell]; + let in_cell = grid_offsets[cell + 1u] - start; + let take = min(in_cell, u32(ceil(f32(in_cell) * rate))); + for (var k = 0u; k < take && found < params.max_neighbors; k++) { + var s = start + k; + if take < in_cell { + s = start + (phase + u32(f32(k) * f32(in_cell) / f32(take))) % in_cell; + } + let j = grid_sorted[s]; + if j == i { continue; } + let diff = load_pos(j) - pos; + if dot(diff, diff) <= r2 { + neighbors[first + found] = j; + found += 1u; + } + } + } + } + } + neighbor_count[i] = found; +} diff --git a/crates/processing_render/src/particles/kernels/neighbors_reduce.wgsl b/crates/processing_render/src/particles/kernels/neighbors_reduce.wgsl new file mode 100644 index 00000000..09beee95 --- /dev/null +++ b/crates/processing_render/src/particles/kernels/neighbors_reduce.wgsl @@ -0,0 +1,73 @@ +import processing::particles::falloff; + +struct Params { + max_distance: f32, + op: u32, + falloff_mode: u32, + components: u32, + // 1 = reduce `source[j] - source[i]` + relative: u32, +} + +const OP_SUM: u32 = 0u; +const OP_MEAN: u32 = 1u; +const OP_COUNT: u32 = 2u; + +@group(0) @binding(0) var position: array; +@group(0) @binding(1) var source: array; +@group(0) @binding(2) var out: array; +@group(0) @binding(3) var neighbors: array; +@group(0) @binding(4) var neighbor_count: array; +@group(0) @binding(5) var params: Params; + +fn load_pos(i: u32) -> vec3 { + return vec3(position[i * 3u], position[i * 3u + 1u], position[i * 3u + 2u]); +} + +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) gid: vec3) { + let i = gid.x; + let n = arrayLength(&neighbor_count); + if i >= n { return; } + + let pos = load_pos(i); + let r2 = params.max_distance * params.max_distance; + let comps = params.components; + + var own = array(0.0, 0.0, 0.0, 0.0); + if params.relative == 1u { + for (var c = 0u; c < comps; c++) { + own[c] = source[i * comps + c]; + } + } + + var value = array(0.0, 0.0, 0.0, 0.0); + var weight_sum = 0.0; + let first = i * (arrayLength(&neighbors) / n); + for (var k = 0u; k < neighbor_count[i]; k++) { + let j = neighbors[first + k]; + let diff = load_pos(j) - pos; + let d2 = dot(diff, diff); + if d2 > r2 { continue; } + let w = falloff(sqrt(d2), params.max_distance, params.falloff_mode); + weight_sum += w; + if params.op != OP_COUNT { + for (var c = 0u; c < comps; c++) { + value[c] += w * (source[j * comps + c] - own[c]); + } + } + } + + if params.op == OP_COUNT { + out[i] = weight_sum; + } else if params.op == OP_MEAN { + let inv = select(0.0, 1.0 / weight_sum, weight_sum > 0.0); + for (var c = 0u; c < comps; c++) { + out[i * comps + c] = value[c] * inv; + } + } else { + for (var c = 0u; c < comps; c++) { + out[i * comps + c] = value[c]; + } + } +} diff --git a/crates/processing_render/src/particles/mod.rs b/crates/processing_render/src/particles/mod.rs index ddc33a17..f0b20316 100644 --- a/crates/processing_render/src/particles/mod.rs +++ b/crates/processing_render/src/particles/mod.rs @@ -6,6 +6,7 @@ mod emit; pub mod grid; pub mod kernels; pub mod material; +pub mod neighbors; pub mod pack; pub mod point_render; pub mod reduce; @@ -27,13 +28,17 @@ pub use grid::{Grid, GridParams, grid_build, grid_create, grid_destroy, grid_get pub use kernels::{ BOUNDS_CLAMP, BOUNDS_REFLECT, BOUNDS_SOFT, BOUNDS_WRAP, COMBINE_ADD, COMBINE_DIV, COMBINE_MAX, COMBINE_MIN, COMBINE_MUL, COMBINE_POW, COMBINE_SUB, FALLOFF_CONST, FALLOFF_CUBIC, - FALLOFF_INVERSE, FALLOFF_LINEAR, FALLOFF_QUADRATIC, FALLOFF_SMOOTHSTEP, particles_kernel_age, - particles_kernel_attr_combine, particles_kernel_attr_linear, particles_kernel_attr_lookup1d, - particles_kernel_attr_lookup2d, particles_kernel_attr_mix, particles_kernel_attract, - particles_kernel_bounds_box, particles_kernel_bounds_geometry, particles_kernel_bounds_sphere, - particles_kernel_drag, particles_kernel_field, particles_kernel_flock, particles_kernel_force, - particles_kernel_impulse, particles_kernel_integrate, particles_kernel_noise, - particles_kernel_orient, particles_kernel_transform, particles_kernel_vortex, + FALLOFF_INVERSE, FALLOFF_INVERSE_SQUARE, FALLOFF_LINEAR, FALLOFF_QUADRATIC, FALLOFF_SMOOTHSTEP, + particles_kernel_age, particles_kernel_attr_combine, particles_kernel_attr_linear, + particles_kernel_attr_lookup1d, particles_kernel_attr_lookup2d, particles_kernel_attr_mix, + particles_kernel_attract, particles_kernel_bounds_box, particles_kernel_bounds_geometry, + particles_kernel_bounds_sphere, particles_kernel_drag, particles_kernel_field, + particles_kernel_flock, particles_kernel_force, particles_kernel_impulse, + particles_kernel_integrate, particles_kernel_noise, particles_kernel_orient, + particles_kernel_transform, particles_kernel_vortex, +}; +pub use neighbors::{ + particles_find_neighbors, particles_neighbor_lists, particles_neighbor_reduce, }; pub use reduce::{REDUCE_OP_MAX, REDUCE_OP_MIN, REDUCE_OP_SUM, reduce}; pub use scan::prefix_sum_u32; @@ -96,6 +101,15 @@ pub struct Particles { pub connectivity: Option, /// Ring-buffer write cursor; wraps at `capacity`. pub emit_head: u32, + pub neighbor_lists: Option, +} + +/// The `neighbors` (up to `max` indices per particle) and `neighbor_count` attributes. +#[derive(Clone, Copy)] +pub struct NeighborLists { + pub neighbors: Entity, + pub count: Entity, + pub max: u32, } #[derive(Clone, Copy)] @@ -150,6 +164,7 @@ pub fn create( raster_draw_entity: None, connectivity: None, emit_head: 0, + neighbor_lists: None, }) .id(); Ok(entity) @@ -231,6 +246,7 @@ pub fn create_from_geometry( raster_draw_entity: None, connectivity, emit_head: 0, + neighbor_lists: None, }) .id(); Ok(entity) @@ -387,6 +403,10 @@ pub fn destroy( commands.entity(connectivity.index_buffer).despawn(); commands.entity(connectivity.indirect_buffer).despawn(); } + if let Some(lists) = p.neighbor_lists { + commands.entity(lists.neighbors).despawn(); + commands.entity(lists.count).despawn(); + } commands.entity(entity).despawn(); Ok(()) } diff --git a/crates/processing_render/src/particles/neighbors.rs b/crates/processing_render/src/particles/neighbors.rs new file mode 100644 index 00000000..321870f4 --- /dev/null +++ b/crates/processing_render/src/particles/neighbors.rs @@ -0,0 +1,136 @@ +use std::sync::Mutex; + +use bevy::prelude::Entity; + +use processing_core::app_mut; +use processing_core::error::{ProcessingError, Result}; + +use crate::particles::grid::grid_build; +use crate::particles::{NeighborLists, Particles}; +use crate::shader_value::ShaderValue; +use crate::{ + buffer_create, buffer_destroy, compute_create, compute_dispatch_no_update, compute_set, + geometry_attribute_position, shader_load, +}; + +const BUILD_SHADER: &str = "embedded://processing_render/particles/kernels/neighbors_build.wgsl"; +const REDUCE_SHADER: &str = "embedded://processing_render/particles/kernels/neighbors_reduce.wgsl"; +const WG: u32 = 64; + +static COMPUTES: Mutex> = Mutex::new(None); + +fn computes() -> Result<(Entity, Entity)> { + let mut guard = COMPUTES.lock().unwrap(); + if let Some(v) = *guard { + return Ok(v); + } + let build = compute_create(shader_load(BUILD_SHADER)?)?; + let reduce = compute_create(shader_load(REDUCE_SHADER)?)?; + *guard = Some((build, reduce)); + Ok((build, reduce)) +} + +fn position_buffer(particles: Entity) -> Result { + crate::particles::particles_buffer(particles, geometry_attribute_position())?.ok_or_else(|| { + ProcessingError::InvalidArgument("neighbors need a `position` attribute".to_string()) + }) +} + +/// The `neighbors` and `neighbor_count` attributes, if `particles_find_neighbors` has run. +pub fn particles_neighbor_lists(particles: Entity) -> Result> { + app_mut(|app| { + Ok(app + .world() + .get::(particles) + .ok_or(ProcessingError::ParticlesNotFound)? + .neighbor_lists) + }) +} + +/// Rebuilds `grid` from the particles' positions, then writes each particle's `neighbors` +/// (up to `max` within `radius`) and `neighbor_count`. Past `max`, a spread sample is kept, +/// not the nearest. +pub fn particles_find_neighbors( + particles: Entity, + grid: Entity, + radius: f32, + max: u32, +) -> Result<()> { + if max == 0 { + return Err(ProcessingError::InvalidArgument( + "find_neighbors: max must be at least 1".to_string(), + )); + } + let capacity = crate::particles::particles_capacity(particles)?; + let lists = match particles_neighbor_lists(particles)? { + Some(lists) if lists.max == max => lists, + previous => { + if let Some(previous) = previous { + buffer_destroy(previous.neighbors)?; + buffer_destroy(previous.count)?; + } + let slots = capacity.max(1) as u64; + let lists = NeighborLists { + neighbors: buffer_create(slots * max as u64 * 4)?, + count: buffer_create(slots * 4)?, + max, + }; + app_mut(|app| { + app.world_mut() + .get_mut::(particles) + .ok_or(ProcessingError::ParticlesNotFound)? + .neighbor_lists = Some(lists); + Ok(()) + })?; + lists + } + }; + + let position = position_buffer(particles)?; + grid_build(grid, position)?; + let (build, _) = computes()?; + compute_set(build, "position", ShaderValue::Buffer(position))?; + compute_set(build, "grid", ShaderValue::Grid(grid))?; + compute_set(build, "neighbors", ShaderValue::Buffer(lists.neighbors))?; + compute_set(build, "neighbor_count", ShaderValue::Buffer(lists.count))?; + compute_set(build, "radius", ShaderValue::Float(radius))?; + compute_set(build, "max_neighbors", ShaderValue::UInt(max))?; + compute_dispatch_no_update(build, capacity.div_ceil(WG), 1, 1) +} + +/// Like `particles_gather`, but over each particle's `neighbors`, so a particle never counts +/// itself. With `relative`, reduces `source[j] - source[i]`. +#[allow(clippy::too_many_arguments)] +pub fn particles_neighbor_reduce( + particles: Entity, + source: Entity, + out: Entity, + op: u32, + radius: f32, + falloff_mode: u32, + components: u32, + relative: bool, +) -> Result<()> { + let lists = particles_neighbor_lists(particles)?.ok_or_else(|| { + ProcessingError::InvalidArgument("run find_neighbors before reading neighbors".to_string()) + })?; + let position = position_buffer(particles)?; + if out == source || out == position { + return Err(ProcessingError::InvalidArgument( + "neighbor: `out` must differ from the source and position".to_string(), + )); + } + let capacity = crate::particles::particles_capacity(particles)?; + let (_, reduce) = computes()?; + compute_set(reduce, "position", ShaderValue::Buffer(position))?; + compute_set(reduce, "source", ShaderValue::Buffer(source))?; + compute_set(reduce, "out", ShaderValue::Buffer(out))?; + compute_set(reduce, "neighbors", ShaderValue::Buffer(lists.neighbors))?; + compute_set(reduce, "neighbor_count", ShaderValue::Buffer(lists.count))?; + compute_set(reduce, "max_distance", ShaderValue::Float(radius))?; + compute_set(reduce, "op", ShaderValue::UInt(op))?; + compute_set(reduce, "falloff_mode", ShaderValue::UInt(falloff_mode))?; + compute_set(reduce, "components", ShaderValue::UInt(components))?; + compute_set(reduce, "relative", ShaderValue::UInt(relative as u32))?; + compute_dispatch_no_update(reduce, capacity.div_ceil(WG), 1, 1) +} From 2a145c5175bd15482dd6ef42fd94199ab3e78f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 28 Sep 2026 21:56:40 -0700 Subject: [PATCH 07/20] Flocking from generic ops. --- crates/processing_ffi/src/lib.rs | 8 -- .../processing_pyo3/examples/flocking_duck.py | 2 +- .../processing_pyo3/examples/flocking_gpu.py | 76 ++++++----- crates/processing_pyo3/src/particles.rs | 30 ----- crates/processing_render/src/lib.rs | 4 +- .../processing_render/src/particles/emit.rs | 28 ---- .../src/particles/kernels/flock.wgsl | 125 ------------------ .../src/particles/kernels/mod.rs | 16 --- crates/processing_render/src/particles/mod.rs | 10 +- 9 files changed, 51 insertions(+), 248 deletions(-) delete mode 100644 crates/processing_render/src/particles/kernels/flock.wgsl diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index 6eec696a..f78a8660 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -3694,14 +3694,6 @@ pub extern "C" fn processing_particles_kernel_impulse() -> u64 { .unwrap_or(0) } -#[unsafe(no_mangle)] -pub extern "C" fn processing_particles_kernel_flock() -> u64 { - error::clear_error(); - error::check(particles_kernel_flock) - .map(|e| e.to_bits()) - .unwrap_or(0) -} - #[unsafe(no_mangle)] pub extern "C" fn processing_particles_kernel_orient() -> u64 { error::clear_error(); diff --git a/crates/processing_pyo3/examples/flocking_duck.py b/crates/processing_pyo3/examples/flocking_duck.py index e60ff4f4..f53d1030 100644 --- a/crates/processing_pyo3/examples/flocking_duck.py +++ b/crates/processing_pyo3/examples/flocking_duck.py @@ -1,4 +1,4 @@ -# Flocking inside a duck: the GPU boids from flocking_gpu.py, seeded from +# Flocking inside a duck: GPU boids seeded from # the vertices of the Duck glTF mesh. Each boid remembers its spawn vertex # in a `home` attribute; a homing force that is negligible near home but # grows quadratically with distance lets the boids swirl and flock locally diff --git a/crates/processing_pyo3/examples/flocking_gpu.py b/crates/processing_pyo3/examples/flocking_gpu.py index ab8ec579..4b33a5a8 100644 --- a/crates/processing_pyo3/examples/flocking_gpu.py +++ b/crates/processing_pyo3/examples/flocking_gpu.py @@ -1,13 +1,10 @@ -# GPU flocking: the boids from flocking.py, moved entirely onto the GPU. -# Positions and velocities live in particle attribute buffers, two compute -# kernels update them each frame, and the flock renders instanced — nothing -# is ever read back to the CPU. Brute-force O(N²) neighbor search is trivial -# for a GPU at this scale; a spatial hash grid is the next step past ~100k. +# GPU flocking: the boids from flocking.py, built from generic particle ops +# over each boid's neighbors. Nothing is read back to the CPU. from mewnala import * from math import cos, sin from random import uniform -BOID_COUNT = 10000 +BOID_COUNT = 100000 BOUND = 30.0 # half-extent of the wrapping box NEIGHBOR_DIST = 5.0 SEPARATION_DIST = 2.5 @@ -15,12 +12,12 @@ MAX_FORCE = 6.0 # units per second² DT = 1.0 / 60.0 -# Pass 1: every boid reads the whole flock's state and writes only its -# steering force. Splitting the read from the write mirrors the CPU -# example's two loops — no boid sees a half-updated neighbor. - -# Pass 2: integrate the steering force, wrap at the box edges, and point -# each instanced boid along its velocity via the rotation quaternion. +# Separation steers away from the summed offsets, so its speed is negative. +RULES = [ + ("separation", -MAX_SPEED, 1.5, "close"), + ("alignment", MAX_SPEED, 1.0, "near"), + ("cohesion", MAX_SPEED, 1.0, "near"), +] p = None boid = None @@ -60,15 +57,7 @@ def setup(): directional_light((0.95, 0.9, 0.85), 800.0) - p = create_particles( - BOID_COUNT, - attributes=[ - Attribute.position(), - Attribute.rotation(), - Attribute.color(), - Attribute.velocity(), - ], - ) + p = create_particles(BOID_COUNT) positions = [] velocities = [] @@ -98,6 +87,38 @@ def setup(): ) +# Reynolds steering. `mask` zeroes it for boids with no neighbors in range, +# which would otherwise brake. +def steer(desired, speed, mask): + p.apply(MAP, desired, op=NORMALIZE, length=speed) + p.apply(COMBINE, desired, "velocity", op=SUB) + p.apply(MAP, desired, op=LIMIT, max_length=MAX_FORCE * DT) + p.apply(COMBINE, desired, mask, op=MUL) + + +def flock(): + p.apply(FIND_NEIGHBORS, grid=grid, radius=NEIGHBOR_DIST) + # what each rule steers toward, summed over the neighbors + p.apply(NEIGHBOR, "position", out="separation", op=SUM, + relative=True, radius=SEPARATION_DIST, falloff=INVERSE_SQUARE) + p.apply(NEIGHBOR, "velocity", out="alignment", op=SUM) + p.apply(NEIGHBOR, "position", out="cohesion", op=SUM, relative=True) + # which boids have any neighbors for each rule + p.apply(NEIGHBOR, out="close", op=COUNT, radius=SEPARATION_DIST) + p.apply(NEIGHBOR, out="near", op=COUNT) + p.apply(MAP, "close", op=GREATER, threshold=0) + p.apply(MAP, "near", op=GREATER, threshold=0) + + for i, (rule, speed, weight, mask) in enumerate(RULES): + steer(rule, speed, mask) + if i == 0: + p.apply(MAP, rule, out="force", op=AFFINE, scale=weight) + else: + p.apply(COMBINE, "force", rule, op="add", b_scale=weight) + p.apply(COMBINE, "velocity", "force", op="add") + p.apply(MAP, "velocity", op=LIMIT, min_length=MAX_SPEED * 0.25, max_length=MAX_SPEED) + + def draw(): global title_last_time, title_last_frame @@ -114,20 +135,11 @@ def draw(): camera_look_at(0.0, 0.0, 0.0) background(10, 12, 18) + flock() + material(mat) particles(p, boid) - p.flock( - grid, - sep_distance=SEPARATION_DIST, - neighbor_distance=NEIGHBOR_DIST, - weight_separation=1.5, - weight_alignment=1.0, - weight_cohesion=1.0, - max_speed=MAX_SPEED, - max_force=MAX_FORCE * DT, - min_speed=MAX_SPEED * 0.25, - ) p.apply(INTEGRATE, dt=DT) p.apply(BOUNDS_BOX, aabb_min=[-BOUND] * 3, aabb_max=[BOUND] * 3, mode=2) p.apply(ORIENT, forward=[0.0, 0.0, 1.0], up=[0.0, 1.0, 0.0]) diff --git a/crates/processing_pyo3/src/particles.rs b/crates/processing_pyo3/src/particles.rs index ea270ede..788f0d6d 100644 --- a/crates/processing_pyo3/src/particles.rs +++ b/crates/processing_pyo3/src/particles.rs @@ -150,18 +150,6 @@ impl Drop for Grid { } } -static FLOCK_COMPUTE: std::sync::Mutex> = std::sync::Mutex::new(None); - -fn flock_compute() -> PyResult { - let mut guard = FLOCK_COMPUTE.lock().unwrap(); - if let Some(e) = *guard { - return Ok(e); - } - let e = particles_kernel_flock().map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; - *guard = Some(e); - Ok(e) -} - static PHYSICS_COMPUTES: std::sync::Mutex>> = std::sync::Mutex::new(None); @@ -1113,24 +1101,6 @@ impl Particles { Ok(Grid { entity }) } - #[pyo3(signature = (grid, **kwargs))] - pub fn flock(&self, grid: &Grid, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<()> { - let flock = flock_compute()?; - if let Some(kwargs) = kwargs { - crate::compute::set_compute_kwargs(flock, kwargs)?; - } - let cell = grid.cell_size()?; - let neighbor_distance = kw_f32(kwargs, "neighbor_distance", cell)?.min(cell); - compute_set( - flock, - "neighbor_distance", - shader_value::ShaderValue::Float(neighbor_distance), - ) - .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; - particles_flock(self.entity, flock, grid.entity) - .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) - } - #[staticmethod] pub fn orient() -> PyResult { let entity = diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index d215ea84..6bbfe67f 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -41,11 +41,11 @@ pub use particles::{ particles_apply, particles_attribute_add, particles_attributes, particles_buffer, particles_capacity, particles_connectivity_indirect, particles_create, particles_create_from_geometry, particles_destroy, particles_emit, particles_emit_gpu, - particles_ensure_attribute, particles_flock, particles_gather, particles_kernel_age, + particles_ensure_attribute, particles_gather, particles_kernel_age, particles_kernel_attr_combine, particles_kernel_attr_linear, particles_kernel_attr_lookup1d, particles_kernel_attr_lookup2d, particles_kernel_attr_mix, particles_kernel_attract, particles_kernel_bounds_box, particles_kernel_bounds_geometry, particles_kernel_bounds_sphere, - particles_kernel_drag, particles_kernel_field, particles_kernel_flock, particles_kernel_force, + particles_kernel_drag, particles_kernel_field, particles_kernel_force, particles_kernel_impulse, particles_kernel_integrate, particles_kernel_noise, particles_kernel_orient, particles_kernel_transform, particles_kernel_vortex, particles_reset_indices, particles_scatter_create, particles_scatter_volume_create, diff --git a/crates/processing_render/src/particles/emit.rs b/crates/processing_render/src/particles/emit.rs index fb5f30e1..51cd91a9 100644 --- a/crates/processing_render/src/particles/emit.rs +++ b/crates/processing_render/src/particles/emit.rs @@ -143,34 +143,6 @@ pub fn particles_emit( }) } -pub fn particles_flock( - particles_entity: Entity, - flock_entity: Entity, - grid: Entity, -) -> error::Result<()> { - let position = app_mut(|app| { - let world = app.world(); - let field = world - .get::(particles_entity) - .ok_or(error::ProcessingError::ParticlesNotFound)?; - for (&attr_entity, &buf_entity) in &field.buffers { - let attr = world - .get::(attr_entity) - .ok_or(error::ProcessingError::InvalidEntity)?; - if attr.name == "position" { - return Ok(buf_entity); - } - } - Err(error::ProcessingError::InvalidArgument( - "particles_flock requires a `position` attribute".to_string(), - )) - })?; - - grid_build(grid, position)?; - compute_set(flock_entity, "grid", ShaderValue::Grid(grid))?; - particles_apply(particles_entity, flock_entity) -} - static NEIGHBOR_COMPUTE: Mutex> = Mutex::new(None); fn neighbor_compute() -> error::Result { diff --git a/crates/processing_render/src/particles/kernels/flock.wgsl b/crates/processing_render/src/particles/kernels/flock.wgsl deleted file mode 100644 index b1e75ec3..00000000 --- a/crates/processing_render/src/particles/kernels/flock.wgsl +++ /dev/null @@ -1,125 +0,0 @@ -import processing::particles::{Grid, cell_coords, cell_index}; - -struct FlockParams { - sep_distance: f32, - neighbor_distance: f32, - weight_separation: f32, - weight_alignment: f32, - weight_cohesion: f32, - max_speed: f32, - max_force: f32, - min_speed: f32, -} - -@group(0) @binding(0) var position: array; -@group(0) @binding(1) var velocity: array; -@group(0) @binding(2) var grid_offsets: array; -@group(0) @binding(3) var grid_sorted: array; -@group(0) @binding(4) var fp: FlockParams; -@group(0) @binding(5) var grid: Grid; - -fn limit_mag(v: vec3, m: f32) -> vec3 { - let len2 = dot(v, v); - if len2 > m * m { return v * (m * inverseSqrt(len2)); } - return v; -} - -fn steer_toward(desired: vec3, vel: vec3, max_speed: f32, max_force: f32) -> vec3 { - let m2 = dot(desired, desired); - if m2 < 0.00000001 { return vec3(0.0); } - return limit_mag(desired * (max_speed * inverseSqrt(m2)) - vel, max_force); -} - -fn load_pos(i: u32) -> vec3 { - return vec3(position[i * 3u], position[i * 3u + 1u], position[i * 3u + 2u]); -} - -fn load_vel(i: u32) -> vec3 { - return vec3(velocity[i * 3u], velocity[i * 3u + 1u], velocity[i * 3u + 2u]); -} - -@compute @workgroup_size(64) -fn main(@builtin(global_invocation_id) gid: vec3) { - let i = gid.x; - let count = arrayLength(&position) / 3u; - if i >= count { return; } - - let pos = load_pos(i); - let vel = load_vel(i); - - let sep_d2 = fp.sep_distance * fp.sep_distance; - let neighbor_d2 = fp.neighbor_distance * fp.neighbor_distance; - - let dims = grid.dims; - let base = cell_coords(pos, grid.origin, grid.cell_size, dims); - let bx = base.x; - let by = base.y; - let bz = base.z; - - var sep_steer = vec3(0.0); - var sep_count = 0u; - var ali_sum = vec3(0.0); - var coh_sum = vec3(0.0); - var flock_count = 0u; - - for (var dz = -1; dz <= 1; dz++) { - let cz = bz + dz; - if cz < 0 || cz >= i32(grid.dims.z) { continue; } - for (var dy = -1; dy <= 1; dy++) { - let cy = by + dy; - if cy < 0 || cy >= i32(grid.dims.y) { continue; } - for (var dx = -1; dx <= 1; dx++) { - let cx = bx + dx; - if cx < 0 || cx >= i32(grid.dims.x) { continue; } - - let cell = cell_index(vec3(u32(cx), u32(cy), u32(cz)), dims); - let start = grid_offsets[cell]; - let end = grid_offsets[cell + 1u]; - for (var s = start; s < end; s++) { - let j = grid_sorted[s]; - if j == i { continue; } - - let diff = pos - load_pos(j); - let d2 = dot(diff, diff); - if d2 > 0.000001 && d2 < neighbor_d2 { - if d2 < sep_d2 { - sep_steer += diff / d2; - sep_count += 1u; - } - ali_sum += load_vel(j); - coh_sum += diff; - flock_count += 1u; - } - } - } - } - } - - var force = vec3(0.0); - if sep_count > 0u { - force += steer_toward(sep_steer / f32(sep_count), vel, - fp.max_speed, fp.max_force) * fp.weight_separation; - } - if flock_count > 0u { - force += steer_toward(ali_sum / f32(flock_count), vel, - fp.max_speed, fp.max_force) * fp.weight_alignment; - force += steer_toward(-coh_sum / f32(flock_count), vel, - fp.max_speed, fp.max_force) * fp.weight_cohesion; - } - - var new_vel = vel + force; - let speed2 = dot(new_vel, new_vel); - let max_speed_sq = fp.max_speed * fp.max_speed; - if speed2 > max_speed_sq { - new_vel = new_vel * (fp.max_speed * inverseSqrt(speed2)); - } else if fp.min_speed > 0.0 { - let min_speed_sq = fp.min_speed * fp.min_speed; - if speed2 < min_speed_sq && speed2 > 0.0 { - new_vel = new_vel * sqrt(min_speed_sq / speed2); - } - } - - velocity[i * 3u] = new_vel.x; - velocity[i * 3u + 1u] = new_vel.y; - velocity[i * 3u + 2u] = new_vel.z; -} diff --git a/crates/processing_render/src/particles/kernels/mod.rs b/crates/processing_render/src/particles/kernels/mod.rs index df9199fd..7cb8c83a 100644 --- a/crates/processing_render/src/particles/kernels/mod.rs +++ b/crates/processing_render/src/particles/kernels/mod.rs @@ -42,7 +42,6 @@ impl Plugin for ParticlesKernelsPlugin { embedded_asset!(app, "bounds_sphere.wgsl"); embedded_asset!(app, "bounds_box.wgsl"); embedded_asset!(app, "impulse.wgsl"); - embedded_asset!(app, "flock.wgsl"); embedded_asset!(app, "orient.wgsl"); embedded_asset!(app, "field.wgsl"); embedded_asset!(app, "attr_linear.wgsl"); @@ -255,21 +254,6 @@ pub fn particles_kernel_impulse() -> error::Result { Ok(entity) } -pub fn particles_kernel_flock() -> error::Result { - let shader = shader_load("embedded://processing_render/particles/kernels/flock.wgsl")?; - let entity = compute_create(shader)?; - set_requires(entity, &["position", "velocity"])?; - compute_set(entity, "sep_distance", ShaderValue::Float(1.2))?; - compute_set(entity, "neighbor_distance", ShaderValue::Float(2.5))?; - compute_set(entity, "weight_separation", ShaderValue::Float(1.5))?; - compute_set(entity, "weight_alignment", ShaderValue::Float(1.0))?; - compute_set(entity, "weight_cohesion", ShaderValue::Float(1.0))?; - compute_set(entity, "max_speed", ShaderValue::Float(0.1))?; - compute_set(entity, "max_force", ShaderValue::Float(0.003))?; - compute_set(entity, "min_speed", ShaderValue::Float(0.02))?; - Ok(entity) -} - pub fn particles_kernel_orient() -> error::Result { let shader = shader_load("embedded://processing_render/particles/kernels/orient.wgsl")?; let entity = compute_create(shader)?; diff --git a/crates/processing_render/src/particles/mod.rs b/crates/processing_render/src/particles/mod.rs index f0b20316..e6013b40 100644 --- a/crates/processing_render/src/particles/mod.rs +++ b/crates/processing_render/src/particles/mod.rs @@ -21,9 +21,7 @@ pub use algebra::{ REDUCE_SUMSQ, combine, extract, generate, lookup, map, mix, pack, reduce_components, }; pub use compact::compact; -pub use emit::{ - particles_apply, particles_emit, particles_emit_gpu, particles_flock, particles_gather, -}; +pub use emit::{particles_apply, particles_emit, particles_emit_gpu, particles_gather}; pub use grid::{Grid, GridParams, grid_build, grid_create, grid_destroy, grid_get}; pub use kernels::{ BOUNDS_CLAMP, BOUNDS_REFLECT, BOUNDS_SOFT, BOUNDS_WRAP, COMBINE_ADD, COMBINE_DIV, COMBINE_MAX, @@ -33,9 +31,9 @@ pub use kernels::{ particles_kernel_attr_lookup1d, particles_kernel_attr_lookup2d, particles_kernel_attr_mix, particles_kernel_attract, particles_kernel_bounds_box, particles_kernel_bounds_geometry, particles_kernel_bounds_sphere, particles_kernel_drag, particles_kernel_field, - particles_kernel_flock, particles_kernel_force, particles_kernel_impulse, - particles_kernel_integrate, particles_kernel_noise, particles_kernel_orient, - particles_kernel_transform, particles_kernel_vortex, + particles_kernel_force, particles_kernel_impulse, particles_kernel_integrate, + particles_kernel_noise, particles_kernel_orient, particles_kernel_transform, + particles_kernel_vortex, }; pub use neighbors::{ particles_find_neighbors, particles_neighbor_lists, particles_neighbor_reduce, From 7348e920a45674862f33d4114f71b7c406f90c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Fri, 2 Oct 2026 20:32:43 -0700 Subject: [PATCH 08/20] Bevy main. --- Cargo.lock | 2567 ++++++----------- Cargo.toml | 62 +- crates/processing_ffi/Cargo.toml | 3 +- crates/processing_pyo3/Cargo.toml | 6 +- crates/processing_pyo3/src/color.rs | 6 + crates/processing_render/Cargo.toml | 2 +- crates/processing_render/src/compute.rs | 29 +- crates/processing_render/src/graphics.rs | 5 +- crates/processing_render/src/image.rs | 5 +- crates/processing_render/src/lib.rs | 8 +- .../processing_render/src/material/custom.rs | 43 +- .../src/particles/material.rs | 6 +- crates/processing_render/src/particles/mod.rs | 4 +- .../processing_render/src/particles/pack.rs | 74 +- .../processing_render/src/particles/pack.wesl | 82 + .../processing_render/src/particles/pack.wgsl | 113 - .../src/particles/particles.wesl | 61 + .../src/particles/particles.wgsl | 60 - .../src/particles/{point.wgsl => point.wesl} | 43 +- .../src/particles/point_render.rs | 5 +- .../src/render/primitive/shape3d.rs | 18 +- src/lib.rs | 3 +- src/prelude.rs | 38 +- 23 files changed, 1276 insertions(+), 1967 deletions(-) create mode 100644 crates/processing_render/src/particles/pack.wesl delete mode 100644 crates/processing_render/src/particles/pack.wgsl create mode 100644 crates/processing_render/src/particles/particles.wesl delete mode 100644 crates/processing_render/src/particles/particles.wgsl rename crates/processing_render/src/particles/{point.wgsl => point.wesl} (59%) diff --git a/Cargo.lock b/Cargo.lock index 350f0e5a..5899f076 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,42 +20,46 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "accesskit" -version = "0.24.1" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +checksum = "ad442f58ee04714aaa0ba0a2768c1ea1935b29507bb22ddece8cbbc76db02932" dependencies = [ "uuid", ] [[package]] name = "accesskit_consumer" -version = "0.35.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" +checksum = "8a45e2bb300bea6d5f33a82c3e051c3ddc6f41c209750f68a0acbe991a284344" dependencies = [ "accesskit", - "hashbrown 0.16.1", + "hashbrown 0.17.1", ] [[package]] -name = "accesskit_consumer" -version = "0.38.0" +name = "accesskit_ios" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" +checksum = "28e539d64b27fd4c14de8f78ada8f7c944c092f52f7e49fb63316507a57f6937" dependencies = [ "accesskit", - "hashbrown 0.16.1", + "accesskit_consumer", + "hashbrown 0.17.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", ] [[package]] name = "accesskit_macos" -version = "0.26.3" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" +checksum = "5701624bf6a11efb6d4c8e922bf44126ed96250e5faa9a7a0468c979635839a4" dependencies = [ "accesskit", - "accesskit_consumer 0.38.0", - "hashbrown 0.16.1", + "accesskit_consumer", + "hashbrown 0.17.1", "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", @@ -63,13 +67,13 @@ dependencies = [ [[package]] name = "accesskit_windows" -version = "0.32.1" +version = "0.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" +checksum = "ce63f35d6bdcf59f26b76b3379063f738e6412cef46999cc772d46aa3de35adb" dependencies = [ "accesskit", - "accesskit_consumer 0.35.0", - "hashbrown 0.16.1", + "accesskit_consumer", + "hashbrown 0.17.1", "static_assertions", "windows 0.62.2", "windows-core 0.62.2", @@ -77,11 +81,12 @@ dependencies = [ [[package]] name = "accesskit_winit" -version = "0.32.2" +version = "0.34.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313" +checksum = "ee96dd150375c05912d8a473f98c2fb155fb3234fac88292d396548a2460fa77" dependencies = [ "accesskit", + "accesskit_ios", "accesskit_macos", "accesskit_windows", "raw-window-handle", @@ -100,7 +105,7 @@ version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ - "cfg-if 1.0.4", + "cfg-if", "getrandom 0.3.4", "once_cell", "version_check", @@ -148,7 +153,7 @@ checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" dependencies = [ "alsa-sys", "bitflags 2.13.1", - "cfg-if 1.0.4", + "cfg-if", "libc", ] @@ -182,15 +187,6 @@ dependencies = [ "thiserror 2.0.20", ] -[[package]] -name = "android-build" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fc9904ad2ad097c3c1cfe2eacaaf0fc24710936fa9ed941cb310b7c6ed2ab7" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "android-properties" version = "0.2.2" @@ -294,6 +290,24 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "wl-clipboard-rs", + "x11rb", +] + [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -347,7 +361,7 @@ version = "0.38.0+1.3.281" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" dependencies = [ - "libloading 0.8.9", + "libloading", ] [[package]] @@ -417,7 +431,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ "autocfg", - "cfg-if 1.0.4", + "cfg-if", "concurrent-queue", "futures-io", "futures-lite", @@ -467,43 +481,6 @@ dependencies = [ "portable-atomic-util", ] -[[package]] -name = "audio-core" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ebbf82d06013f4c41fe71303feb980cddd78496d904d06be627972de51a24" - -[[package]] -name = "audioadapter" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75c3943c6c7279bb25a449a8d1727480730ab2efd7b6fd5d6ca51927096e6e4" -dependencies = [ - "audio-core", - "num-traits", -] - -[[package]] -name = "audioadapter-buffers" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece3390b6eb40379094843a1da5aaccc34bc0d85a8cbf68d09fe092fee6de29e" -dependencies = [ - "audioadapter", - "audioadapter-sample", - "num-traits", -] - -[[package]] -name = "audioadapter-sample" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1592f90413568e259413c21a41a3d571feb1774255c209e7966d98f9db708c90" -dependencies = [ - "audio-core", - "num-traits", -] - [[package]] name = "autocfg" version = "1.5.1" @@ -539,7 +516,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom 8.0.0", + "nom", "num-rational", "v_frame", ] @@ -555,22 +532,22 @@ dependencies = [ [[package]] name = "base64" -version = "0.22.1" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "bevy" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_internal", ] [[package]] name = "bevy_a11y" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "accesskit", "bevy_app", @@ -581,21 +558,22 @@ dependencies = [ [[package]] name = "bevy_android" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "android-activity", ] [[package]] name = "bevy_animation" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_animation_macros", "bevy_app", "bevy_asset", "bevy_color", + "bevy_curve", "bevy_derive", "bevy_ecs", "bevy_math", @@ -605,6 +583,7 @@ dependencies = [ "bevy_time", "bevy_transform", "bevy_utils", + "bitvec", "blake3", "derive_more", "downcast-rs 2.0.2", @@ -621,18 +600,18 @@ dependencies = [ [[package]] name = "bevy_animation_macros" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ - "bevy_macro_utils 0.19.0", + "bevy_macro_utils", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "bevy_anti_alias" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", @@ -641,6 +620,7 @@ dependencies = [ "bevy_derive", "bevy_diagnostic", "bevy_ecs", + "bevy_extract", "bevy_image", "bevy_math", "bevy_reflect", @@ -652,8 +632,8 @@ dependencies = [ [[package]] name = "bevy_app" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_derive", "bevy_ecs", @@ -673,8 +653,8 @@ dependencies = [ [[package]] name = "bevy_asset" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "async-broadcast", "async-channel", @@ -716,19 +696,19 @@ dependencies = [ [[package]] name = "bevy_asset_macros" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ - "bevy_macro_utils 0.19.0", + "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "bevy_audio" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", @@ -742,8 +722,8 @@ dependencies = [ [[package]] name = "bevy_camera" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", @@ -754,6 +734,7 @@ dependencies = [ "bevy_math", "bevy_mesh", "bevy_reflect", + "bevy_shape", "bevy_transform", "bevy_utils", "bevy_window", @@ -762,21 +743,27 @@ dependencies = [ "serde", "smallvec", "thiserror 2.0.20", - "wgpu-types", + "wgpu-types 30.0.1", ] [[package]] name = "bevy_camera_controller" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_camera", + "bevy_color", + "bevy_curve", + "bevy_derive", "bevy_ecs", + "bevy_image", "bevy_input", "bevy_log", "bevy_math", "bevy_picking", + "bevy_platform", + "bevy_render", "bevy_time", "bevy_transform", "bevy_window", @@ -784,9 +771,10 @@ dependencies = [ [[package]] name = "bevy_clipboard" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ + "arboard", "bevy_app", "bevy_ecs", "bevy_log", @@ -798,9 +786,10 @@ dependencies = [ [[package]] name = "bevy_color" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ + "bevy_curve", "bevy_math", "bevy_reflect", "bytemuck", @@ -808,13 +797,13 @@ dependencies = [ "encase", "serde", "thiserror 2.0.20", - "wgpu-types", + "wgpu-types 30.0.1", ] [[package]] name = "bevy_core_pipeline" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", @@ -823,6 +812,7 @@ dependencies = [ "bevy_derive", "bevy_diagnostic", "bevy_ecs", + "bevy_extract", "bevy_image", "bevy_light", "bevy_log", @@ -848,56 +838,77 @@ dependencies = [ "bevy", "cudarc", "thiserror 2.0.20", - "wgpu", - "wgpu-hal", + "wgpu 29.0.4", + "wgpu-hal 29.0.4", "windows 0.58.0", ] +[[package]] +name = "bevy_curve" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" +dependencies = [ + "approx", + "bevy_math", + "bevy_reflect", + "itertools 0.14.0", + "serde", + "thiserror 2.0.20", + "variadics_please", +] + [[package]] name = "bevy_derive" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ - "bevy_macro_utils 0.19.0", + "bevy_macro_utils", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "bevy_dev_tools" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ + "bevy_animation", "bevy_app", "bevy_asset", + "bevy_audio", "bevy_camera", "bevy_color", - "bevy_core_pipeline", "bevy_diagnostic", "bevy_ecs", + "bevy_extract", "bevy_image", "bevy_input", + "bevy_light", "bevy_log", "bevy_math", - "bevy_pbr", + "bevy_mesh", "bevy_picking", + "bevy_platform", "bevy_reflect", "bevy_render", "bevy_shader", + "bevy_sprite", "bevy_state", "bevy_text", "bevy_time", "bevy_transform", "bevy_ui", - "bevy_ui_render", + "bevy_ui_widgets", + "bevy_utils", "bevy_window", + "bevy_world_serialization", "tracing", ] [[package]] name = "bevy_diagnostic" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "atomic-waker", "bevy_app", @@ -913,8 +924,8 @@ dependencies = [ [[package]] name = "bevy_ecs" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "arrayvec", "bevy_ecs_macros", @@ -940,40 +951,68 @@ dependencies = [ [[package]] name = "bevy_ecs_macro_logic" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ - "bevy_macro_utils 0.19.0", + "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "bevy_ecs_macros" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_ecs_macro_logic", - "bevy_macro_utils 0.19.0", + "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "bevy_encase_derive" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ - "bevy_macro_utils 0.19.0", + "bevy_macro_utils", "encase_derive_impl", ] +[[package]] +name = "bevy_extract" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" +dependencies = [ + "bevy_app", + "bevy_camera", + "bevy_derive", + "bevy_ecs", + "bevy_extract_macros", + "bevy_log", + "bevy_platform", + "bevy_reflect", + "bevy_time", + "bevy_utils", +] + +[[package]] +name = "bevy_extract_macros" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "bevy_feathers" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "accesskit", "bevy_a11y", @@ -1003,8 +1042,8 @@ dependencies = [ [[package]] name = "bevy_gilrs" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_ecs", @@ -1018,13 +1057,14 @@ dependencies = [ [[package]] name = "bevy_gizmos" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", "bevy_camera", "bevy_color", + "bevy_curve", "bevy_ecs", "bevy_gizmos_macros", "bevy_input", @@ -1032,6 +1072,7 @@ dependencies = [ "bevy_math", "bevy_mesh", "bevy_reflect", + "bevy_shape", "bevy_time", "bevy_transform", "bevy_utils", @@ -1040,18 +1081,18 @@ dependencies = [ [[package]] name = "bevy_gizmos_macros" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ - "bevy_macro_utils 0.19.0", + "bevy_macro_utils", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "bevy_gizmos_render" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", @@ -1059,6 +1100,7 @@ dependencies = [ "bevy_color", "bevy_core_pipeline", "bevy_ecs", + "bevy_extract", "bevy_gizmos", "bevy_image", "bevy_log", @@ -1069,6 +1111,7 @@ dependencies = [ "bevy_reflect", "bevy_render", "bevy_shader", + "bevy_shape", "bevy_sprite_render", "bevy_transform", "bevy_utils", @@ -1078,8 +1121,8 @@ dependencies = [ [[package]] name = "bevy_gltf" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "async-lock", "base64", @@ -1088,6 +1131,7 @@ dependencies = [ "bevy_asset", "bevy_camera", "bevy_color", + "bevy_curve", "bevy_ecs", "bevy_image", "bevy_light", @@ -1101,20 +1145,20 @@ dependencies = [ "bevy_world_serialization", "fixedbitset", "gltf", - "itertools", + "itertools 0.14.0", "percent-encoding", "serde", "serde_json", "smallvec", "thiserror 2.0.20", "tracing", - "wgpu-types", + "wgpu-types 30.0.1", ] [[package]] name = "bevy_image" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", @@ -1136,13 +1180,13 @@ dependencies = [ "serde", "thiserror 2.0.20", "tracing", - "wgpu-types", + "wgpu-types 30.0.1", ] [[package]] name = "bevy_input" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_ecs", @@ -1157,10 +1201,11 @@ dependencies = [ [[package]] name = "bevy_input_focus" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", + "bevy_camera", "bevy_ecs", "bevy_input", "bevy_math", @@ -1173,8 +1218,8 @@ dependencies = [ [[package]] name = "bevy_internal" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_a11y", "bevy_android", @@ -1188,10 +1233,12 @@ dependencies = [ "bevy_clipboard", "bevy_color", "bevy_core_pipeline", + "bevy_curve", "bevy_derive", "bevy_dev_tools", "bevy_diagnostic", "bevy_ecs", + "bevy_extract", "bevy_feathers", "bevy_gilrs", "bevy_gizmos", @@ -1214,6 +1261,7 @@ dependencies = [ "bevy_render", "bevy_scene", "bevy_shader", + "bevy_shape", "bevy_sprite", "bevy_sprite_render", "bevy_state", @@ -1232,13 +1280,14 @@ dependencies = [ [[package]] name = "bevy_light" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", "bevy_camera", "bevy_color", + "bevy_curve", "bevy_ecs", "bevy_gizmos", "bevy_image", @@ -1247,18 +1296,19 @@ dependencies = [ "bevy_mesh", "bevy_platform", "bevy_reflect", + "bevy_shape", "bevy_transform", "bevy_utils", "half", "smallvec", "tracing", - "wgpu-types", + "wgpu-types 30.0.1", ] [[package]] name = "bevy_log" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "android_log-sys", "bevy_app", @@ -1274,31 +1324,19 @@ dependencies = [ [[package]] name = "bevy_macro_utils" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "toml_edit 0.25.13+spec-1.1.0", -] - -[[package]] -name = "bevy_macro_utils" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7164fe229422295bba15f1885048f34f4660db069aacb895208f996b0c7784fb" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", - "toml_edit 0.25.13+spec-1.1.0", + "syn 3.0.3", + "toml_edit", ] [[package]] name = "bevy_material" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_asset", "bevy_derive", @@ -1314,33 +1352,30 @@ dependencies = [ "thiserror 2.0.20", "tracing", "variadics_please", - "wgpu-types", + "wgpu-types 30.0.1", ] [[package]] name = "bevy_material_macros" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ - "bevy_macro_utils 0.19.0", + "bevy_macro_utils", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "bevy_math" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "approx", - "arrayvec", "bevy_reflect", "derive_more", - "glam 0.32.1", - "itertools", + "glam", "libm", "rand 0.10.2", - "rand_distr", "serde", "thiserror 2.0.20", "variadics_please", @@ -1348,29 +1383,30 @@ dependencies = [ [[package]] name = "bevy_mesh" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", "bevy_derive", "bevy_ecs", - "bevy_encase_derive", "bevy_math", "bevy_mikktspace", "bevy_platform", "bevy_reflect", + "bevy_shape", "bevy_transform", "bitflags 2.13.1", "bytemuck", "derive_more", "encase", - "glam 0.32.1", + "glam", "half", "hexasphere", + "serde", "thiserror 2.0.20", "tracing", - "wgpu-types", + "wgpu-types 30.0.1", ] [[package]] @@ -1382,16 +1418,16 @@ checksum = "bff34eb29ff4b8a8688bc7299f14fb6b597461ca80fec03ed7d22939ab33e48f" [[package]] name = "bevy_naga_reflect" version = "0.2.0" -source = "git+https://github.com/tychedelia/bevy_naga_reflect#3e59d3698281fcf6c56a6478be3ee12049737218" +source = "git+https://github.com/tychedelia/bevy_naga_reflect?branch=bevy-020-fixes#8c50ac2dc36eb0aa463ad7b29a8d979ea5a8adae" dependencies = [ "bevy", - "naga", + "naga 29.0.4", ] [[package]] name = "bevy_pbr" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "arrayvec", "bevy_app", @@ -1402,6 +1438,7 @@ dependencies = [ "bevy_derive", "bevy_diagnostic", "bevy_ecs", + "bevy_extract", "bevy_gltf", "bevy_image", "bevy_light", @@ -1413,6 +1450,7 @@ dependencies = [ "bevy_reflect", "bevy_render", "bevy_shader", + "bevy_shape", "bevy_tasks", "bevy_transform", "bevy_utils", @@ -1427,13 +1465,13 @@ dependencies = [ "static_assertions", "thiserror 2.0.20", "tracing", - "wgpu-types", + "wgpu-types 30.0.1", ] [[package]] name = "bevy_picking" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", @@ -1445,6 +1483,7 @@ dependencies = [ "bevy_mesh", "bevy_platform", "bevy_reflect", + "bevy_shape", "bevy_time", "bevy_transform", "bevy_window", @@ -1455,37 +1494,40 @@ dependencies = [ [[package]] name = "bevy_platform" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ + "bytemuck", "critical-section", "foldhash 0.2.0", "futures-channel", "futures-lite", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "js-sys", "portable-atomic", "portable-atomic-util", "serde", - "spin 0.10.1", + "spin", "wasm-bindgen", "wasm-bindgen-futures", "web-time", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "bevy_post_process" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", "bevy_camera", "bevy_color", "bevy_core_pipeline", + "bevy_curve", "bevy_derive", "bevy_ecs", + "bevy_extract", "bevy_image", "bevy_math", "bevy_platform", @@ -1500,13 +1542,13 @@ dependencies = [ [[package]] name = "bevy_ptr" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" [[package]] name = "bevy_reflect" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "assert_type_match", "bevy_platform", @@ -1518,7 +1560,7 @@ dependencies = [ "downcast-rs 2.0.2", "erased-serde", "foldhash 0.2.0", - "glam 0.32.1", + "glam", "indexmap", "inventory", "petgraph 0.8.3", @@ -1528,26 +1570,26 @@ dependencies = [ "thiserror 2.0.20", "uuid", "variadics_please", - "wgpu-types", + "wgpu-types 30.0.1", ] [[package]] name = "bevy_reflect_derive" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ - "bevy_macro_utils 0.19.0", + "bevy_macro_utils", "indexmap", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", "uuid", ] [[package]] name = "bevy_render" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "async-channel", "bevy_app", @@ -1558,6 +1600,8 @@ dependencies = [ "bevy_diagnostic", "bevy_ecs", "bevy_encase_derive", + "bevy_extract", + "bevy_extract_macros", "bevy_image", "bevy_log", "bevy_material", @@ -1568,6 +1612,7 @@ dependencies = [ "bevy_reflect", "bevy_render_macros", "bevy_shader", + "bevy_shape", "bevy_tasks", "bevy_time", "bevy_transform", @@ -1578,46 +1623,47 @@ dependencies = [ "derive_more", "downcast-rs 2.0.2", "encase", - "glam 0.32.1", + "glam", "image", "indexmap", - "itertools", + "itertools 0.14.0", "js-sys", - "naga", "nonmax", "offset-allocator", "send_wrapper", "smallvec", + "static_assertions", "thiserror 2.0.20", + "tracing", "variadics_please", "wasm-bindgen", "weak-table", "web-sys", - "wgpu", - "wgpu-types", + "wgpu 30.0.1", + "wgpu-types 30.0.1", ] [[package]] name = "bevy_render_macros" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ - "bevy_macro_utils 0.19.0", + "bevy_extract_macros", + "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "bevy_scene" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", "bevy_derive", "bevy_ecs", - "bevy_log", "bevy_platform", "bevy_reflect", "bevy_scene_macros", @@ -1630,76 +1676,51 @@ dependencies = [ [[package]] name = "bevy_scene_macros" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_ecs_macro_logic", - "bevy_macro_utils 0.19.0", + "bevy_macro_utils", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] -name = "bevy_seedling" -version = "0.8.0" -source = "git+https://github.com/CorvusPrudens/bevy_seedling?branch=master#ab4a88b430d216e8614549926e3532bcec5ce2db" +name = "bevy_shader" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ - "bevy_app", "bevy_asset", - "bevy_diagnostic", - "bevy_ecs", - "bevy_log", - "bevy_math", "bevy_platform", "bevy_reflect", - "bevy_seedling_macros", - "bevy_time", - "bevy_transform", "bevy_utils", - "cpal 0.18.1", - "firewheel", - "firewheel-ircam-hrtf", - "rand 0.10.2", - "smallvec", - "symphonia", - "symphonium", -] - -[[package]] -name = "bevy_seedling_macros" -version = "0.8.0" -source = "git+https://github.com/CorvusPrudens/bevy_seedling?branch=master#ab4a88b430d216e8614549926e3532bcec5ce2db" -dependencies = [ - "bevy_macro_utils 0.19.1", - "proc-macro2", - "quote", - "syn 2.0.119", + "serde", + "thiserror 2.0.20", + "tracing", + "wesl 0.4.4", + "wgpu-types 30.0.1", ] [[package]] -name = "bevy_shader" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +name = "bevy_shape" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ - "bevy_asset", - "bevy_platform", + "arrayvec", + "bevy_math", "bevy_reflect", - "bevy_utils", - "naga", - "naga_oil", + "derive_more", + "rand 0.10.2", + "rand_distr", "serde", "thiserror 2.0.20", - "tracing", - "wesl", - "wgpu-naga-bridge", - "wgpu-types", ] [[package]] name = "bevy_sprite" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", @@ -1718,13 +1739,13 @@ dependencies = [ "bevy_window", "radsort", "tracing", - "wgpu-types", + "wgpu-types 30.0.1", ] [[package]] name = "bevy_sprite_render" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", @@ -1733,6 +1754,7 @@ dependencies = [ "bevy_core_pipeline", "bevy_derive", "bevy_ecs", + "bevy_extract", "bevy_image", "bevy_material", "bevy_math", @@ -1741,6 +1763,7 @@ dependencies = [ "bevy_reflect", "bevy_render", "bevy_shader", + "bevy_shape", "bevy_sprite", "bevy_text", "bevy_transform", @@ -1750,13 +1773,15 @@ dependencies = [ "derive_more", "fixedbitset", "nonmax", + "smallvec", + "static_assertions", "tracing", ] [[package]] name = "bevy_state" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_ecs", @@ -1770,18 +1795,18 @@ dependencies = [ [[package]] name = "bevy_state_macros" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ - "bevy_macro_utils 0.19.0", + "bevy_macro_utils", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "bevy_tasks" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "async-channel", "async-executor", @@ -1798,8 +1823,8 @@ dependencies = [ [[package]] name = "bevy_text" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", @@ -1813,21 +1838,20 @@ dependencies = [ "bevy_platform", "bevy_reflect", "bevy_utils", - "parley 0.9.0", + "parley 0.11.1", "serde", "smallvec", "smol_str", "swash", - "sys-locale", "thiserror 2.0.20", "tracing", - "wgpu-types", + "wgpu-types 30.0.1", ] [[package]] name = "bevy_time" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_ecs", @@ -1840,8 +1864,8 @@ dependencies = [ [[package]] name = "bevy_transform" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_ecs", @@ -1856,8 +1880,8 @@ dependencies = [ [[package]] name = "bevy_ui" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "accesskit", "bevy_a11y", @@ -1882,7 +1906,7 @@ dependencies = [ "bevy_utils", "bevy_window", "derive_more", - "parley 0.9.0", + "parley 0.11.1", "smallvec", "swash", "taffy", @@ -1893,9 +1917,10 @@ dependencies = [ [[package]] name = "bevy_ui_render" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ + "bevy_a11y", "bevy_app", "bevy_asset", "bevy_camera", @@ -1903,6 +1928,7 @@ dependencies = [ "bevy_core_pipeline", "bevy_derive", "bevy_ecs", + "bevy_extract", "bevy_image", "bevy_input_focus", "bevy_math", @@ -1920,13 +1946,14 @@ dependencies = [ "bytemuck", "derive_more", "indexmap", + "smallvec", "tracing", ] [[package]] name = "bevy_ui_widgets" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "accesskit", "bevy_a11y", @@ -1938,18 +1965,21 @@ dependencies = [ "bevy_log", "bevy_math", "bevy_picking", + "bevy_platform", "bevy_reflect", "bevy_text", + "bevy_time", "bevy_ui", "bevy_window", - "parley 0.9.0", + "parley 0.11.1", "smol_str", + "web-sys", ] [[package]] name = "bevy_utils" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "async-channel", "bevy_platform", @@ -1960,8 +1990,8 @@ dependencies = [ [[package]] name = "bevy_window" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", @@ -1978,8 +2008,8 @@ dependencies = [ [[package]] name = "bevy_winit" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "accesskit", "accesskit_winit", @@ -2004,14 +2034,14 @@ dependencies = [ "tracing", "wasm-bindgen", "web-sys", - "wgpu-types", + "wgpu-types 30.0.1", "winit", ] [[package]] name = "bevy_world_serialization" -version = "0.19.0" -source = "git+https://github.com/processing/bevy?branch=main#9da7c6250114ee8613ae54ab3ad7bb7e43a2f91b" +version = "0.20.0-dev" +source = "git+https://github.com/processing/bevy?branch=processing-batches#97dc507fda302ef87c434c7a0d0f06e22fc846fb" dependencies = [ "bevy_app", "bevy_asset", @@ -2029,29 +2059,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "bindgen" -version = "0.65.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" -dependencies = [ - "bitflags 1.3.2", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex 1.3.0", - "syn 2.0.119", - "which", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -2070,6 +2077,15 @@ dependencies = [ "bit-vec 0.9.1", ] +[[package]] +name = "bit-set" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2f926cc3060f09db9ebc5b52823d85268d24bb917e472c0c4bea35780a7d" +dependencies = [ + "bit-vec 0.9.1", +] + [[package]] name = "bit-vec" version = "0.8.0" @@ -2113,6 +2129,18 @@ dependencies = [ "no_std_io2", ] +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake3" version = "1.8.6" @@ -2122,17 +2150,11 @@ dependencies = [ "arrayref", "arrayvec", "cc", - "cfg-if 1.0.4", + "cfg-if", "constant_time_eq", "cpufeatures 0.3.0", ] -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - [[package]] name = "block-buffer" version = "0.10.4" @@ -2277,7 +2299,7 @@ dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex 2.0.1", + "shlex", ] [[package]] @@ -2286,21 +2308,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom 7.1.3", -] - -[[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.4" @@ -2319,22 +2326,11 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ - "cfg-if 1.0.4", + "cfg-if", "cpufeatures 0.3.0", "rand_core 0.10.1", ] -[[package]] -name = "clang-sys" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" -dependencies = [ - "glob", - "libc", - "libloading 0.8.9", -] - [[package]] name = "clap" version = "4.6.6" @@ -2363,69 +2359,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] -name = "cmake" -version = "0.1.58" +name = "clipboard-win" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" dependencies = [ - "cc", + "error-code", ] [[package]] -name = "cocoa" -version = "0.20.2" +name = "codespan-reporting" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c49e86fc36d5704151f5996b7b3795385f50ce09e3be0f47a0cfde869681cf8" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.7.0", - "core-graphics 0.19.2", - "foreign-types 0.3.2", - "libc", - "objc", + "serde", + "termcolor", + "unicode-width", ] [[package]] -name = "cocoa-foundation" -version = "0.2.1" +name = "color_quant" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" -dependencies = [ - "bitflags 2.13.1", - "block", - "core-foundation 0.10.1", - "core-graphics-types 0.2.0", - "objc", -] - -[[package]] -name = "codespan-reporting" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - -[[package]] -name = "codespan-reporting" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" @@ -2459,7 +2416,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "cfg-if 1.0.4", + "cfg-if", "wasm-bindgen", ] @@ -2508,60 +2465,22 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "core-foundation" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" -dependencies = [ - "core-foundation-sys 0.7.0", - "libc", -] - [[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "core-foundation-sys 0.8.7", + "core-foundation-sys", "libc", ] -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys 0.8.7", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" - [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core-graphics" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.7.0", - "foreign-types 0.3.2", - "libc", -] - [[package]] name = "core-graphics" version = "0.23.2" @@ -2569,9 +2488,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "foreign-types 0.5.0", + "core-foundation", + "core-graphics-types", + "foreign-types", "libc", ] @@ -2582,46 +2501,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.4", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.13.1", - "core-foundation 0.10.1", + "core-foundation", "libc", ] -[[package]] -name = "core-media-sys" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "273bf3fc5bf51fd06a7766a84788c1540b6527130a0bce39e00567d6ab9f31f1" -dependencies = [ - "cfg-if 0.1.10", - "core-foundation-sys 0.7.0", - "libc", -] - -[[package]] -name = "core-video-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" -dependencies = [ - "cfg-if 0.1.10", - "core-foundation-sys 0.7.0", - "core-graphics 0.19.2", - "libc", - "metal", - "objc", -] - [[package]] name = "core_maths" version = "0.1.1" @@ -2645,27 +2528,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "coremidi" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a57ede822fdaf19280cf1320a5a5d3a522c75c910d01750af1e8122b6ad2595b" -dependencies = [ - "block2 0.6.2", - "core-foundation 0.10.1", - "core-foundation-sys 0.8.7", - "coremidi-sys", -] - -[[package]] -name = "coremidi-sys" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2b8c2cefa9a8f712213c5a1383ffe428efc8f1a1fd1e2f757be94daf7e256a" -dependencies = [ - "core-foundation-sys 0.8.7", -] - [[package]] name = "cpal" version = "0.17.3" @@ -2678,38 +2540,7 @@ dependencies = [ "jni 0.21.1", "js-sys", "libc", - "mach2 0.5.0", - "ndk", - "ndk-context", - "num-derive", - "num-traits", - "objc2 0.6.4", - "objc2-audio-toolbox", - "objc2-avf-audio", - "objc2-core-audio", - "objc2-core-audio-types", - "objc2-core-foundation", - "objc2-foundation 0.3.2", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows 0.62.2", -] - -[[package]] -name = "cpal" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f77b11176c37874be37e8d691c946e31b2b8c357abce9526f6a99eb469e1028" -dependencies = [ - "alsa", - "block2 0.6.2", - "coreaudio-rs", - "dasp_sample", - "jni 0.22.4", - "js-sys", - "libc", - "mach2 0.6.0", + "mach2", "ndk", "ndk-context", "num-derive", @@ -2725,7 +2556,6 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "windows 0.62.2", - "windows-core 0.62.2", ] [[package]] @@ -2752,7 +2582,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "cfg-if 1.0.4", + "cfg-if", ] [[package]] @@ -2837,7 +2667,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3aa12038120eb13347a6ae2ffab1d34efe78150125108627fd85044dd4d6ff1e" dependencies = [ - "libloading 0.8.9", + "libloading", ] [[package]] @@ -2852,12 +2682,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" -[[package]] -name = "data-encoding" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" - [[package]] name = "derive_more" version = "2.1.1" @@ -2932,17 +2756,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ - "libloading 0.8.9", -] - -[[package]] -name = "dlopen2" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60768c353d76ba6dd1b6332a5dbbdd3bc2dddadc2935f4cc6a9d0f17ca8ecb6a" -dependencies = [ - "libc", - "windows-sys 0.61.2", + "libloading", ] [[package]] @@ -2972,12 +2786,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "either" version = "1.17.0" @@ -2995,9 +2803,9 @@ dependencies = [ [[package]] name = "encase" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3e0ff2ee0b7aa97428308dd9e1e42369cb22f5fb8dc1c55546637443a60f1e" +checksum = "fdf6312703f02e0625e1750eadf556054b6da5e44960a3a771e9127cb357dbd8" dependencies = [ "const_panic", "encase_derive", @@ -3006,22 +2814,22 @@ dependencies = [ [[package]] name = "encase_derive" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4d90c5d7d527c6cb8a3b114efd26a6304d9ab772656e73d8f4e32b1f3d601a2" +checksum = "8429e24f02acf09b0fb3c4560c329a1a79fab14bbb9c2d632c073d0a61b0816a" dependencies = [ "encase_derive_impl", ] [[package]] name = "encase_derive_impl" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8bad72d8308f7a382de2391ec978ddd736e0103846b965d7e2a63a75768af30" +checksum = "3ac3bf06563c72a9b64bc4d8da00ebcabd1d776ea4526c3b1461697142fe340f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -3071,6 +2879,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + [[package]] name = "euclid" version = "0.22.14" @@ -3117,12 +2931,6 @@ dependencies = [ "zune-inflate", ] -[[package]] -name = "extended" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" - [[package]] name = "fastrand" version = "2.5.0" @@ -3159,166 +2967,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" -[[package]] -name = "firewheel" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cce6a0e2684404563f22daf00caa3672a9fd1503af5c58cd79a544a8221cec6e" -dependencies = [ - "firewheel-core", - "firewheel-cpal", - "firewheel-graph", - "firewheel-nodes", - "firewheel-rtaudio", - "firewheel-symphonium", - "thiserror 2.0.20", -] - -[[package]] -name = "firewheel-core" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c793ba83a0c4861d467af68844970bd063249189790f00c7feda5019732466b" -dependencies = [ - "arrayvec", - "audioadapter", - "audioadapter-buffers", - "audioadapter-sample", - "bevy_ecs", - "bevy_platform", - "bevy_reflect", - "bitflags 2.13.1", - "firewheel-macros", - "glam 0.29.3", - "glam 0.30.10", - "glam 0.31.1", - "glam 0.32.1", - "num-traits", - "portable-atomic", - "ringbuf", - "smallvec", - "thiserror 2.0.20", - "thunderdome", - "wmidi", -] - -[[package]] -name = "firewheel-cpal" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cf893a68d8b89fbb6eeca6deb5166fd342cc827bf4ad37d31a973bc861bf038" -dependencies = [ - "audioadapter-buffers", - "bevy_platform", - "cpal 0.18.1", - "firewheel-core", - "firewheel-graph", - "fixed-resample", - "thiserror 2.0.20", - "tracing", -] - -[[package]] -name = "firewheel-graph" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00257afdba7edc48ce1667fc144d453f7bd17674a78450207507ca286e818b82" -dependencies = [ - "arrayvec", - "audioadapter", - "bevy_platform", - "bevy_reflect", - "bitflags 2.13.1", - "firewheel-core", - "num-traits", - "ringbuf", - "smallvec", - "thiserror 2.0.20", - "thunderdome", - "tracing", - "triple_buffer", - "wasm-bindgen", -] - -[[package]] -name = "firewheel-ircam-hrtf" -version = "0.5.0" -source = "git+https://github.com/CorvusPrudens/bevy_seedling?branch=master#ab4a88b430d216e8614549926e3532bcec5ce2db" -dependencies = [ - "bevy_ecs", - "bevy_reflect", - "firewheel", - "glam 0.32.1", - "hrtf", -] - -[[package]] -name = "firewheel-macros" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19ca164d0116746e81d86266ad78d0d57e1be18437439ca6c4c8a97160bec2f" -dependencies = [ - "bevy_macro_utils 0.19.1", - "proc-macro2", - "quote", - "syn 2.0.119", - "toml_edit 0.23.10+spec-1.0.0", -] - -[[package]] -name = "firewheel-nodes" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f4d58cfbb1c65d19e4490ed81b2e16153ab5e355535ef79aef044583adfcba" -dependencies = [ - "bevy_ecs", - "bevy_platform", - "bevy_reflect", - "firewheel-core", - "num-traits", - "smallvec", - "thiserror 2.0.20", - "triple_buffer", -] - -[[package]] -name = "firewheel-rtaudio" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a136e3893d1c1b691f355ba7b535e0a7df16ad49b7788aa88805bf473105a755" -dependencies = [ - "audioadapter-buffers", - "bevy_platform", - "firewheel-core", - "firewheel-graph", - "rtaudio", - "thiserror 2.0.20", - "tracing", -] - -[[package]] -name = "firewheel-symphonium" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8261282ec848bcd8e64c715fb6d3778e2208819b1c5c015f04bc8709ac5465f" -dependencies = [ - "bevy_platform", - "firewheel-core", - "symphonium", -] - -[[package]] -name = "fixed-resample" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3505607cc0adc388ce98f59707477c02fbd05a2fa0c1b8c4cf42b71a3cb9c34" -dependencies = [ - "audioadapter", - "audioadapter-buffers", - "ringbuf", - "rubato 4.0.0", -] - [[package]] name = "fixedbitset" version = "0.5.7" @@ -3341,18 +2989,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" -[[package]] -name = "flume" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" -dependencies = [ - "futures-core", - "futures-sink", - "nanorand", - "spin 0.9.9", -] - [[package]] name = "fnv" version = "1.0.7" @@ -3380,15 +3016,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "font-types" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" -dependencies = [ - "bytemuck", -] - [[package]] name = "font-types" version = "0.12.3" @@ -3423,27 +3050,18 @@ dependencies = [ [[package]] name = "fontique" -version = "0.9.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c20b425addb8661e97fe1d51c4d8bcec3ec29ed6ad0db983976a7521276b8f7" +checksum = "6688bc1294fe7117d788937b6c53480169b29c566954af490830d4c09da9516a" dependencies = [ "hashbrown 0.17.1", "linebender_resource_handle", "memmap2", "parlance", - "read-fonts 0.39.2", + "read-fonts 0.41.0", "smallvec", ] -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared 0.1.1", -] - [[package]] name = "foreign-types" version = "0.5.0" @@ -3451,7 +3069,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared 0.3.1", + "foreign-types-shared", ] [[package]] @@ -3465,12 +3083,6 @@ dependencies = [ "syn 3.0.3", ] -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -3486,6 +3098,12 @@ dependencies = [ "libc", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures-channel" version = "0.3.34" @@ -3531,12 +3149,6 @@ dependencies = [ "syn 3.0.3", ] -[[package]] -name = "futures-sink" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" - [[package]] name = "futures-task" version = "0.3.34" @@ -3589,7 +3201,7 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "cfg-if 1.0.4", + "cfg-if", "js-sys", "libc", "wasi", @@ -3602,7 +3214,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ - "cfg-if 1.0.4", + "cfg-if", "js-sys", "libc", "r-efi 5.3.0", @@ -3616,7 +3228,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ - "cfg-if 1.0.4", + "cfg-if", "js-sys", "libc", "r-efi 6.0.0", @@ -3681,28 +3293,11 @@ dependencies = [ [[package]] name = "glam" -version = "0.29.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee" - -[[package]] -name = "glam" -version = "0.30.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9" - -[[package]] -name = "glam" -version = "0.31.1" +version = "0.33.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556f6b2ea90b8d15a74e0e7bb41671c9bdf38cd9f78c284d750b9ce58a2b5be7" - -[[package]] -name = "glam" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0" +checksum = "37bfe73dc8ec21f54d181e5a60554327ec3f1c600c455a75ccbfc65d2106a6c4" dependencies = [ + "approx", "bytemuck", "encase", "libm", @@ -3734,12 +3329,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "glob" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" - [[package]] name = "glow" version = "0.17.0" @@ -3842,20 +3431,13 @@ dependencies = [ "bitflags 2.13.1", ] -[[package]] -name = "grid" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5" - [[package]] name = "guillotiere" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" dependencies = [ "euclid", - "svg_fmt", ] [[package]] @@ -3865,7 +3447,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "bytemuck", - "cfg-if 1.0.4", + "cfg-if", "crunchy", "num-traits", "serde", @@ -3887,14 +3469,13 @@ dependencies = [ [[package]] name = "harfrust" -version = "0.6.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551ed25397e4b444e89686602877d5cf3a7f6e3d548dcac37a8357d1e195f4df" +checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" dependencies = [ "bitflags 2.13.1", "bytemuck", - "core_maths", - "read-fonts 0.39.2", + "read-fonts 0.41.0", "smallvec", ] @@ -3935,7 +3516,10 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ + "equivalent", "foldhash 0.2.0", + "serde", + "serde_core", ] [[package]] @@ -3963,12 +3547,12 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hexasphere" -version = "18.0.0" +version = "19.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177ea6330876de4ef08e184f827b98acc037614a4ce409902c5b0d53a9c2e951" +checksum = "163385eff5a0cf606ebe2c10b3a44ab02f4306e83fbd068a64f5e1d72a3049ac" dependencies = [ "constgebra", - "glam 0.32.1", + "glam", "tinyvec", ] @@ -3978,26 +3562,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "hrtf" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4de47a84fd55fa33aa5ef337016814fdc869fdad23e7898b5322fa290248e6" -dependencies = [ - "byteorder", - "rubato 0.14.1", - "rustfft", -] - [[package]] name = "icu_collections" version = "2.3.0" @@ -4240,19 +3804,19 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.18" +name = "itertools" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] [[package]] -name = "java-locator" -version = "0.1.9" +name = "itoa" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c46c1fe465c59b1474e665e85e1256c3893dd00927b8d55f63b09044c1e64f" -dependencies = [ - "glob", -] +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" @@ -4261,11 +3825,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", - "cfg-if 1.0.4", + "cfg-if", "combine", - "java-locator", "jni-sys 0.3.1", - "libloading 0.7.4", "log", "thiserror 1.0.69", "walkdir", @@ -4278,7 +3840,7 @@ version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cfg-if 1.0.4", + "cfg-if", "combine", "jni-macros", "jni-sys 0.4.1", @@ -4302,21 +3864,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "jni-min-helper" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913370a00ba1851b0f3369b49d0f3e745da2d20d3f15ecc296a64234ccda45f7" -dependencies = [ - "android-build", - "dlopen2", - "jni 0.21.1", - "libc", - "log", - "ndk-context", - "process_path", -] - [[package]] name = "jni-sys" version = "0.3.1" @@ -4361,7 +3908,7 @@ version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ - "cfg-if 1.0.4", + "cfg-if", "futures-util", "wasm-bindgen", ] @@ -4382,7 +3929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading 0.8.9", + "libloading", "pkg-config", ] @@ -4430,13 +3977,34 @@ dependencies = [ "ascii-canvas", "bit-set 0.8.0", "ena", - "itertools", - "lalrpop-util", + "itertools 0.14.0", + "lalrpop-util 0.22.2", "petgraph 0.7.1", "regex", "regex-syntax", "sha3", - "string_cache", + "string_cache 0.8.9", + "term", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a80a963123205c7157323c99611bc4abb65dcbd62ef46dc4bac74a3941bc75" +dependencies = [ + "ascii-canvas", + "bit-set 0.9.1", + "ena", + "itertools 0.14.0", + "lalrpop-util 0.23.1", + "petgraph 0.8.3", + "regex", + "regex-syntax", + "sha3", + "string_cache 0.9.0", "term", "unicode-xid", "walkdir", @@ -4453,16 +4021,19 @@ dependencies = [ ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "lalrpop-util" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "884f3e747ed2dcee867cda1b0c31a048f9e20de2d916a248949319921a2e666e" +dependencies = [ + "regex-automata", +] [[package]] -name = "lazycell" -version = "1.3.0" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lebe" @@ -4563,23 +4134,13 @@ dependencies = [ "cc", ] -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if 1.0.4", - "winapi", -] - [[package]] name = "libloading" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ - "cfg-if 1.0.4", + "cfg-if", "windows-link", ] @@ -4758,21 +4319,6 @@ dependencies = [ "libc", ] -[[package]] -name = "mach2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "matchers" version = "0.2.0" @@ -4788,7 +4334,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" dependencies = [ - "cfg-if 1.0.4", + "cfg-if", "rayon", ] @@ -4807,48 +4353,6 @@ dependencies = [ "libc", ] -[[package]] -name = "metal" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e198a0ee42bdbe9ef2c09d0b9426f3b2b47d90d93a4a9b0395c4cea605e92dc0" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa", - "core-graphics 0.19.2", - "foreign-types 0.3.2", - "log", - "objc", -] - -[[package]] -name = "midir" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c12e74a8604bc07f9a3fcf3d5889a81bc96ca6e07d6a114d63fc0371f3e5a4" -dependencies = [ - "alsa", - "cc", - "coremidi", - "jni 0.21.1", - "jni-min-helper", - "js-sys", - "libc", - "log", - "ndk-context", - "parking_lot", - "wasm-bindgen", - "web-sys", - "windows 0.62.2", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -4882,29 +4386,10 @@ dependencies = [ ] [[package]] -name = "mozjpeg" -version = "0.10.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7891b80aaa86097d38d276eb98b3805d6280708c4e0a1e6f6aed9380c51fec9" -dependencies = [ - "arrayvec", - "bytemuck", - "libc", - "mozjpeg-sys", - "rgb", -] - -[[package]] -name = "mozjpeg-sys" -version = "2.2.3" +name = "mutants" +version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f0dc668bf9bf888c88e2fb1ab16a406d2c380f1d082b20d51dd540ab2aa70c1" -dependencies = [ - "cc", - "dunce", - "libc", - "nasm-rs", -] +checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126" [[package]] name = "naga" @@ -4915,9 +4400,9 @@ dependencies = [ "arrayvec", "bit-set 0.9.1", "bitflags 2.13.1", - "cfg-if 1.0.4", + "cfg-if", "cfg_aliases", - "codespan-reporting 0.13.1", + "codespan-reporting", "half", "hashbrown 0.16.1", "hexf-parse", @@ -4926,8 +4411,7 @@ dependencies = [ "log", "num-traits", "once_cell", - "pp-rs", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "spirv", "thiserror 2.0.20", @@ -4935,63 +4419,42 @@ dependencies = [ ] [[package]] -name = "naga_oil" -version = "0.22.0" +name = "naga" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "319f03062940ed85c03da020042618356bb8ca5263020322930883b05e30208e" +checksum = "a616d2fb8c89516ac2723a581f69d6c18576046bed761bd6b305e5618e6ae130" dependencies = [ - "codespan-reporting 0.12.0", - "data-encoding", + "arrayvec", + "bit-set 0.10.0", + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.17.1", "indexmap", - "naga", - "regex", - "rustc-hash", + "libm", + "log", + "naga-types", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", "thiserror 2.0.20", - "tracing", "unicode-ident", ] [[package]] -name = "nannou_midi" -version = "0.1.0" -source = "git+https://github.com/nannou-org/nannou?branch=bevy-refactor#f713c439c03b4e4614c2ef8ff899932efd0a1c44" -dependencies = [ - "bevy", - "crossbeam-channel", - "midir", -] - -[[package]] -name = "nannou_webcam" -version = "0.1.0" -source = "git+https://github.com/nannou-org/nannou?branch=bevy-refactor#f713c439c03b4e4614c2ef8ff899932efd0a1c44" -dependencies = [ - "bevy", - "console_error_panic_hook", - "flume", - "getrandom 0.3.4", - "nokhwa", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "nanorand" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "nasm-rs" -version = "0.3.2" +name = "naga-types" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "706bf8a5e8c8ddb99128c3291d31bd21f4bcde17f0f4c20ec678d85c74faa149" +checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49" dependencies = [ - "jobserver", - "log", + "hashbrown 0.17.1", + "indexmap", + "rustc-hash 1.1.0", + "serde", + "thiserror 2.0.20", ] [[package]] @@ -5037,7 +4500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ "bitflags 2.13.1", - "cfg-if 1.0.4", + "cfg-if", "cfg_aliases", "libc", ] @@ -5051,83 +4514,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "nokhwa" -version = "0.10.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d63f10b450319a0ace7aa8e0e25477d1fdb345313a97e220e886175539a1dbb" -dependencies = [ - "flume", - "image", - "nokhwa-bindings-linux", - "nokhwa-bindings-macos", - "nokhwa-bindings-windows", - "nokhwa-core", - "paste", - "thiserror 2.0.20", -] - -[[package]] -name = "nokhwa-bindings-linux" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb67e22201a53322291740ca064b20eaaade7222ef0349f312d9b37b004e1984" -dependencies = [ - "libc", - "nokhwa-core", - "v4l", -] - -[[package]] -name = "nokhwa-bindings-macos" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f70d3908ea68324e44a6b3a0f885aa59e433fb1f6678839d09e0df7d226fb42d" -dependencies = [ - "block", - "cocoa-foundation", - "core-foundation 0.10.1", - "core-media-sys", - "core-video-sys", - "flume", - "nokhwa-core", - "objc", - "once_cell", -] - -[[package]] -name = "nokhwa-bindings-windows" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be28886bad8abcec3655c1f24b965b4cb596a72b23164c910c54439ce55d2a4" -dependencies = [ - "nokhwa-core", - "once_cell", - "windows 0.62.2", -] - -[[package]] -name = "nokhwa-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1cba20bebd3bd9ae22f9273ade5bbe49da3e047c8512b53fbaf8b4b9c80d496" -dependencies = [ - "bytes", - "image", - "mozjpeg", - "thiserror 2.0.20", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nom" version = "8.0.0" @@ -5296,16 +4682,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", - "objc_exception", -] - [[package]] name = "objc-sys" version = "0.3.5" @@ -5389,7 +4765,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" dependencies = [ - "bitflags 2.13.1", "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -5672,6 +5047,7 @@ dependencies = [ "bitflags 2.13.1", "objc2 0.6.4", "objc2-core-foundation", + "objc2-core-graphics", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", ] @@ -5731,15 +5107,6 @@ dependencies = [ "objc2-foundation 0.2.2", ] -[[package]] -name = "objc_exception" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" -dependencies = [ - "cc", -] - [[package]] name = "offset-allocator" version = "0.2.0" @@ -5790,6 +5157,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "owned_ttf_parser" version = "0.25.1" @@ -5821,7 +5198,7 @@ version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "cfg-if 1.0.4", + "cfg-if", "libc", "redox_syscall 0.5.18", "smallvec", @@ -5850,12 +5227,12 @@ dependencies = [ [[package]] name = "parley" -version = "0.9.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fad031076f48f0d4d85ce1aea9b94b4e715a4d636a030a123038f8f5b5e4343" +checksum = "22d2ff88bd3f7d68d1d9b09c7e6209f9a8e8c05088295140a2bcf2e9b17038c5" dependencies = [ - "fontique 0.9.0", - "harfrust 0.6.2", + "fontique 0.11.1", + "harfrust 0.12.0", "hashbrown 0.17.1", "icu_normalizer", "icu_properties", @@ -5863,14 +5240,14 @@ dependencies = [ "linebender_resource_handle", "parlance", "parley_data", - "skrifa 0.42.1", + "skrifa 0.44.0", ] [[package]] name = "parley_data" -version = "0.9.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ab9ace3fad1b9ed603ddac5b595e69931fc50263d7e04e4055015b77b02da5" +checksum = "1567535334d6ba2d3cde19221ba9a7bd0fabb3cbd99046ddfb10ae061cfcc889" dependencies = [ "icu_properties", ] @@ -5887,12 +5264,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "percent-encoding" version = "2.3.2" @@ -5926,7 +5297,16 @@ dependencies = [ name = "phf_shared" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ "siphasher", ] @@ -5999,7 +5379,7 @@ version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ - "cfg-if 1.0.4", + "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", @@ -6033,15 +5413,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "pp-rs" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb458bb7f6e250e6eb79d5026badc10a3ebb8f9a15d1fff0f13d17c71f4d6dee" -dependencies = [ - "unicode-xid", -] - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -6063,32 +5434,13 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.119", -] - -[[package]] -name = "primal-check" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" -dependencies = [ - "num-integer", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.13+spec-1.1.0", + "toml_edit", ] [[package]] @@ -6100,30 +5452,17 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "process_path" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f676f11eb0b3e2ea0fbaee218fa6b806689e2297b8c8adc5bf73df465c4f6171" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "processing" version = "0.0.8" dependencies = [ "bevy", "js-sys", - "processing_audio", "processing_core", "processing_cuda", "processing_glfw", "processing_input", - "processing_midi", "processing_render", - "processing_webcam", "rand 0.10.2", "tracing", "tracing-subscriber", @@ -6131,14 +5470,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "processing_audio" -version = "0.1.0" -dependencies = [ - "bevy", - "bevy_seedling", -] - [[package]] name = "processing_core" version = "0.0.5" @@ -6189,15 +5520,6 @@ dependencies = [ "processing_core", ] -[[package]] -name = "processing_midi" -version = "0.0.5" -dependencies = [ - "bevy", - "nannou_midi", - "processing_core", -] - [[package]] name = "processing_pyo3" version = "0.0.8" @@ -6209,7 +5531,6 @@ dependencies = [ "processing_cuda", "processing_glfw", "processing_render", - "processing_webcam", "pyo3", "rand 0.10.2", "rand_distr", @@ -6226,7 +5547,7 @@ dependencies = [ "half", "js-sys", "lyon", - "naga", + "naga 29.0.4", "notosans", "objc2 0.6.4", "objc2-app-kit 0.3.2", @@ -6238,8 +5559,8 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "wesl", - "wgpu", + "wesl 0.3.2", + "wgpu 30.0.1", "windows 0.58.0", ] @@ -6259,16 +5580,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "processing_webcam" -version = "0.0.5" -dependencies = [ - "bevy", - "nannou_webcam", - "processing_core", - "processing_render", -] - [[package]] name = "profiling" version = "1.0.18" @@ -6295,7 +5606,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" dependencies = [ "bytemuck", - "cfg-if 1.0.4", + "cfg-if", "libm", "num-complex", "paste", @@ -6426,6 +5737,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "radsort" version = "0.1.1" @@ -6508,9 +5825,9 @@ dependencies = [ "av1-grain", "bitstream-io", "built", - "cfg-if 1.0.4", + "cfg-if", "interpolate_name", - "itertools", + "itertools 0.14.0", "libc", "libfuzzer-sys", "log", @@ -6602,16 +5919,6 @@ dependencies = [ "font-types 0.10.1", ] -[[package]] -name = "read-fonts" -version = "0.39.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" -dependencies = [ - "bytemuck", - "font-types 0.11.3", -] - [[package]] name = "read-fonts" version = "0.41.0" @@ -6623,15 +5930,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "realfft" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" -dependencies = [ - "rustfft", -] - [[package]] name = "reborrow" version = "0.5.5" @@ -6694,12 +5992,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" - [[package]] name = "regex-syntax" version = "0.8.11" @@ -6717,20 +6009,6 @@ name = "rgb" version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "ringbuf" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a158e09ede21a14b172ca6cdd6208386c6ae2cb6acef58d774368ef8c450dfa7" -dependencies = [ - "crossbeam-utils", - "portable-atomic", - "portable-atomic-util", -] [[package]] name = "rodio" @@ -6738,7 +6016,7 @@ version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a536bb79db59098ef71a4dd4246c02eb87b316deceb1b68e0cde7167ec01eb" dependencies = [ - "cpal 0.17.3", + "cpal", "dasp_sample", "lewton", "num-rational", @@ -6770,60 +6048,16 @@ dependencies = [ ] [[package]] -name = "rtaudio" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7384a8837efe4abea0e53f3887b5072cbbd1ee95c298cff4fbd7a9f3ff23a48b" -dependencies = [ - "bitflags 2.13.1", - "rtaudio-sys", - "thiserror 2.0.20", - "tracing", -] - -[[package]] -name = "rtaudio-sys" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "931ec9a72c48e96e8e2f9868aef1d4e452723354779d01159f7399647427b57b" -dependencies = [ - "cmake", - "pkg-config", -] - -[[package]] -name = "rubato" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6dd52e80cfc21894deadf554a5673002938ae4625f7a283e536f9cf7c17b0d5" -dependencies = [ - "num-complex", - "num-integer", - "num-traits", - "realfft", -] - -[[package]] -name = "rubato" -version = "4.0.0" +name = "rustc-hash" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f57c655d11e929f05a8663b323ff553f8d9773be05dfdc087795955bedeb8d92" -dependencies = [ - "audioadapter", - "audioadapter-buffers", - "num-complex", - "num-integer", - "num-traits", - "realfft", - "visibility", - "windowfunctions", -] +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -6834,20 +6068,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustfft" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" -dependencies = [ - "num-complex", - "num-integer", - "num-traits", - "primal-check", - "strength_reduce", - "transpose", -] - [[package]] name = "rustix" version = "0.38.44" @@ -7026,12 +6246,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "shlex" version = "2.0.1" @@ -7085,16 +6299,6 @@ dependencies = [ "read-fonts 0.35.0", ] -[[package]] -name = "skrifa" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" -dependencies = [ - "bytemuck", - "read-fonts 0.39.2", -] - [[package]] name = "skrifa" version = "0.44.0" @@ -7162,18 +6366,9 @@ dependencies = [ [[package]] name = "spin" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spin" -version = "0.10.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" +checksum = "0134f9043ed38b087ac4f7d4af44c79e2c9e5094421fe3164f435ce585953b10" dependencies = [ "portable-atomic", ] @@ -7208,12 +6403,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "strength_reduce" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" - [[package]] name = "strict-num" version = "0.1.1" @@ -7228,21 +6417,27 @@ checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared", + "phf_shared 0.11.3", "precomputed-hash", ] [[package]] -name = "strsim" -version = "0.11.1" +name = "string_cache" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] [[package]] -name = "svg_fmt" -version = "0.4.5" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "swash" @@ -7255,116 +6450,6 @@ dependencies = [ "zeno", ] -[[package]] -name = "symphonia" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7edef6a96b696d4e0cab5ee9ebb7ca155ed95f30a6b45bbb8b97d2727f02424" -dependencies = [ - "lazy_static", - "symphonia-codec-pcm", - "symphonia-codec-vorbis", - "symphonia-core", - "symphonia-format-ogg", - "symphonia-format-riff", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-codec-pcm" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04ba75686acbe43542fdd374571195f0530c0b7785ca25cc6840e9c6c4b6eea" -dependencies = [ - "log", - "symphonia-core", -] - -[[package]] -name = "symphonia-codec-vorbis" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73d90b4fcf796137cc683c538282804ff9629f8ad9dbfd881fcbba331ac4e986" -dependencies = [ - "log", - "symphonia-common", - "symphonia-core", -] - -[[package]] -name = "symphonia-common" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acc3fcc18ec9b8cdd48614e259c4cf0d27b71d41e5d9b120b42c5adab12d7c4" -dependencies = [ - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-core" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c412864d599d4750d0c3d684d7e093ec05e5309681ef5252cc1096a437f6e0" -dependencies = [ - "bitflags 2.13.1", - "bytemuck", - "lazy_static", - "log", - "num-complex", - "rustfft", - "smallvec", -] - -[[package]] -name = "symphonia-format-ogg" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5495e7f7e3c7035328d82b6d6e377eef289bb0c4105bdeb557fc93a833f994" -dependencies = [ - "log", - "symphonia-common", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-format-riff" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff70929083a8c1a5f6cd7c904b6071c7914ad04739b510c2f7239dfc9b7dabe" -dependencies = [ - "extended", - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-metadata" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83713a97705d77bdef7cdbc0768fd6e5a54e4cd7e48d60a806ae85639e2c87c6" -dependencies = [ - "lazy_static", - "log", - "regex-lite", - "smallvec", - "symphonia-core", -] - -[[package]] -name = "symphonium" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cc514081a8a6eb44a4ee5e68d981bf11d976876d0612082121969b31e7d53a8" -dependencies = [ - "fixed-resample", - "symphonia", - "tracing", -] - [[package]] name = "syn" version = "2.0.119" @@ -7398,20 +6483,11 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "sys-locale" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" -dependencies = [ - "libc", -] - [[package]] name = "sysinfo" -version = "0.38.4" +version = "0.39.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" dependencies = [ "libc", "memchr", @@ -7423,16 +6499,22 @@ dependencies = [ [[package]] name = "taffy" -version = "0.10.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aea22054047c16c3f34d3ac473a2170be1424b1115b2a3adcf28cfb067c88859" +checksum = "639627c87f43b9181c811f40a6296409e093a17bc761214cba3c15df74f86b99" dependencies = [ "arrayvec", - "grid", "serde", "slotmap", + "smallvec", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-lexicon" version = "0.13.5" @@ -7514,16 +6596,10 @@ dependencies = [ name = "thread_local" version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" -dependencies = [ - "cfg-if 1.0.4", -] - -[[package]] -name = "thunderdome" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e170f93360bf9ae6fe3c31116bbf27adb1d054cedd6bc3d7857e34f2d98d0b" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] [[package]] name = "tiff" @@ -7548,7 +6624,7 @@ dependencies = [ "arrayref", "arrayvec", "bytemuck", - "cfg-if 1.0.4", + "cfg-if", "log", "tiny-skia-path", ] @@ -7623,18 +6699,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" -dependencies = [ - "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "winnow 0.7.15", -] - [[package]] name = "toml_edit" version = "0.25.13+spec-1.1.0" @@ -7712,7 +6776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76902d2a8d5f9f55a81155c08971734071968c90f2d9bfe645fe700579b2950" dependencies = [ "cc", - "cfg-if 1.0.4", + "cfg-if", "tracing-core", "tracing-subscriber", ] @@ -7747,22 +6811,14 @@ dependencies = [ ] [[package]] -name = "transpose" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" -dependencies = [ - "num-integer", - "strength_reduce", -] - -[[package]] -name = "triple_buffer" -version = "9.0.0" +name = "tree_magic_mini" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e875ab7068a75b74f419da453927e05527c36f0001b3c7aad3ce443640683487" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" dependencies = [ - "crossbeam-utils", + "memchr", + "nom", + "petgraph 0.8.3", ] [[package]] @@ -7819,6 +6875,17 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsynn" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501a7adf1a4bd9951501e5c66621e972ef8874d787628b7f90e64f936ef7ec0a" +dependencies = [ + "mutants", + "proc-macro2", + "rustc-hash 2.1.3", +] + [[package]] name = "utf8_iter" version = "1.0.4" @@ -7843,26 +6910,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "v4l" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8fbfea44a46799d62c55323f3c55d06df722fbe577851d848d328a1041c3403" -dependencies = [ - "bitflags 1.3.2", - "libc", - "v4l2-sys-mit", -] - -[[package]] -name = "v4l2-sys-mit" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6779878362b9bacadc7893eac76abe69612e8837ef746573c4a5239daf11990b" -dependencies = [ - "bindgen", -] - [[package]] name = "v_frame" version = "0.3.9" @@ -7882,13 +6929,12 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "variadics_please" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" +checksum = "b53d0f7f2d0182759a549f1f313ce16b07d8f9ba93098157b5fa10ee3e61117f" dependencies = [ - "proc-macro2", "quote", - "syn 2.0.119", + "unsynn", ] [[package]] @@ -7903,17 +6949,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "visibility" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d674d135b4a8c1d7e813e2f8d1c9a58308aee4a680323066025e53132218bd91" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "walkdir" version = "2.5.0" @@ -7945,7 +6980,7 @@ version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ - "cfg-if 1.0.4", + "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", @@ -8105,9 +7140,13 @@ dependencies = [ [[package]] name = "weak-table" -version = "0.3.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" +checksum = "5eba2ec791141b0bf4db542f6b038a8f9310cee33e076cb181245780b09a24ff" +dependencies = [ + "cfg-if", + "hashbrown 0.16.1", +] [[package]] name = "web-sys" @@ -8126,7 +7165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cdc136a53ccd64a1211f107ccc34404769fbcc0f165f1afa065f5d88ab93538" dependencies = [ "async-task", - "cfg-if 1.0.4", + "cfg-if", "js-sys", "wasm-bindgen", ] @@ -8156,14 +7195,32 @@ dependencies = [ "annotate-snippets", "derive_more", "half", - "itertools", + "itertools 0.14.0", "num-traits", "proc-macro2", "quote", "thiserror 2.0.20", - "wesl-macros", - "wgsl-parse", - "wgsl-types", + "wesl-macros 0.3.2", + "wgsl-parse 0.3.2", + "wgsl-types 0.3.2", +] + +[[package]] +name = "wesl" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb84b87be534d24ac15773f8942ce90a00afe06093025d014c8cfa5277afa25b" +dependencies = [ + "annotate-snippets", + "anstream", + "derive_more", + "half", + "itertools 0.15.0", + "num-traits", + "thiserror 2.0.20", + "wesl-macros 0.4.4", + "wgsl-parse 0.4.4", + "wgsl-types 0.4.4", ] [[package]] @@ -8172,12 +7229,24 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d321ea9e1cc0a2e3de41c02e3a7586695a9c03beccb5460e938a7ccf00cb612" dependencies = [ - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.119", ] +[[package]] +name = "wesl-macros" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0de205d9eec254c378fa9ab1fd01e84537b6e330063d85fbf9245f65731aced" +dependencies = [ + "itertools 0.15.0", + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "wgpu" version = "29.0.4" @@ -8187,13 +7256,43 @@ dependencies = [ "arrayvec", "bitflags 2.13.1", "bytemuck", - "cfg-if 1.0.4", + "cfg-if", "cfg_aliases", "document-features", "hashbrown 0.16.1", "js-sys", "log", - "naga", + "naga 29.0.4", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core 29.0.4", + "wgpu-hal 29.0.4", + "wgpu-types 29.0.4", +] + +[[package]] +name = "wgpu" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527ccdf43dd5b2e8676eed9984ce00e2bbb0a1b85b70c1969dcb6cd2eb55ab9e" +dependencies = [ + "arrayvec", + "bitflags 2.13.1", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "js-sys", + "log", + "naga 30.0.1", "parking_lot", "portable-atomic", "profiling", @@ -8203,9 +7302,9 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", + "wgpu-core 30.0.1", + "wgpu-hal 30.0.1", + "wgpu-types 30.0.1", ] [[package]] @@ -8224,22 +7323,56 @@ dependencies = [ "hashbrown 0.16.1", "indexmap", "log", - "naga", + "naga 29.0.4", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.20", + "wgpu-core-deps-apple 29.0.4", + "wgpu-core-deps-emscripten 29.0.4", + "wgpu-core-deps-windows-linux-android 29.0.4", + "wgpu-hal 29.0.4", + "wgpu-naga-bridge 29.0.4", + "wgpu-types 29.0.4", +] + +[[package]] +name = "wgpu-core" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c018fce9b6270aa203c2fdd56f3cce996713534bd757e4ea58c8560b121f14" +dependencies = [ + "arrayvec", + "bit-set 0.10.0", + "bit-vec 0.9.1", + "bitflags 2.13.1", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.17.1", + "indexmap", + "log", + "naga 30.0.1", + "naga-types", "once_cell", "parking_lot", "portable-atomic", "profiling", "raw-window-handle", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror 2.0.20", - "wgpu-core-deps-apple", - "wgpu-core-deps-emscripten", + "wgpu-core-deps-apple 30.0.1", + "wgpu-core-deps-emscripten 30.0.1", "wgpu-core-deps-wasm", - "wgpu-core-deps-windows-linux-android", - "wgpu-hal", - "wgpu-naga-bridge", - "wgpu-types", + "wgpu-core-deps-windows-linux-android 30.0.1", + "wgpu-hal 30.0.1", + "wgpu-naga-bridge 30.0.1", + "wgpu-types 30.0.1", ] [[package]] @@ -8248,7 +7381,16 @@ version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3" dependencies = [ - "wgpu-hal", + "wgpu-hal 29.0.4", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061f3d319a40d39d00b1ecc2c33b89fe21d4e6fe01859df3500a3a8ecccd6b68" +dependencies = [ + "wgpu-hal 30.0.1", ] [[package]] @@ -8257,16 +7399,25 @@ version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01e09be551dc939498bdd5f6b2c66e55ab275dad25825267a08605a80fc9f0af" dependencies = [ - "wgpu-hal", + "wgpu-hal 29.0.4", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98b86cf4abf524a902dd35f18ca6a3f08fc2ae9847c8f10b48e30491b1f0b86" +dependencies = [ + "wgpu-hal 30.0.1", ] [[package]] name = "wgpu-core-deps-wasm" -version = "29.0.4" +version = "30.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1fb1798be2a912497d4c224f72d39bb0cb34af50e8bcc29865bc339c943059" +checksum = "d260ca2ca8adc654f513a869bf3cac0b47f5b8d3f87803d6942015f8de597aae" dependencies = [ - "wgpu-hal", + "wgpu-hal 30.0.1", ] [[package]] @@ -8275,7 +7426,16 @@ version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" dependencies = [ - "wgpu-hal", + "wgpu-hal 29.0.4", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7586165fd5f6d881cb9ce4bb71f40d6caab2c0f1837e3fc1d9788a197fb6004f" +dependencies = [ + "wgpu-hal 30.0.1", ] [[package]] @@ -8291,7 +7451,7 @@ dependencies = [ "bitflags 2.13.1", "block2 0.6.2", "bytemuck", - "cfg-if 1.0.4", + "cfg-if", "cfg_aliases", "glow", "glutin_wgl_sys", @@ -8301,12 +7461,67 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.8.9", + "libloading", + "log", + "naga 29.0.4", + "ndk-sys", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.20", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge 29.0.4", + "wgpu-types 29.0.4", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-result 0.4.1", +] + +[[package]] +name = "wgpu-hal" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b7fb58561a792bc237628ba0792e332de418fefe145f13b5ed8201e6d52f58" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set 0.10.0", + "bitflags 2.13.1", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "hashbrown 0.17.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", "log", - "naga", + "naga 30.0.1", + "naga-types", "ndk-sys", "objc2 0.6.4", "objc2-core-foundation", + "objc2-core-graphics", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", "objc2-quartz-core 0.3.2", @@ -8321,12 +7536,13 @@ dependencies = [ "raw-window-metal", "renderdoc-sys", "smallvec", + "static_assertions", "thiserror 2.0.20", "wasm-bindgen", "wayland-sys", "web-sys", - "wgpu-naga-bridge", - "wgpu-types", + "wgpu-naga-bridge 30.0.1", + "wgpu-types 30.0.1", "windows 0.62.2", "windows-core 0.62.2", "windows-result 0.4.1", @@ -8338,8 +7554,18 @@ version = "29.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" dependencies = [ - "naga", - "wgpu-types", + "naga 29.0.4", + "wgpu-types 29.0.4", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f62e73117bb7a62bfd9c5a5841438a823f6566c6442a808ee269d2d055c081" +dependencies = [ + "naga 30.0.1", + "wgpu-types 30.0.1", ] [[package]] @@ -8353,7 +7579,23 @@ dependencies = [ "js-sys", "log", "raw-window-handle", + "web-sys", +] + +[[package]] +name = "wgpu-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "js-sys", + "log", + "naga-types", + "raw-window-handle", "serde", + "static_assertions", "web-sys", ] @@ -8365,13 +7607,30 @@ checksum = "946419efa46664a8070c512fb358ac4acfc6e6215c2396ae5afeb9b72dd6ca7d" dependencies = [ "annotate-snippets", "derive_more", - "itertools", - "lalrpop", - "lalrpop-util", + "itertools 0.14.0", + "lalrpop 0.22.2", + "lalrpop-util 0.22.2", + "lexical", + "logos", + "thiserror 2.0.20", + "wgsl-types 0.3.2", +] + +[[package]] +name = "wgsl-parse" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0588236e8f7cd43cd1d88322095d971fba842c7d985f484453461a376a1ce6" +dependencies = [ + "annotate-snippets", + "derive_more", + "itertools 0.15.0", + "lalrpop 0.23.1", + "lalrpop-util 0.23.1", "lexical", "logos", "thiserror 2.0.20", - "wgsl-types", + "wgsl-types 0.4.4", ] [[package]] @@ -8381,20 +7640,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7aed068ca1dc8fb182f574c9f2ace45ec988c71ba050f6dd334883fe3b8a88e2" dependencies = [ "half", - "itertools", + "itertools 0.14.0", "num-traits", ] [[package]] -name = "which" -version = "4.4.2" +name = "wgsl-types" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "5dfc32acf991054fb0290c5fb03d9dae60be8cbc9245c9c26e4420e11e8c0045" dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", + "half", + "itertools 0.15.0", + "num-traits", ] [[package]] @@ -8428,15 +7686,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windowfunctions" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90628d739333b7c5d2ee0b70210b97b8cddc38440c682c96fd9e2c24c2db5f3a" -dependencies = [ - "num-traits", -] - [[package]] name = "windows" version = "0.58.0" @@ -8857,8 +8106,8 @@ dependencies = [ "calloop", "cfg_aliases", "concurrent-queue", - "core-foundation 0.9.4", - "core-graphics 0.23.2", + "core-foundation", + "core-graphics", "cursor-icon", "dpi", "js-sys", @@ -8899,9 +8148,6 @@ name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] [[package]] name = "winnow" @@ -8919,10 +8165,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "wmidi" -version = "4.0.11" +name = "wl-clipboard-rs" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b1b1e28ac6301b1c097d21c11130090f7ecc5db9d5ef4e52595ab942f8d76d5" +checksum = "4d7888ccd4896447b2d14d3a9350a85df2aeb6f181e2e7a31349d104ac46cac1" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 1.1.4", + "thiserror 2.0.20", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] [[package]] name = "writeable" @@ -8930,6 +8188,15 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.21.0" @@ -8960,7 +8227,7 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "libloading 0.8.9", + "libloading", "once_cell", "rustix 1.1.4", "x11rb-protocol", diff --git a/Cargo.toml b/Cargo.toml index be9f10df..6b00c786 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,19 +11,23 @@ workspace = true default = ["wayland", "x11"] wayland = ["processing_render/wayland"] x11 = ["processing_render/x11"] -webcam = ["dep:processing_webcam"] +# TEMP(bevy-020): nannou_webcam pins bevy 0.19 +# webcam = ["dep:processing_webcam"] cuda = ["dep:processing_cuda", "processing_cuda/cuda"] [workspace] resolver = "3" members = ["crates/*", "tools/*"] +# TEMP(bevy-020): midi/audio pin bevy 0.19 +# TEMP(bevy-020): nannou_webcam pins bevy 0.19 +exclude = ["crates/processing_audio", "crates/processing_midi", "crates/processing_webcam"] [workspace.lints.clippy] type_complexity = "allow" too_many_arguments = "allow" [workspace.dependencies] -bevy = { git = "https://github.com/processing/bevy", branch = "main", default-features = false, features = [ +bevy = { git = "https://github.com/processing/bevy", branch = "processing-batches", default-features = false, features = [ # 2d "2d_bevy_render", "default_app", @@ -48,12 +52,11 @@ bevy = { git = "https://github.com/processing/bevy", branch = "main", default-fe "wayland", "webgl2", "x11", - "shader_format_wesl", "free_camera", "pan_camera", ] } -bevy_naga_reflect = { git = "https://github.com/tychedelia/bevy_naga_reflect" } +bevy_naga_reflect = { git = "https://github.com/tychedelia/bevy_naga_reflect", branch = "bevy-020-fixes" } bevy_cuda = { git = "https://github.com/tychedelia/bevy_cuda" } naga = { version = "29", features = ["wgsl-in"] } wesl = { version = "0.3", default-features = false } @@ -64,11 +67,13 @@ processing_core = { path = "crates/processing_core" } processing_cuda = { path = "crates/processing_cuda" } processing_pyo3 = { path = "crates/processing_pyo3" } processing_render = { path = "crates/processing_render" } -processing_midi = { path = "crates/processing_midi" } -processing_audio = { path = "crates/processing_audio" } +# TEMP(bevy-020): midi/audio pin bevy 0.19 +# processing_midi = { path = "crates/processing_midi" } +# processing_audio = { path = "crates/processing_audio" } processing_input = { path = "crates/processing_input" } processing_glfw = { path = "crates/processing_glfw" } -processing_webcam = { path = "crates/processing_webcam" } +# TEMP(bevy-020): nannou_webcam pins bevy 0.19 +# processing_webcam = { path = "crates/processing_webcam" } rand = "0.10" rand_distr = "0.6" @@ -76,10 +81,12 @@ rand_distr = "0.6" bevy = { workspace = true } processing_core = { workspace = true } processing_render = { workspace = true } -processing_midi = { workspace = true } -processing_audio = { workspace = true } +# TEMP(bevy-020) +# processing_midi = { workspace = true } +# processing_audio = { workspace = true } processing_input = { workspace = true } -processing_webcam = { workspace = true, optional = true } +# TEMP(bevy-020) +# processing_webcam = { workspace = true, optional = true } processing_cuda = { workspace = true, optional = true } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } @@ -98,22 +105,22 @@ processing_glfw = { workspace = true, features = ["wayland", "x11"] } ## TODO: Remove these patches once we've moved back to depending on upstream bevy [patch."https://github.com/bevyengine/bevy"] -bevy = { git = "https://github.com/processing/bevy", branch = "main" } +bevy = { git = "https://github.com/processing/bevy", branch = "processing-batches" } [patch.crates-io] -bevy = { git = "https://github.com/processing/bevy", branch = "main" } +bevy = { git = "https://github.com/processing/bevy", branch = "processing-batches" } # bevy_seedling depends on bevy sub-crates directly, so they must be patched too -bevy_app = { git = "https://github.com/processing/bevy", branch = "main" } -bevy_asset = { git = "https://github.com/processing/bevy", branch = "main" } -bevy_diagnostic = { git = "https://github.com/processing/bevy", branch = "main" } -bevy_ecs = { git = "https://github.com/processing/bevy", branch = "main" } -bevy_log = { git = "https://github.com/processing/bevy", branch = "main" } -bevy_math = { git = "https://github.com/processing/bevy", branch = "main" } -bevy_platform = { git = "https://github.com/processing/bevy", branch = "main" } -bevy_reflect = { git = "https://github.com/processing/bevy", branch = "main" } -bevy_time = { git = "https://github.com/processing/bevy", branch = "main" } -bevy_transform = { git = "https://github.com/processing/bevy", branch = "main" } -bevy_utils = { git = "https://github.com/processing/bevy", branch = "main" } +bevy_app = { git = "https://github.com/processing/bevy", branch = "processing-batches" } +bevy_asset = { git = "https://github.com/processing/bevy", branch = "processing-batches" } +bevy_diagnostic = { git = "https://github.com/processing/bevy", branch = "processing-batches" } +bevy_ecs = { git = "https://github.com/processing/bevy", branch = "processing-batches" } +bevy_log = { git = "https://github.com/processing/bevy", branch = "processing-batches" } +bevy_math = { git = "https://github.com/processing/bevy", branch = "processing-batches" } +bevy_platform = { git = "https://github.com/processing/bevy", branch = "processing-batches" } +bevy_reflect = { git = "https://github.com/processing/bevy", branch = "processing-batches" } +bevy_time = { git = "https://github.com/processing/bevy", branch = "processing-batches" } +bevy_transform = { git = "https://github.com/processing/bevy", branch = "processing-batches" } +bevy_utils = { git = "https://github.com/processing/bevy", branch = "processing-batches" } [[example]] name = "rectangle" @@ -163,10 +170,11 @@ path = "examples/midi.rs" name = "gltf_load" path = "examples/gltf_load.rs" -[[example]] -name = "webcam" -path = "examples/webcam.rs" -required-features = ["webcam"] +# TEMP(bevy-020): webcam disabled +# [[example]] +# name = "webcam" +# path = "examples/webcam.rs" +# required-features = ["webcam"] [[example]] name = "stroke_2d" diff --git a/crates/processing_ffi/Cargo.toml b/crates/processing_ffi/Cargo.toml index 16d41364..86e45307 100644 --- a/crates/processing_ffi/Cargo.toml +++ b/crates/processing_ffi/Cargo.toml @@ -14,7 +14,8 @@ crate-type = ["cdylib", "staticlib"] default = [] wayland = ["processing/wayland"] x11 = ["processing/x11"] -webcam = ["processing/webcam"] +# TEMP(bevy-020): webcam disabled +# webcam = ["processing/webcam"] cuda = ["processing/cuda"] [dependencies] diff --git a/crates/processing_pyo3/Cargo.toml b/crates/processing_pyo3/Cargo.toml index ce5c3449..1c1926dc 100644 --- a/crates/processing_pyo3/Cargo.toml +++ b/crates/processing_pyo3/Cargo.toml @@ -15,7 +15,8 @@ default = ["wayland", "x11", "static-link"] wayland = ["processing/wayland", "processing_glfw/wayland"] static-link = ["processing_glfw/static-link"] x11 = ["processing/x11", "processing_glfw/x11"] -webcam = ["processing/webcam", "dep:processing_webcam"] +# TEMP(bevy-020): webcam disabled +# webcam = ["processing/webcam", "dep:processing_webcam"] cuda = ["dep:processing_cuda", "processing_cuda/cuda", "processing/cuda"] [dependencies] @@ -23,7 +24,8 @@ pyo3 = { workspace = true, features = ["experimental-inspect", "multiple-pymetho processing = { workspace = true } processing_core = { workspace = true } processing_render = { workspace = true } -processing_webcam = { workspace = true, optional = true } +# TEMP(bevy-020) +# processing_webcam = { workspace = true, optional = true } processing_glfw = { workspace = true } bevy = { workspace = true, features = ["file_watcher"] } png = "0.18" diff --git a/crates/processing_pyo3/src/color.rs b/crates/processing_pyo3/src/color.rs index 6aef7072..7d861d6b 100644 --- a/crates/processing_pyo3/src/color.rs +++ b/crates/processing_pyo3/src/color.rs @@ -124,6 +124,9 @@ fn components(color: &Color) -> [f32; 4] { Color::Lcha(c) => c.to_f32_array(), Color::Oklaba(c) => c.to_f32_array(), Color::Oklcha(c) => c.to_f32_array(), + Color::Okhsla(c) => c.to_f32_array(), + Color::Okhsva(c) => c.to_f32_array(), + Color::Okhwba(c) => c.to_f32_array(), Color::Xyza(c) => c.to_f32_array(), } } @@ -140,6 +143,9 @@ fn components_no_alpha(color: &Color) -> [f32; 3] { Color::Lcha(c) => c.to_f32_array_no_alpha(), Color::Oklaba(c) => c.to_f32_array_no_alpha(), Color::Oklcha(c) => c.to_f32_array_no_alpha(), + Color::Okhsla(c) => c.to_f32_array_no_alpha(), + Color::Okhsva(c) => c.to_f32_array_no_alpha(), + Color::Okhwba(c) => c.to_f32_array_no_alpha(), Color::Xyza(c) => c.to_f32_array_no_alpha(), } } diff --git a/crates/processing_render/Cargo.toml b/crates/processing_render/Cargo.toml index 6dddeabe..cf2d68a5 100644 --- a/crates/processing_render/Cargo.toml +++ b/crates/processing_render/Cargo.toml @@ -15,7 +15,7 @@ x11 = ["bevy/x11"] bevy = { workspace = true } bevy_naga_reflect = { workspace = true } # direct wgpu dep to reach `wgpu::util::TextureBlitter` (unifies to bevy's wgpu) -wgpu = "29" +wgpu = "30" naga = { workspace = true } wesl = { workspace = true } lyon = "1.0" diff --git a/crates/processing_render/src/compute.rs b/crates/processing_render/src/compute.rs index e11a2810..a4b24433 100644 --- a/crates/processing_render/src/compute.rs +++ b/crates/processing_render/src/compute.rs @@ -15,7 +15,7 @@ use bevy::{ PipelineCache, PollType, }, renderer::{RenderDevice, RenderQueue}, - storage::{GpuShaderBuffer, ShaderBuffer}, + storage::{GpuShaderBuffer, ShaderBuffer, ShaderBufferData}, texture::GpuImage, }, }; @@ -59,7 +59,7 @@ pub fn create_buffer( render_device: Res, ) -> Entity { let handle = buffers.add(ShaderBuffer::new( - &vec![0u8; size as usize], + vec![0u8; size as usize], RenderAssetUsages::all(), )); commands @@ -79,8 +79,8 @@ pub fn create_buffer_with_usage( mut buffers: ResMut>, render_device: Res, ) -> Entity { - let mut shader_buffer = ShaderBuffer::new(&vec![0u8; size as usize], RenderAssetUsages::all()); - shader_buffer.buffer_description.usage |= extra_usage; + let mut shader_buffer = ShaderBuffer::new(vec![0u8; size as usize], RenderAssetUsages::all()); + shader_buffer.buffer_usage |= extra_usage; let handle = buffers.add(shader_buffer); commands .spawn(Buffer { @@ -100,7 +100,7 @@ pub fn create_buffer_with_data( render_device: Res, ) -> Entity { let size = data.len() as u64; - let handle = buffers.add(ShaderBuffer::new(&data, RenderAssetUsages::all())); + let handle = buffers.add(ShaderBuffer::new(data, RenderAssetUsages::all())); commands .spawn(Buffer { handle, @@ -142,8 +142,19 @@ pub fn write_buffer_asset( patched.ok_or(ProcessingError::BufferNotFound)? } +/// The CPU copy of a buffer's contents, if it has one. +pub fn shader_buffer_bytes(asset: &ShaderBuffer) -> Option<&[u8]> { + match &asset.data { + ShaderBufferData::Initialized { data, .. } => Some(data.as_slice()), + ShaderBufferData::Uninitialized(_) => None, + } +} + fn patch_asset(asset: &mut ShaderBuffer, offset: u64, data: &[u8]) -> Result<()> { - let dst = asset.data.as_mut().ok_or(ProcessingError::BufferNotFound)?; + let ShaderBufferData::Initialized { data: dst, .. } = &mut asset.data else { + return Err(ProcessingError::BufferNotFound); + }; + let dst = dst.as_mut_slice(); let start = offset as usize; dst[start..start + data.len()].copy_from_slice(data); Ok(()) @@ -178,7 +189,10 @@ pub fn read_buffer_gpu( .map_err(|e| ProcessingError::BufferMapError(format!("map channel closed: {e}")))? .map_err(|e| ProcessingError::BufferMapError(format!("map failed: {e}")))?; - let bytes = buffer_slice.get_mapped_range().to_vec(); + let bytes = buffer_slice + .get_mapped_range() + .map_err(|e| ProcessingError::BufferMapError(format!("map range failed: {e}")))? + .to_vec(); readback_buffer.unmap(); Ok(bytes) } @@ -302,6 +316,7 @@ pub fn create_compute(app: &mut App, shader_entity: Entity) -> Result { shader: shader_handle.clone(), shader_defs: Vec::new(), entry_point: Some(entry_point.clone().into()), + constants: Vec::new(), zero_initialize_workgroup_memory: true, }; diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index c8005e79..7a88ec34 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -700,7 +700,10 @@ pub fn readback_raw( r.recv().expect("Failed to receive the map_async message"); - let data = buffer_slice.get_mapped_range().to_vec(); + let data = buffer_slice + .get_mapped_range() + .expect("Failed to get mapped range for readback") + .to_vec(); graphics.readback_buffer.unmap(); diff --git a/crates/processing_render/src/image.rs b/crates/processing_render/src/image.rs index 4945b3b5..9ff018de 100644 --- a/crates/processing_render/src/image.rs +++ b/crates/processing_render/src/image.rs @@ -310,7 +310,10 @@ pub fn readback( r.recv().expect("Failed to receive the map_async message"); - let data = buffer_slice.get_mapped_range().to_vec(); + let data = buffer_slice + .get_mapped_range() + .expect("Failed to get mapped range for readback") + .to_vec(); let mut image = images .get_mut(&p_image.handle) diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 6bbfe67f..12f7a53c 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -2405,7 +2405,11 @@ fn ensure_buffer_synced(app: &mut App, entity: Entity) -> error::Result<()> { let asset = buffers .get_mut_untracked(handle.id()) .ok_or(error::ProcessingError::BufferNotFound)?; - asset.data = Some(bytes); + let buffer_size = bytes.len() as u64; + asset.data = bevy::render::storage::ShaderBufferData::Initialized { + data: bytes.into(), + buffer_size, + }; } let mut buf = world @@ -2502,7 +2506,7 @@ fn buffer_read_range(entity: Entity, offset: u64, len: u64) -> error::Result>(); let data = buffers .get(&handle) - .and_then(|a| a.data.as_ref()) + .and_then(compute::shader_buffer_bytes) .ok_or(error::ProcessingError::BufferNotFound)?; Ok(data[offset as usize..(offset + len) as usize].to_vec()) }) diff --git a/crates/processing_render/src/material/custom.rs b/crates/processing_render/src/material/custom.rs index bc9535cb..50f04b75 100644 --- a/crates/processing_render/src/material/custom.rs +++ b/crates/processing_render/src/material/custom.rs @@ -25,9 +25,9 @@ use bevy::{ mesh::MeshVertexBufferLayoutRef, pbr::{ DrawMaterial, EntitiesNeedingSpecialization, MainPassOpaqueDrawFunction, - MainPassTransparentDrawFunction, MaterialBindGroupAllocator, MaterialBindGroupAllocators, - MaterialFragmentShader, MaterialVertexShader, MeshPipelineKey, PreparedMaterial, - RenderMaterialBindings, RenderMaterialInstance, RenderMaterialInstances, base_specialize, + MainPassTransparentDrawFunction, MaterialFragmentShader, MaterialVertexShader, + MeshPipelineKey, PreparedMaterial, RenderMaterialInstance, RenderMaterialInstances, + base_specialize, }, prelude::*, reflect::{PartialReflect, ReflectMut, ReflectRef, structs::Struct}, @@ -37,8 +37,12 @@ use bevy::{ erased_render_asset::{ErasedRenderAsset, ErasedRenderAssetPlugin, PrepareAssetError}, render_asset::RenderAssets, render_phase::DrawFunctions, + material_bind_groups::{ + MaterialBindGroupAllocator, MaterialBindGroupAllocators, RenderMaterialBindings, + }, render_resource::{ - BindGroupLayoutDescriptor, BindingResources, BlendState, Face, UnpreparedBindGroup, + BindGroupBuilder, BindGroupLayoutDescriptor, BlendState, Face, OwnedBindingResource, + UnpreparedBindingResource, }, renderer::RenderDevice, storage::GpuShaderBuffer, @@ -630,9 +634,27 @@ impl ErasedRenderAsset for CustomMaterial { gpu_buffers, ); - let unprepared = UnpreparedBindGroup { - bindings: BindingResources(bindings), - }; + let mut unprepared = BindGroupBuilder::default(); + for (index, resource) in bindings { + let resource = match resource { + OwnedBindingResource::Buffer(buffer) => UnpreparedBindingResource::Buffer(buffer), + OwnedBindingResource::ShaderBuffer(handle) => { + UnpreparedBindingResource::ShaderBuffer(handle) + } + OwnedBindingResource::TextureView(dimension, view) => { + UnpreparedBindingResource::TextureView(dimension, view) + } + OwnedBindingResource::Sampler(kind, sampler) => { + UnpreparedBindingResource::Sampler(kind, sampler) + } + OwnedBindingResource::Data(data) => { + let start = unprepared.data_buffer.len() as u32; + unprepared.data_buffer.extend_from_slice(&data.0); + UnpreparedBindingResource::Data(start..unprepared.data_buffer.len() as u32) + } + }; + unprepared.binding_resources.push((index, resource)); + } let bind_group_allocator = bind_group_allocators .get_mut(&TypeId::of::()) @@ -642,12 +664,13 @@ impl ErasedRenderAsset for CustomMaterial { Entry::Occupied(mut occupied_entry) => { bind_group_allocator.free(*occupied_entry.get()); let new_binding = - bind_group_allocator.allocate_unprepared(unprepared, &bind_group_layout); + bind_group_allocator.allocate_unprepared(&mut unprepared, &bind_group_layout); *occupied_entry.get_mut() = new_binding; new_binding } - Entry::Vacant(vacant_entry) => *vacant_entry - .insert(bind_group_allocator.allocate_unprepared(unprepared, &bind_group_layout)), + Entry::Vacant(vacant_entry) => *vacant_entry.insert( + bind_group_allocator.allocate_unprepared(&mut unprepared, &bind_group_layout), + ), }; let draw_function = opaque_draw_functions.read().id::(); diff --git a/crates/processing_render/src/particles/material.rs b/crates/processing_render/src/particles/material.rs index 90b55a98..b4773595 100644 --- a/crates/processing_render/src/particles/material.rs +++ b/crates/processing_render/src/particles/material.rs @@ -20,7 +20,7 @@ pub struct ParticlesMaterialPlugin; impl Plugin for ParticlesMaterialPlugin { fn build(&self, app: &mut App) { - embedded_asset!(app, "particles.wgsl"); + embedded_asset!(app, "particles.wesl"); app.add_plugins(MaterialPlugin::::default()); } } @@ -53,11 +53,11 @@ pub struct ParticlesExtension { impl MaterialExtension for ParticlesExtension { fn fragment_shader() -> ShaderRef { - "embedded://processing_render/particles/particles.wgsl".into() + "embedded://processing_render/particles/particles.wesl".into() } fn deferred_fragment_shader() -> ShaderRef { - "embedded://processing_render/particles/particles.wgsl".into() + "embedded://processing_render/particles/particles.wesl".into() } fn specialize( diff --git a/crates/processing_render/src/particles/mod.rs b/crates/processing_render/src/particles/mod.rs index e6013b40..2e131085 100644 --- a/crates/processing_render/src/particles/mod.rs +++ b/crates/processing_render/src/particles/mod.rs @@ -325,8 +325,8 @@ fn make_buffer_with_usage( extra_usage: BufferUsages, ) -> Entity { let byte_size = initial.len() as u64; - let mut shader_buffer = ShaderBuffer::new(initial, RenderAssetUsages::all()); - shader_buffer.buffer_description.usage |= extra_usage; + let mut shader_buffer = ShaderBuffer::new(initial.to_vec(), RenderAssetUsages::all()); + shader_buffer.buffer_usage |= extra_usage; let handle = shader_buffers.add(shader_buffer); let readback = render_device.create_buffer(&BufferDescriptor { label: Some("Particles Buffer Readback"), diff --git a/crates/processing_render/src/particles/pack.rs b/crates/processing_render/src/particles/pack.rs index 0d844113..fa3b3cc6 100644 --- a/crates/processing_render/src/particles/pack.rs +++ b/crates/processing_render/src/particles/pack.rs @@ -1,22 +1,17 @@ use std::num::NonZeroU64; -use bevy::core_pipeline::Core3d; -use bevy::pbr::{ - MeshCullingDataBuffer, MeshInputUniform, MeshUniform, early_gpu_preprocess, - gpu_instance_batch::GpuInstanceBatchReservations, -}; +use bevy::pbr::gpu_instance_batch::{GpuInstanceBatchReservations, GpuInstanceBatchSystems}; use bevy::platform::collections::HashMap; use bevy::prelude::*; use bevy::render::{ Extract, ExtractSchedule, Render, RenderApp, RenderSystems, - batching::gpu_preprocessing::BatchedInstanceBuffers, render_asset::RenderAssets, render_resource::{ BindGroup, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BufferBindingType, CachedComputePipelineId, CachedPipelineState, ComputePassDescriptor, ComputePipelineDescriptor, PipelineCache, ShaderStages, ShaderType, UniformBuffer, }, - renderer::{RenderContext, RenderDevice, RenderQueue}, + renderer::{RenderContext, RenderDevice, RenderGraph, RenderQueue}, storage::{GpuShaderBuffer, ShaderBuffer}, sync_world::{MainEntity, MainEntityHashMap}, }; @@ -35,9 +30,9 @@ impl Plugin for ParticlesPackPlugin { fn build(&self, app: &mut App) { let shader = { let mut shaders = app.world_mut().resource_mut::>(); - shaders.add(Shader::from_wgsl( - include_str!("pack.wgsl"), - "processing_render/particles/pack.wgsl", + shaders.add(Shader::from_wesl( + include_str!("pack.wesl"), + "processing_render/particles/pack.wesl", )) }; app.insert_resource(ParticlesPackShader(shader.clone())); @@ -55,7 +50,12 @@ impl Plugin for ParticlesPackPlugin { Render, prepare_pack_bind_groups.in_set(RenderSystems::PrepareBindGroups), ) - .add_systems(Core3d, dispatch_pack.before(early_gpu_preprocess)); + // Publish into each draw's persistent `GpuMeshInstance` output; the + // renderer materializes those into mesh inputs before any view. + .add_systems( + RenderGraph, + dispatch_pack.in_set(GpuInstanceBatchSystems::Publish), + ); } } @@ -81,10 +81,10 @@ pub struct ParticlesPackPipelines { #[derive(Copy, Clone, Default, ShaderType)] struct ParticlesPackParams { - base_input_index: u32, count: u32, _pad0: u32, _pad1: u32, + _pad2: u32, } pub struct ExtractedParticlesData { @@ -128,21 +128,18 @@ fn pack_layout_entries(key: PackPipelineKey) -> Vec { min_binding_size: NonZeroU64::new(16), }; - let mut entries = vec![ - layout_entry(0, storage_rw), - layout_entry(1, storage_rw), - layout_entry(2, storage_r), - ]; + // 0: the draw's `GpuMeshInstance` output, 1..: particle attributes, last: params. + let mut entries = vec![layout_entry(0, storage_rw), layout_entry(1, storage_r)]; if key.has_rotation { - entries.push(layout_entry(3, storage_r)); + entries.push(layout_entry(2, storage_r)); } if key.has_scale { - entries.push(layout_entry(4, storage_r)); + entries.push(layout_entry(3, storage_r)); } if key.has_life { - entries.push(layout_entry(5, storage_r)); + entries.push(layout_entry(4, storage_r)); } - entries.push(layout_entry(6, uniform)); + entries.push(layout_entry(5, uniform)); entries } @@ -264,28 +261,16 @@ fn prepare_pack_bind_groups( pipeline_cache: Res, extracted: Res, reservations: Res, - batched_instance_buffers: Res>, - culling_data_buffer: Res, gpu_buffers: Res>, render_device: Res, render_queue: Res, mut bind_groups: ResMut, ) { + // Rebuilt every frame: a resized reservation replaces its output buffer. bind_groups.per_batch.clear(); - let Some(input_buffer) = batched_instance_buffers - .current_input_buffer - .buffer() - .buffer() - else { - return; - }; - let Some(culling_buffer) = culling_data_buffer.buffer() else { - return; - }; - for (main_entity, data) in extracted.by_main.iter() { - let Some(reservation) = reservations.by_entity.get(main_entity) else { + let Some(reservation) = reservations.get(*main_entity) else { continue; }; let Some(gpu_position) = gpu_buffers.get(&data.position) else { @@ -315,8 +300,7 @@ fn prepare_pack_bind_groups( let cached = pipelines.by_key.get(&data.key).unwrap(); let params = ParticlesPackParams { - base_input_index: reservation.input_buffer_base, - count: reservation.max_capacity, + count: reservation.max_capacity(), ..default() }; let mut uniform = UniformBuffer::from(params); @@ -325,37 +309,33 @@ fn prepare_pack_bind_groups( let mut entries: Vec = vec![ BindGroupEntry { binding: 0, - resource: input_buffer.as_entire_binding(), + resource: reservation.output_buffer().as_entire_binding(), }, BindGroupEntry { binding: 1, - resource: culling_buffer.as_entire_binding(), - }, - BindGroupEntry { - binding: 2, resource: gpu_position.buffer.as_entire_binding(), }, ]; if let Some(gpu_rotation) = gpu_rotation { entries.push(BindGroupEntry { - binding: 3, + binding: 2, resource: gpu_rotation.buffer.as_entire_binding(), }); } if let Some(gpu_scale) = gpu_scale { entries.push(BindGroupEntry { - binding: 4, + binding: 3, resource: gpu_scale.buffer.as_entire_binding(), }); } if let Some(gpu_life) = gpu_life { entries.push(BindGroupEntry { - binding: 5, + binding: 4, resource: gpu_life.buffer.as_entire_binding(), }); } entries.push(BindGroupEntry { - binding: 6, + binding: 5, resource: uniform.binding().unwrap(), }); @@ -365,7 +345,7 @@ fn prepare_pack_bind_groups( &entries, ); - let dispatch_count = reservation.max_capacity.div_ceil(WORKGROUP_SIZE); + let dispatch_count = reservation.max_capacity().div_ceil(WORKGROUP_SIZE); bind_groups.per_batch.insert( *main_entity, PerBatchBindGroup { diff --git a/crates/processing_render/src/particles/pack.wesl b/crates/processing_render/src/particles/pack.wesl new file mode 100644 index 00000000..660b25aa --- /dev/null +++ b/crates/processing_render/src/particles/pack.wesl @@ -0,0 +1,82 @@ +// Packs particle attributes into the draw's persistent `GpuMeshInstance` +// output (one slot per particle, slot i = particle i). The renderer +// materializes mesh inputs from it; culling uses the mesh's own bounds. +import bevy_pbr::gpu_instance_batch::gpu_instance::GpuMeshInstance; + +struct PackParams { + count: u32, + _pad0: u32, + _pad1: u32, + _pad2: u32, +} + +@group(0) @binding(0) var instances: array; +@group(0) @binding(1) var position: array; +@if(HAS_ROTATION) +@group(0) @binding(2) var rotation: array; +@if(HAS_SCALE) +@group(0) @binding(3) var scale: array; +@if(HAS_LIFE) +@group(0) @binding(4) var life: array; +@group(0) @binding(5) var params: PackParams; + +fn quat_to_basis(q: vec4) -> mat3x3 { + let x = q.x; let y = q.y; let z = q.z; let w = q.w; + let xx = x * x; let yy = y * y; let zz = z * z; + let xy = x * y; let xz = x * z; let yz = y * z; + let wx = w * x; let wy = w * y; let wz = w * z; + return mat3x3( + vec3(1.0 - 2.0 * (yy + zz), 2.0 * (xy + wz), 2.0 * (xz - wy)), + vec3(2.0 * (xy - wz), 1.0 - 2.0 * (xx + zz), 2.0 * (yz + wx)), + vec3(2.0 * (xz + wy), 2.0 * (yz - wx), 1.0 - 2.0 * (xx + yy)), + ); +} + +@compute @workgroup_size(64) +fn pack(@builtin(global_invocation_id) gid: vec3) { + let i = gid.x; + if i >= params.count { + return; + } + let slot = i; + + let pos = vec3( + position[i * 3u + 0u], + position[i * 3u + 1u], + position[i * 3u + 2u], + ); + + var basis = mat3x3( + vec3(1.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(0.0, 0.0, 1.0), + ); + @if(HAS_ROTATION) { + basis = quat_to_basis(vec4( + rotation[i * 4u + 0u], + rotation[i * 4u + 1u], + rotation[i * 4u + 2u], + rotation[i * 4u + 3u], + )); + } + + var s = vec3(1.0, 1.0, 1.0); + @if(HAS_SCALE) { + s = vec3(scale[i * 3u + 0u], scale[i * 3u + 1u], scale[i * 3u + 2u]); + } + + let c0 = basis[0] * s.x; + let c1 = basis[1] * s.y; + let c2 = basis[2] * s.z; + instances[slot].world_from_local = mat3x4( + vec4(c0.x, c1.x, c2.x, pos.x), + vec4(c0.y, c1.y, c2.y, pos.y), + vec4(c0.z, c1.z, c2.z, pos.z), + ); + instances[slot].tag = i; + // A dead particle (life <= 0) is an inactive slot and draws nothing. + instances[slot].is_active = 1u; + @if(HAS_LIFE) { + instances[slot].is_active = select(0u, 1u, life[i] > 0.0); + } +} diff --git a/crates/processing_render/src/particles/pack.wgsl b/crates/processing_render/src/particles/pack.wgsl deleted file mode 100644 index c1b0e460..00000000 --- a/crates/processing_render/src/particles/pack.wgsl +++ /dev/null @@ -1,113 +0,0 @@ -struct MeshInput { - world_from_local: mat3x4, - lightmap_uv_rect: vec2, - flags: u32, - previous_input_index: u32, - first_vertex_index: u32, - first_index_index: u32, - index_count: u32, - current_skin_index: u32, - material_and_lightmap_bind_group_slot: u32, - timestamp: u32, - tag: u32, - morph_descriptor_index: u32, -} - -struct MeshCullingData { - aabb_center: vec3, - _pad: f32, - aabb_half_extents: vec3, - life: f32, -} - -struct PackParams { - base_input_index: u32, - count: u32, - _pad0: u32, - _pad1: u32, -} - -@group(0) @binding(0) var mesh_input_buffer: array; -@group(0) @binding(1) var mesh_culling_buffer: array; -@group(0) @binding(2) var position: array; -#ifdef HAS_ROTATION -@group(0) @binding(3) var rotation: array; -#endif -#ifdef HAS_SCALE -@group(0) @binding(4) var scale: array; -#endif -#ifdef HAS_LIFE -@group(0) @binding(5) var life: array; -#endif -@group(0) @binding(6) var params: PackParams; - -fn quat_to_basis(q: vec4) -> mat3x3 { - let x = q.x; let y = q.y; let z = q.z; let w = q.w; - let xx = x * x; let yy = y * y; let zz = z * z; - let xy = x * y; let xz = x * z; let yz = y * z; - let wx = w * x; let wy = w * y; let wz = w * z; - return mat3x3( - vec3(1.0 - 2.0 * (yy + zz), 2.0 * (xy + wz), 2.0 * (xz - wy)), - vec3(2.0 * (xy - wz), 1.0 - 2.0 * (xx + zz), 2.0 * (yz + wx)), - vec3(2.0 * (xz + wy), 2.0 * (yz - wx), 1.0 - 2.0 * (xx + yy)), - ); -} - -@compute @workgroup_size(64) -fn pack(@builtin(global_invocation_id) gid: vec3) { - let i = gid.x; - if i >= params.count { - return; - } - let slot = params.base_input_index + i; - - let pos = vec3( - position[i * 3u + 0u], - position[i * 3u + 1u], - position[i * 3u + 2u], - ); - -#ifdef HAS_ROTATION - let q = vec4( - rotation[i * 4u + 0u], - rotation[i * 4u + 1u], - rotation[i * 4u + 2u], - rotation[i * 4u + 3u], - ); - let basis = quat_to_basis(q); -#else - let basis = mat3x3( - vec3(1.0, 0.0, 0.0), - vec3(0.0, 1.0, 0.0), - vec3(0.0, 0.0, 1.0), - ); -#endif - -#ifdef HAS_SCALE - let s = vec3( - scale[i * 3u + 0u], - scale[i * 3u + 1u], - scale[i * 3u + 2u], - ); -#else - let s = vec3(1.0, 1.0, 1.0); -#endif - - let c0 = basis[0] * s.x; - let c1 = basis[1] * s.y; - let c2 = basis[2] * s.z; - mesh_input_buffer[slot].world_from_local = mat3x4( - vec4(c0.x, c1.x, c2.x, pos.x), - vec4(c0.y, c1.y, c2.y, pos.y), - vec4(c0.z, c1.z, c2.z, pos.z), - ); - mesh_input_buffer[slot].tag = i; - - mesh_culling_buffer[slot].aabb_center = vec3(0.0, 0.0, 0.0); - mesh_culling_buffer[slot].aabb_half_extents = vec3(1.0, 1.0, 1.0); -#ifdef HAS_LIFE - mesh_culling_buffer[slot].life = life[i]; -#else - mesh_culling_buffer[slot].life = 1.0; -#endif -} diff --git a/crates/processing_render/src/particles/particles.wesl b/crates/processing_render/src/particles/particles.wesl new file mode 100644 index 00000000..ed8f5438 --- /dev/null +++ b/crates/processing_render/src/particles/particles.wesl @@ -0,0 +1,61 @@ +import bevy_pbr::render::{ + pbr_fragment::pbr_input_from_standard_material, + pbr_functions::alpha_discard, + mesh_functions, +}; + +@if(PREPASS_PIPELINE) +import bevy_pbr::{ + prepass::io::{VertexOutput, FragmentOutput}, + deferred::functions::deferred_output, +}; +@else +import bevy_pbr::render::{ + forward_io::{VertexOutput, FragmentOutput}, + pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing}, +}; + +@if(HAS_COLORS) +@group(constants::MATERIAL_BIND_GROUP) @binding(100) +var particle_colors: array>; + +@if(HAS_EMISSIVE_COLORS) +@group(constants::MATERIAL_BIND_GROUP) @binding(101) +var particle_emissive_colors: array>; + +@fragment +fn fragment( + in: VertexOutput, + @builtin(front_facing) is_front: bool, +) -> FragmentOutput { + var pbr_input = pbr_input_from_standard_material(in, is_front); + + let tag = mesh_functions::get_tag(in.instance_index); + + @if(HAS_COLORS) { + pbr_input.material.base_color = pbr_input.material.base_color * particle_colors[tag]; + } + + @if(HAS_EMISSIVE_COLORS) { + pbr_input.material.emissive = vec4( + pbr_input.material.emissive.rgb + particle_emissive_colors[tag].rgb, + pbr_input.material.emissive.a + ); + } + + pbr_input.material.base_color = alpha_discard( + pbr_input.material.flags, + pbr_input.material.alpha_cutoff, + pbr_input.material.base_color + ); + + var out: FragmentOutput; +@if(PREPASS_PIPELINE) { + out = deferred_output(in, pbr_input); +} @else { + out.color = apply_pbr_lighting(pbr_input); + out.color = main_pass_post_lighting_processing(pbr_input, out.color); +} + + return out; +} diff --git a/crates/processing_render/src/particles/particles.wgsl b/crates/processing_render/src/particles/particles.wgsl deleted file mode 100644 index 05a7419d..00000000 --- a/crates/processing_render/src/particles/particles.wgsl +++ /dev/null @@ -1,60 +0,0 @@ -#import bevy_pbr::{ - pbr_fragment::pbr_input_from_standard_material, - pbr_functions::alpha_discard, - mesh_functions, -} - -#ifdef PREPASS_PIPELINE -#import bevy_pbr::{ - prepass_io::{VertexOutput, FragmentOutput}, - pbr_deferred_functions::deferred_output, -} -#else -#import bevy_pbr::{ - forward_io::{VertexOutput, FragmentOutput}, - pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing}, -} -#endif - -#ifdef HAS_COLORS -@group(#{MATERIAL_BIND_GROUP}) @binding(100) -var particle_colors: array>; -#endif - -#ifdef HAS_EMISSIVE_COLORS -@group(#{MATERIAL_BIND_GROUP}) @binding(101) -var particle_emissive_colors: array>; -#endif - -@fragment -fn fragment( - in: VertexOutput, - @builtin(front_facing) is_front: bool, -) -> FragmentOutput { - var pbr_input = pbr_input_from_standard_material(in, is_front); - - let tag = mesh_functions::get_tag(in.instance_index); - -#ifdef HAS_COLORS - pbr_input.material.base_color = pbr_input.material.base_color * particle_colors[tag]; -#endif - -#ifdef HAS_EMISSIVE_COLORS - pbr_input.material.emissive = vec4( - pbr_input.material.emissive.rgb + particle_emissive_colors[tag].rgb, - pbr_input.material.emissive.a - ); -#endif - - pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color); - -#ifdef PREPASS_PIPELINE - let out = deferred_output(in, pbr_input); -#else - var out: FragmentOutput; - out.color = apply_pbr_lighting(pbr_input); - out.color = main_pass_post_lighting_processing(pbr_input, out.color); -#endif - - return out; -} diff --git a/crates/processing_render/src/particles/point.wgsl b/crates/processing_render/src/particles/point.wesl similarity index 59% rename from crates/processing_render/src/particles/point.wgsl rename to crates/processing_render/src/particles/point.wesl index 14d418ed..bb57df72 100644 --- a/crates/processing_render/src/particles/point.wgsl +++ b/crates/processing_render/src/particles/point.wesl @@ -1,13 +1,11 @@ -#import bevy_render::view::View +import bevy_render::view::View; @group(0) @binding(0) var view: View; @group(1) @binding(0) var positions: array; -#ifdef HAS_COLORS +@if(HAS_COLORS) @group(1) @binding(1) var colors: array; -#endif -#ifdef HAS_NORMALS +@if(HAS_NORMALS) @group(1) @binding(2) var normals: array; -#endif struct VertexOutput { @builtin(position) clip_position: vec4, @@ -24,32 +22,29 @@ fn vertex(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { var out: VertexOutput; out.world = world; out.clip_position = view.clip_from_world * vec4(world, 1.0); -#ifdef HAS_COLORS - out.color = vec4( - colors[i * 4u], colors[i * 4u + 1u], colors[i * 4u + 2u], colors[i * 4u + 3u]); -#else out.color = vec4(1.0, 1.0, 1.0, 1.0); -#endif -#ifdef HAS_NORMALS - out.normal = vec3(normals[i * 3u], normals[i * 3u + 1u], normals[i * 3u + 2u]); -#else + @if(HAS_COLORS) { + out.color = vec4( + colors[i * 4u], colors[i * 4u + 1u], colors[i * 4u + 2u], colors[i * 4u + 3u]); + } out.normal = vec3(0.0, 0.0, 0.0); -#endif + @if(HAS_NORMALS) { + out.normal = vec3(normals[i * 3u], normals[i * 3u + 1u], normals[i * 3u + 2u]); + } return out; } @fragment fn fragment(frag: VertexOutput) -> @location(0) vec4 { var rgb = frag.color.rgb; -#ifdef SHADED - #ifdef HAS_NORMALS - let normal = normalize(frag.normal); - #else - let normal = normalize(cross(dpdx(frag.world), dpdy(frag.world))); - #endif - let light = normalize(vec3(0.4, 0.85, 0.35)); - let diffuse = abs(dot(normal, light)); - rgb = rgb * (0.2 + 0.8 * diffuse); -#endif + @if(SHADED) { + var normal = normalize(cross(dpdx(frag.world), dpdy(frag.world))); + @if(HAS_NORMALS) { + normal = normalize(frag.normal); + } + let light = normalize(vec3(0.4, 0.85, 0.35)); + let diffuse = abs(dot(normal, light)); + rgb = rgb * (0.2 + 0.8 * diffuse); + } return vec4(rgb, frag.color.a); } diff --git a/crates/processing_render/src/particles/point_render.rs b/crates/processing_render/src/particles/point_render.rs index b82ea6e3..3c7917c5 100644 --- a/crates/processing_render/src/particles/point_render.rs +++ b/crates/processing_render/src/particles/point_render.rs @@ -46,7 +46,7 @@ pub struct ParticlesPointRenderPlugin; impl Plugin for ParticlesPointRenderPlugin { fn build(&self, app: &mut App) { - embedded_asset!(app, "point.wgsl"); + embedded_asset!(app, "point.wesl"); app.add_plugins(ExtractComponentPlugin::::default()); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { @@ -73,6 +73,7 @@ impl Plugin for ParticlesPointRenderPlugin { #[derive(Component, Clone, ExtractComponent)] #[require(VisibilityClass)] #[component(on_add = visibility::add_visibility_class::)] +#[extract_app(RenderApp)] pub struct ParticleRasterDraw { pub position: Handle, pub count: u32, @@ -155,7 +156,7 @@ impl FromWorld for ParticleRasterPipeline { let render_device = world.resource::().clone(); let asset_server = world.resource::(); let shader: Handle = - asset_server.load("embedded://processing_render/particles/point.wgsl"); + asset_server.load("embedded://processing_render/particles/point.wesl"); let view_entries: Vec<_> = BindGroupLayoutEntries::single( ShaderStages::VERTEX, diff --git a/crates/processing_render/src/render/primitive/shape3d.rs b/crates/processing_render/src/render/primitive/shape3d.rs index 1e4889b3..01563f27 100644 --- a/crates/processing_render/src/render/primitive/shape3d.rs +++ b/crates/processing_render/src/render/primitive/shape3d.rs @@ -17,35 +17,35 @@ fn ensure_vertex_colors(mesh: &mut Mesh) { } pub fn box_mesh(width: f32, height: f32, depth: f32) -> Mesh { - let cuboid = bevy::math::primitives::Cuboid::new(width, height, depth); + let cuboid = bevy::shape::Cuboid::new(width, height, depth); let mut mesh = Mesh::from(cuboid); ensure_vertex_colors(&mut mesh); mesh } pub fn sphere_mesh(radius: f32, sectors: u32, stacks: u32) -> Mesh { - let sphere = bevy::math::primitives::Sphere::new(radius); + let sphere = bevy::shape::Sphere::new(radius); let mut mesh = sphere.mesh().uv(sectors, stacks); ensure_vertex_colors(&mut mesh); mesh } pub fn cylinder_mesh(radius: f32, height: f32, detail: u32) -> Mesh { - let cylinder = bevy::math::primitives::Cylinder::new(radius, height); + let cylinder = bevy::shape::Cylinder::new(radius, height); let mut mesh = cylinder.mesh().resolution(detail).build(); ensure_vertex_colors(&mut mesh); mesh } pub fn cone_mesh(radius: f32, height: f32, detail: u32) -> Mesh { - let cone = bevy::math::primitives::Cone::new(radius, height); + let cone = bevy::shape::Cone::new(radius, height); let mut mesh = cone.mesh().resolution(detail).build(); ensure_vertex_colors(&mut mesh); mesh } pub fn torus_mesh(radius: f32, tube_radius: f32, major_segments: u32, minor_segments: u32) -> Mesh { - let torus = bevy::math::primitives::Torus::new(tube_radius, radius); + let torus = bevy::shape::Torus::new(tube_radius, radius); let mut mesh = torus .mesh() .major_resolution(major_segments as usize) @@ -56,7 +56,7 @@ pub fn torus_mesh(radius: f32, tube_radius: f32, major_segments: u32, minor_segm } pub fn capsule_mesh(radius: f32, length: f32, detail: u32) -> Mesh { - let capsule = bevy::math::primitives::Capsule3d::new(radius, length); + let capsule = bevy::shape::Capsule3d::new(radius, length); let mut mesh = capsule .mesh() .longitudes(detail) @@ -67,7 +67,7 @@ pub fn capsule_mesh(radius: f32, length: f32, detail: u32) -> Mesh { } pub fn conical_frustum_mesh(radius_top: f32, radius_bottom: f32, height: f32, detail: u32) -> Mesh { - let frustum = bevy::math::primitives::ConicalFrustum { + let frustum = bevy::shape::ConicalFrustum { radius_top, radius_bottom, height, @@ -79,7 +79,7 @@ pub fn conical_frustum_mesh(radius_top: f32, radius_bottom: f32, height: f32, de pub fn tetrahedron_mesh(radius: f32) -> Mesh { let r = radius; - let tetrahedron = bevy::math::primitives::Tetrahedron::new( + let tetrahedron = bevy::shape::Tetrahedron::new( bevy::math::Vec3::new(r, r, r), bevy::math::Vec3::new(r, -r, -r), bevy::math::Vec3::new(-r, r, -r), @@ -122,7 +122,7 @@ pub fn grid_mesh(nx: u32, ny: u32, nz: u32, spacing: f32) -> Mesh { } pub fn plane_mesh(width: f32, height: f32) -> Mesh { - let plane = bevy::math::primitives::Plane3d::default(); + let plane = bevy::shape::Plane3d::default(); let mut mesh = plane.mesh().size(width, height).build(); ensure_vertex_colors(&mut mesh); mesh diff --git a/src/lib.rs b/src/lib.rs index 7620f21c..fed331f1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,8 @@ fn create_app(config: Config) -> App { }); app.add_plugins(plugins); - app.add_plugins(processing_midi::MidiPlugin); + // TEMP(bevy-020): midi disabled + // app.add_plugins(processing_midi::MidiPlugin); app.add_plugins(processing_input::InputPlugin); app.add_plugins(processing_render::ProcessingRenderPlugin); diff --git a/src/prelude.rs b/src/prelude.rs index 34f95a3d..8ce57fee 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -4,11 +4,41 @@ pub use bevy::prelude::default; pub use bevy::render::render_resource::TextureFormat; pub use processing_core::{config::*, constants, error}; pub use processing_input::*; +// TEMP(bevy-020): processing_midi pins bevy 0.19; stubbed while the port is +// in progress. Restore the re-export (plus Cargo.toml deps and the +// MidiPlugin registration in lib.rs) once nannou_midi is ported. #[cfg(not(target_arch = "wasm32"))] -pub use processing_midi::{ - midi_connect, midi_disconnect, midi_list_ports, midi_note_off, midi_note_on, midi_play_notes, - midi_refresh_ports, -}; +mod midi_stubs { + use processing_core::error::{ProcessingError, Result}; + fn unavailable() -> Result { + Err(ProcessingError::InvalidArgument( + "midi support is temporarily disabled in this build".to_string(), + )) + } + pub fn midi_connect(_port: usize) -> Result<()> { + unavailable() + } + pub fn midi_disconnect() -> Result<()> { + unavailable() + } + pub fn midi_list_ports() -> Result> { + unavailable() + } + pub fn midi_refresh_ports() -> Result<()> { + unavailable() + } + pub fn midi_play_notes(_note: u8, _duration: u64) -> Result<()> { + unavailable() + } + pub fn midi_note_on(_note: u8, _velocity: u8) -> Result<()> { + unavailable() + } + pub fn midi_note_off(_note: u8) -> Result<()> { + unavailable() + } +} +#[cfg(not(target_arch = "wasm32"))] +pub use midi_stubs::*; pub use processing_render::{ render::command::{ ArcMode, BlendMode, DrawCommand, ShapeKind, ShapeMode, StrokeCapMode, StrokeJoinMode, From b4d70a3ccf3261bea808272317c8a66afe9c7083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Fri, 2 Oct 2026 21:16:47 -0700 Subject: [PATCH 09/20] Ship processing and lygia shaders in the wheel. --- .github/workflows/release.yml | 16 +-- .gitignore | 1 + Cargo.lock | 4 + crates/processing_pyo3/README.md | 15 +++ crates/processing_pyo3/mewnala/__init__.py | 1 + crates/processing_pyo3/mewnala/__main__.py | 32 ++++++ crates/processing_pyo3/mewnala/_shaders.py | 115 +++++++++++++++++++++ crates/processing_pyo3/pyproject.toml | 2 + justfile | 5 +- tools/bundle_shaders/Cargo.toml | 5 + tools/bundle_shaders/src/main.rs | 83 +++++++++++++++ 11 files changed, 272 insertions(+), 7 deletions(-) create mode 100644 crates/processing_pyo3/mewnala/__main__.py create mode 100644 crates/processing_pyo3/mewnala/_shaders.py create mode 100644 tools/bundle_shaders/Cargo.toml create mode 100644 tools/bundle_shaders/src/main.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 466d830e..4368fa31 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,10 +20,14 @@ jobs: run: | cargo build --release -p processing_pyo3 cargo run --release -p generate_stubs + - name: Bundle shader packages + run: cargo run --release -p bundle_shaders - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: type-stubs - path: crates/processing_pyo3/mewnala/*.pyi + name: python-assets + path: | + crates/processing_pyo3/mewnala/*.pyi + crates/processing_pyo3/mewnala/shaders/ linux: runs-on: ubuntu-22.04 @@ -41,7 +45,7 @@ jobs: python-version: '3.x' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: type-stubs + name: python-assets path: crates/processing_pyo3/mewnala - name: Setup QEMU if: matrix.target == 'aarch64' @@ -90,7 +94,7 @@ jobs: python-version: '3.x' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: type-stubs + name: python-assets path: crates/processing_pyo3/mewnala - name: Build wheels uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 @@ -115,7 +119,7 @@ jobs: python-version: '3.x' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: type-stubs + name: python-assets path: crates/processing_pyo3/mewnala - name: Build wheels uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 @@ -137,7 +141,7 @@ jobs: submodules: true - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: type-stubs + name: python-assets path: crates/processing_pyo3/mewnala - name: Build sdist uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 diff --git a/.gitignore b/.gitignore index 82eb289f..3c0b6ec5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ /site/ crates/processing_ffi/include/ crates/processing_pyo3/mewnala/*.pyi +crates/processing_pyo3/mewnala/shaders/ .DS_Store .ipynb_checkpoints/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 5899f076..b1bc2097 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2207,6 +2207,10 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "bundle_shaders" +version = "0.0.8" + [[package]] name = "bytemuck" version = "1.25.2" diff --git a/crates/processing_pyo3/README.md b/crates/processing_pyo3/README.md index 3e28caa0..804085cc 100644 --- a/crates/processing_pyo3/README.md +++ b/crates/processing_pyo3/README.md @@ -22,3 +22,18 @@ $ python >>> import processing >>> processing.size(500, 500) ``` + +### Shader editor support + +The wheel ships the `processing` and `lygia` WESL packages. To have +[wgsl-analyzer](https://github.com/wgsl-analyzer/wgsl-analyzer) resolve +`import processing::...` and `import lygia::...` in your sketch's shaders: + +``` +$ python -m mewnala shaders init +``` + +This writes (or updates) `wesl.toml` in the current directory, pointing at the +installed packages. Pass `--vendor` to copy them into `.mewnala/shaders` instead. +Rerun after upgrading mewnala. In a source checkout, `just py-shaders` generates +the bundle. diff --git a/crates/processing_pyo3/mewnala/__init__.py b/crates/processing_pyo3/mewnala/__init__.py index 44d2cbae..f4fd0a28 100644 --- a/crates/processing_pyo3/mewnala/__init__.py +++ b/crates/processing_pyo3/mewnala/__init__.py @@ -18,6 +18,7 @@ import importlib as _importlib math = _importlib.import_module(f"{__name__}.math") # noqa: E402 from .math import * # noqa: E402,F401,F403 +from ._shaders import shader_workspace # noqa: E402,F401 # global var handling. for wildcard import of our module, we copy into globals, otherwise # we dispatch to get attr and call the underlying getter method diff --git a/crates/processing_pyo3/mewnala/__main__.py b/crates/processing_pyo3/mewnala/__main__.py new file mode 100644 index 00000000..c25aaa72 --- /dev/null +++ b/crates/processing_pyo3/mewnala/__main__.py @@ -0,0 +1,32 @@ +import argparse + +from ._shaders import shader_packages, shader_workspace + + +def main(argv=None): + parser = argparse.ArgumentParser(prog="python -m mewnala") + commands = parser.add_subparsers(dest="command", required=True) + + shaders = commands.add_parser("shaders", help="bundled WESL packages") + shaders_commands = shaders.add_subparsers(dest="shaders_command", required=True) + + init = shaders_commands.add_parser( + "init", help="create or update wesl.toml so editors resolve processing:: and lygia::" + ) + init.add_argument("dir", nargs="?", default=".") + init.add_argument( + "--vendor", action="store_true", help="copy the packages into DIR/.mewnala/shaders" + ) + + shaders_commands.add_parser("path", help="print where the bundled packages are installed") + + args = parser.parse_args(argv) + if args.shaders_command == "init": + print(f"wrote {shader_workspace(args.dir, vendor=args.vendor)}") + else: + for name, path in shader_packages().items(): + print(f"{name}\t{path}") + + +if __name__ == "__main__": + main() diff --git a/crates/processing_pyo3/mewnala/_shaders.py b/crates/processing_pyo3/mewnala/_shaders.py new file mode 100644 index 00000000..0ff55c6a --- /dev/null +++ b/crates/processing_pyo3/mewnala/_shaders.py @@ -0,0 +1,115 @@ +"""WESL packages bundled with mewnala, and wiring them into a project's `wesl.toml` +so editor tooling (wgsl-analyzer) resolves `import processing::...` and +`import lygia::...` the same way `Shader` does at runtime.""" + +import json +import shutil +from pathlib import Path + +PACKAGES = ("processing", "lygia") + +_BEGIN = "# BEGIN mewnala" +_END = "# END mewnala" +_VENDOR_DIR = Path(".mewnala") / "shaders" + + +def shader_packages(): + """Return `{name: directory}` for the WESL packages shipped in the wheel.""" + root = Path(__file__).parent / "shaders" + found = {name: root / name for name in PACKAGES if (root / name / "wesl.toml").is_file()} + if not found: + raise FileNotFoundError( + f"no bundled shader packages under {root}; " + "in a source checkout run `cargo run -p bundle_shaders`" + ) + return found + + +def _toml_path(path, base): + try: + p = path.relative_to(base) + except ValueError: + p = path + return json.dumps(p.as_posix()) + + +def _dependency_block(packages, base): + lines = [_BEGIN] + for name, path in packages.items(): + lines.append(f"{name} = {{ path = {_toml_path(path, base)} }}") + lines.append(_END) + return lines + + +def _splice(text, block): + lines = text.splitlines() + if _BEGIN in lines: + start = lines.index(_BEGIN) + try: + end = lines.index(_END, start) + except ValueError: + raise ValueError(f"`{_BEGIN}` without a matching `{_END}`") from None + lines[start : end + 1] = block + else: + header = next( + (i for i, l in enumerate(lines) if l.split("#", 1)[0].strip() == "[dependencies]"), + None, + ) + if header is None: + if lines and lines[-1].strip(): + lines.append("") + lines += ["[dependencies]", *block] + else: + lines[header + 1 : header + 1] = block + return "\n".join(lines) + "\n" + + +def _validate(text, path): + try: + import tomllib + except ImportError: + return + try: + doc = tomllib.loads(text) + except tomllib.TOMLDecodeError as e: + raise ValueError(f"updating {path} would produce invalid TOML ({e}); edit it by hand") from None + if not isinstance(doc.get("dependencies"), dict): + raise ValueError(f"{path}: `dependencies` is not a table") + + +def shader_workspace(path=".", vendor=False): + """Create or update `wesl.toml` in `path` so the bundled `processing` and + `lygia` packages resolve in the editor. + + Dependencies point into the installed package. With `vendor=True` they are + copied to `.mewnala/shaders` first, for editors that can't see the + environment's site-packages or for checking them in. Rerun after upgrading + mewnala either way. Returns the path of the `wesl.toml`. + """ + base = Path(path).resolve() + base.mkdir(parents=True, exist_ok=True) + packages = shader_packages() + + if vendor: + dst = base / _VENDOR_DIR + vendored = {} + for name, src in packages.items(): + target = dst / name + if target.exists(): + shutil.rmtree(target) + shutil.copytree(src, target) + vendored[name] = target + packages = vendored + + manifest = base / "wesl.toml" + block = _dependency_block(packages, base) + if manifest.exists(): + text = _splice(manifest.read_text(encoding="utf-8"), block) + else: + header = ['edition = "2026_pre"', 'root = "."'] + if vendor: + header.append(f'exclude = ["{_VENDOR_DIR.as_posix()}/**"]') + text = "\n".join([*header, "", "[dependencies]", *block]) + "\n" + _validate(text, manifest) + manifest.write_text(text, encoding="utf-8") + return manifest diff --git a/crates/processing_pyo3/pyproject.toml b/crates/processing_pyo3/pyproject.toml index dc68485e..80241664 100644 --- a/crates/processing_pyo3/pyproject.toml +++ b/crates/processing_pyo3/pyproject.toml @@ -48,6 +48,8 @@ include = [ { path = "mewnala/*.pyi", format = "wheel" }, { path = "mewnala/py.typed", format = "sdist" }, { path = "mewnala/py.typed", format = "wheel" }, + { path = "mewnala/shaders/**/*", format = "sdist" }, + { path = "mewnala/shaders/**/*", format = "wheel" }, ] [tool.uv] diff --git a/justfile b/justfile index 6cd2a1a9..5ff39b35 100644 --- a/justfile +++ b/justfile @@ -5,9 +5,12 @@ export PROCESSING_ASSET_ROOT := canonicalize("./assets") default: @just --list -py-build *args: +py-build *args: py-shaders cd crates/processing_pyo3; uv run maturin develop --release {{args}} +py-shaders: + cargo run -p bundle_shaders + py-stubs: py-build cargo run --release -p generate_stubs diff --git a/tools/bundle_shaders/Cargo.toml b/tools/bundle_shaders/Cargo.toml new file mode 100644 index 00000000..6be87017 --- /dev/null +++ b/tools/bundle_shaders/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "bundle_shaders" +version = "0.0.8" +edition = "2024" +publish = false diff --git a/tools/bundle_shaders/src/main.rs b/tools/bundle_shaders/src/main.rs new file mode 100644 index 00000000..54c2ad51 --- /dev/null +++ b/tools/bundle_shaders/src/main.rs @@ -0,0 +1,83 @@ +//! Lays out the `processing` and `lygia` WESL packages under `mewnala/shaders` so +//! the wheel ships them for editor tooling (`python -m mewnala shaders init`). + +use std::fs; +use std::io; +use std::path::Path; + +// wgsl-analyzer rejects lygia's own `unstable_2025` edition. +const PACKAGE_MANIFEST: &str = "edition = \"2026_pre\"\nroot = \".\"\n"; + +fn workspace_root() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap() +} + +fn copy_wesl_tree(src: &Path, dst: &Path) -> io::Result { + let mut count = 0; + let mut entries = fs::read_dir(src)?.collect::>>()?; + entries.sort_by_key(|e| e.file_name()); + for entry in entries { + let path = entry.path(); + let name = entry.file_name(); + if entry.file_type()?.is_dir() { + if name == "test" || name == "node_modules" || name.to_string_lossy().starts_with('.') { + continue; + } + count += copy_wesl_tree(&path, &dst.join(&name))?; + } else if path.extension().is_some_and(|e| e == "wesl") { + fs::create_dir_all(dst)?; + fs::copy(&path, dst.join(&name))?; + count += 1; + } + } + Ok(count) +} + +fn bundle_processing(root: &Path, out: &Path) -> io::Result { + let shaders = root.join("crates/processing_render/shaders"); + let dst = out.join("processing"); + // the wesl 0.3 `PkgBuilder` takes the root module from the sibling + // `processing.wesl`, the spec and wgsl-analyzer expect `package.wesl` + let count = copy_wesl_tree(&shaders.join("processing"), &dst)?; + fs::copy(shaders.join("processing.wesl"), dst.join("package.wesl"))?; + fs::write(dst.join("wesl.toml"), PACKAGE_MANIFEST)?; + Ok(count + 1) +} + +fn bundle_lygia(root: &Path, out: &Path) -> io::Result { + let src = root.join("lygia"); + if !src.join("wesl.toml").is_file() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "lygia submodule is not checked out (git submodule update --init)", + )); + } + let dst = out.join("lygia"); + // `.wesl` only: the analyzer ignores `include` and would index the sibling + // `.wgsl` copies, most of which still use `#include` + let count = copy_wesl_tree(&src, &dst)?; + fs::copy(src.join("LICENSE.md"), dst.join("LICENSE.md"))?; + fs::write(dst.join("wesl.toml"), PACKAGE_MANIFEST)?; + Ok(count) +} + +fn main() -> io::Result<()> { + let root = workspace_root(); + let out = root.join("crates/processing_pyo3/mewnala/shaders"); + if out.exists() { + fs::remove_dir_all(&out)?; + } + fs::create_dir_all(&out)?; + + let processing = bundle_processing(root, &out)?; + let lygia = bundle_lygia(root, &out)?; + eprintln!( + "Bundled {processing} processing and {lygia} lygia modules into {}", + out.display() + ); + Ok(()) +} From 68581d7389963527e7f7766bc7cc2c27fc2e9954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Fri, 2 Oct 2026 21:57:16 -0700 Subject: [PATCH 10/20] Python input fixes and sketch folder asset root. --- crates/processing_core/src/constants.rs | 45 +++- crates/processing_ffi/src/lib.rs | 240 ++------------------- crates/processing_input/src/keys.rs | 145 +++++++++++++ crates/processing_input/src/lib.rs | 2 + crates/processing_pyo3/mewnala/__init__.py | 20 +- crates/processing_pyo3/src/constants.rs | 35 ++- crates/processing_pyo3/src/graphics.rs | 16 +- crates/processing_pyo3/src/input.rs | 169 +++++---------- crates/processing_pyo3/src/lib.rs | 107 ++++++--- crates/processing_pyo3/src/surface.rs | 1 - crates/processing_wasm/src/lib.rs | 239 ++------------------ 11 files changed, 393 insertions(+), 626 deletions(-) create mode 100644 crates/processing_input/src/keys.rs diff --git a/crates/processing_core/src/constants.rs b/crates/processing_core/src/constants.rs index 3fb7ba0c..f6771497 100644 --- a/crates/processing_core/src/constants.rs +++ b/crates/processing_core/src/constants.rs @@ -138,7 +138,7 @@ pub const TAU: f32 = std::f32::consts::TAU; pub const DEG_TO_RAD: f32 = std::f32::consts::PI / 180.0; pub const RAD_TO_DEG: f32 = 180.0 / std::f32::consts::PI; -// Key codes stay numeric (compared against `key_code`); values must match `key_code_to_u32`. +// Key codes are GLFW values, mapped to Bevy `KeyCode`s by `processing_input::keys`. pub const KEY_A: u32 = 65; pub const KEY_B: u32 = 66; @@ -223,3 +223,46 @@ pub const F9: u32 = 298; pub const F10: u32 = 299; pub const F11: u32 = 300; pub const F12: u32 = 301; +pub const F13: u32 = 302; +pub const F14: u32 = 303; +pub const F15: u32 = 304; +pub const F16: u32 = 305; +pub const F17: u32 = 306; +pub const F18: u32 = 307; +pub const F19: u32 = 308; +pub const F20: u32 = 309; +pub const F21: u32 = 310; +pub const F22: u32 = 311; +pub const F23: u32 = 312; +pub const F24: u32 = 313; +pub const F25: u32 = 314; + +pub const CAPS_LOCK: u32 = 280; +pub const SCROLL_LOCK: u32 = 281; +pub const NUM_LOCK: u32 = 282; +pub const PRINT_SCREEN: u32 = 283; +pub const PAUSE: u32 = 284; + +pub const NUMPAD_0: u32 = 320; +pub const NUMPAD_1: u32 = 321; +pub const NUMPAD_2: u32 = 322; +pub const NUMPAD_3: u32 = 323; +pub const NUMPAD_4: u32 = 324; +pub const NUMPAD_5: u32 = 325; +pub const NUMPAD_6: u32 = 326; +pub const NUMPAD_7: u32 = 327; +pub const NUMPAD_8: u32 = 328; +pub const NUMPAD_9: u32 = 329; +pub const NUMPAD_DECIMAL: u32 = 330; +pub const NUMPAD_DIVIDE: u32 = 331; +pub const NUMPAD_MULTIPLY: u32 = 332; +pub const NUMPAD_SUBTRACT: u32 = 333; +pub const NUMPAD_ADD: u32 = 334; +pub const NUMPAD_ENTER: u32 = 335; +pub const NUMPAD_EQUAL: u32 = 336; + +pub const SHIFT_RIGHT: u32 = 344; +pub const CONTROL_RIGHT: u32 = 345; +pub const ALT_RIGHT: u32 = 346; +pub const SUPER_RIGHT: u32 = 347; +pub const CONTEXT_MENU: u32 = 348; diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index f78a8660..f78a7731 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -4088,6 +4088,19 @@ pub const PROCESSING_KEY_F9: u32 = 298; pub const PROCESSING_KEY_F10: u32 = 299; pub const PROCESSING_KEY_F11: u32 = 300; pub const PROCESSING_KEY_F12: u32 = 301; +pub const PROCESSING_KEY_F13: u32 = 302; +pub const PROCESSING_KEY_F14: u32 = 303; +pub const PROCESSING_KEY_F15: u32 = 304; +pub const PROCESSING_KEY_F16: u32 = 305; +pub const PROCESSING_KEY_F17: u32 = 306; +pub const PROCESSING_KEY_F18: u32 = 307; +pub const PROCESSING_KEY_F19: u32 = 308; +pub const PROCESSING_KEY_F20: u32 = 309; +pub const PROCESSING_KEY_F21: u32 = 310; +pub const PROCESSING_KEY_F22: u32 = 311; +pub const PROCESSING_KEY_F23: u32 = 312; +pub const PROCESSING_KEY_F24: u32 = 313; +pub const PROCESSING_KEY_F25: u32 = 314; pub const PROCESSING_KEY_NUMPAD_0: u32 = 320; pub const PROCESSING_KEY_NUMPAD_1: u32 = 321; pub const PROCESSING_KEY_NUMPAD_2: u32 = 322; @@ -4306,7 +4319,8 @@ pub extern "C" fn processing_key() -> u32 { #[unsafe(no_mangle)] pub extern "C" fn processing_key_code() -> u32 { error::clear_error(); - error::check(|| input_key_code().map(|opt| opt.map(key_code_to_u32).unwrap_or(0))).unwrap_or(0) + error::check(|| input_key_code().map(|opt| opt.and_then(key_code_to_u32).unwrap_or(0))) + .unwrap_or(0) } #[unsafe(no_mangle)] @@ -4326,227 +4340,3 @@ pub extern "C" fn processing_mouse_wheel() -> f32 { error::clear_error(); error::check(input_mouse_wheel).unwrap_or(0.0) } - -fn key_code_from_u32(val: u32) -> processing::prelude::error::Result { - match val { - PROCESSING_KEY_SPACE => Ok(KeyCode::Space), - PROCESSING_KEY_QUOTE => Ok(KeyCode::Quote), - PROCESSING_KEY_COMMA => Ok(KeyCode::Comma), - PROCESSING_KEY_MINUS => Ok(KeyCode::Minus), - PROCESSING_KEY_PERIOD => Ok(KeyCode::Period), - PROCESSING_KEY_SLASH => Ok(KeyCode::Slash), - PROCESSING_KEY_0 => Ok(KeyCode::Digit0), - PROCESSING_KEY_1 => Ok(KeyCode::Digit1), - PROCESSING_KEY_2 => Ok(KeyCode::Digit2), - PROCESSING_KEY_3 => Ok(KeyCode::Digit3), - PROCESSING_KEY_4 => Ok(KeyCode::Digit4), - PROCESSING_KEY_5 => Ok(KeyCode::Digit5), - PROCESSING_KEY_6 => Ok(KeyCode::Digit6), - PROCESSING_KEY_7 => Ok(KeyCode::Digit7), - PROCESSING_KEY_8 => Ok(KeyCode::Digit8), - PROCESSING_KEY_9 => Ok(KeyCode::Digit9), - PROCESSING_KEY_SEMICOLON => Ok(KeyCode::Semicolon), - PROCESSING_KEY_EQUAL => Ok(KeyCode::Equal), - PROCESSING_KEY_A => Ok(KeyCode::KeyA), - PROCESSING_KEY_B => Ok(KeyCode::KeyB), - PROCESSING_KEY_C => Ok(KeyCode::KeyC), - PROCESSING_KEY_D => Ok(KeyCode::KeyD), - PROCESSING_KEY_E => Ok(KeyCode::KeyE), - PROCESSING_KEY_F => Ok(KeyCode::KeyF), - PROCESSING_KEY_G => Ok(KeyCode::KeyG), - PROCESSING_KEY_H => Ok(KeyCode::KeyH), - PROCESSING_KEY_I => Ok(KeyCode::KeyI), - PROCESSING_KEY_J => Ok(KeyCode::KeyJ), - PROCESSING_KEY_K => Ok(KeyCode::KeyK), - PROCESSING_KEY_L => Ok(KeyCode::KeyL), - PROCESSING_KEY_M => Ok(KeyCode::KeyM), - PROCESSING_KEY_N => Ok(KeyCode::KeyN), - PROCESSING_KEY_O => Ok(KeyCode::KeyO), - PROCESSING_KEY_P => Ok(KeyCode::KeyP), - PROCESSING_KEY_Q => Ok(KeyCode::KeyQ), - PROCESSING_KEY_R => Ok(KeyCode::KeyR), - PROCESSING_KEY_S => Ok(KeyCode::KeyS), - PROCESSING_KEY_T => Ok(KeyCode::KeyT), - PROCESSING_KEY_U => Ok(KeyCode::KeyU), - PROCESSING_KEY_V => Ok(KeyCode::KeyV), - PROCESSING_KEY_W => Ok(KeyCode::KeyW), - PROCESSING_KEY_X => Ok(KeyCode::KeyX), - PROCESSING_KEY_Y => Ok(KeyCode::KeyY), - PROCESSING_KEY_Z => Ok(KeyCode::KeyZ), - PROCESSING_KEY_BRACKET_LEFT => Ok(KeyCode::BracketLeft), - PROCESSING_KEY_BACKSLASH => Ok(KeyCode::Backslash), - PROCESSING_KEY_BRACKET_RIGHT => Ok(KeyCode::BracketRight), - PROCESSING_KEY_BACKQUOTE => Ok(KeyCode::Backquote), - PROCESSING_KEY_ESCAPE => Ok(KeyCode::Escape), - PROCESSING_KEY_ENTER => Ok(KeyCode::Enter), - PROCESSING_KEY_TAB => Ok(KeyCode::Tab), - PROCESSING_KEY_BACKSPACE => Ok(KeyCode::Backspace), - PROCESSING_KEY_INSERT => Ok(KeyCode::Insert), - PROCESSING_KEY_DELETE => Ok(KeyCode::Delete), - PROCESSING_KEY_RIGHT => Ok(KeyCode::ArrowRight), - PROCESSING_KEY_LEFT => Ok(KeyCode::ArrowLeft), - PROCESSING_KEY_DOWN => Ok(KeyCode::ArrowDown), - PROCESSING_KEY_UP => Ok(KeyCode::ArrowUp), - PROCESSING_KEY_PAGE_UP => Ok(KeyCode::PageUp), - PROCESSING_KEY_PAGE_DOWN => Ok(KeyCode::PageDown), - PROCESSING_KEY_HOME => Ok(KeyCode::Home), - PROCESSING_KEY_END => Ok(KeyCode::End), - PROCESSING_KEY_CAPS_LOCK => Ok(KeyCode::CapsLock), - PROCESSING_KEY_SCROLL_LOCK => Ok(KeyCode::ScrollLock), - PROCESSING_KEY_NUM_LOCK => Ok(KeyCode::NumLock), - PROCESSING_KEY_PRINT_SCREEN => Ok(KeyCode::PrintScreen), - PROCESSING_KEY_PAUSE => Ok(KeyCode::Pause), - PROCESSING_KEY_F1 => Ok(KeyCode::F1), - PROCESSING_KEY_F2 => Ok(KeyCode::F2), - PROCESSING_KEY_F3 => Ok(KeyCode::F3), - PROCESSING_KEY_F4 => Ok(KeyCode::F4), - PROCESSING_KEY_F5 => Ok(KeyCode::F5), - PROCESSING_KEY_F6 => Ok(KeyCode::F6), - PROCESSING_KEY_F7 => Ok(KeyCode::F7), - PROCESSING_KEY_F8 => Ok(KeyCode::F8), - PROCESSING_KEY_F9 => Ok(KeyCode::F9), - PROCESSING_KEY_F10 => Ok(KeyCode::F10), - PROCESSING_KEY_F11 => Ok(KeyCode::F11), - PROCESSING_KEY_F12 => Ok(KeyCode::F12), - PROCESSING_KEY_NUMPAD_0 => Ok(KeyCode::Numpad0), - PROCESSING_KEY_NUMPAD_1 => Ok(KeyCode::Numpad1), - PROCESSING_KEY_NUMPAD_2 => Ok(KeyCode::Numpad2), - PROCESSING_KEY_NUMPAD_3 => Ok(KeyCode::Numpad3), - PROCESSING_KEY_NUMPAD_4 => Ok(KeyCode::Numpad4), - PROCESSING_KEY_NUMPAD_5 => Ok(KeyCode::Numpad5), - PROCESSING_KEY_NUMPAD_6 => Ok(KeyCode::Numpad6), - PROCESSING_KEY_NUMPAD_7 => Ok(KeyCode::Numpad7), - PROCESSING_KEY_NUMPAD_8 => Ok(KeyCode::Numpad8), - PROCESSING_KEY_NUMPAD_9 => Ok(KeyCode::Numpad9), - PROCESSING_KEY_NUMPAD_DECIMAL => Ok(KeyCode::NumpadDecimal), - PROCESSING_KEY_NUMPAD_DIVIDE => Ok(KeyCode::NumpadDivide), - PROCESSING_KEY_NUMPAD_MULTIPLY => Ok(KeyCode::NumpadMultiply), - PROCESSING_KEY_NUMPAD_SUBTRACT => Ok(KeyCode::NumpadSubtract), - PROCESSING_KEY_NUMPAD_ADD => Ok(KeyCode::NumpadAdd), - PROCESSING_KEY_NUMPAD_ENTER => Ok(KeyCode::NumpadEnter), - PROCESSING_KEY_NUMPAD_EQUAL => Ok(KeyCode::NumpadEqual), - PROCESSING_KEY_SHIFT_LEFT => Ok(KeyCode::ShiftLeft), - PROCESSING_KEY_CONTROL_LEFT => Ok(KeyCode::ControlLeft), - PROCESSING_KEY_ALT_LEFT => Ok(KeyCode::AltLeft), - PROCESSING_KEY_SUPER_LEFT => Ok(KeyCode::SuperLeft), - PROCESSING_KEY_SHIFT_RIGHT => Ok(KeyCode::ShiftRight), - PROCESSING_KEY_CONTROL_RIGHT => Ok(KeyCode::ControlRight), - PROCESSING_KEY_ALT_RIGHT => Ok(KeyCode::AltRight), - PROCESSING_KEY_SUPER_RIGHT => Ok(KeyCode::SuperRight), - PROCESSING_KEY_CONTEXT_MENU => Ok(KeyCode::ContextMenu), - _ => Err(ProcessingError::InvalidArgument(format!( - "unknown key code: {val}" - ))), - } -} - -fn key_code_to_u32(kc: KeyCode) -> u32 { - match kc { - KeyCode::Space => PROCESSING_KEY_SPACE, - KeyCode::Quote => PROCESSING_KEY_QUOTE, - KeyCode::Comma => PROCESSING_KEY_COMMA, - KeyCode::Minus => PROCESSING_KEY_MINUS, - KeyCode::Period => PROCESSING_KEY_PERIOD, - KeyCode::Slash => PROCESSING_KEY_SLASH, - KeyCode::Digit0 => PROCESSING_KEY_0, - KeyCode::Digit1 => PROCESSING_KEY_1, - KeyCode::Digit2 => PROCESSING_KEY_2, - KeyCode::Digit3 => PROCESSING_KEY_3, - KeyCode::Digit4 => PROCESSING_KEY_4, - KeyCode::Digit5 => PROCESSING_KEY_5, - KeyCode::Digit6 => PROCESSING_KEY_6, - KeyCode::Digit7 => PROCESSING_KEY_7, - KeyCode::Digit8 => PROCESSING_KEY_8, - KeyCode::Digit9 => PROCESSING_KEY_9, - KeyCode::Semicolon => PROCESSING_KEY_SEMICOLON, - KeyCode::Equal => PROCESSING_KEY_EQUAL, - KeyCode::KeyA => PROCESSING_KEY_A, - KeyCode::KeyB => PROCESSING_KEY_B, - KeyCode::KeyC => PROCESSING_KEY_C, - KeyCode::KeyD => PROCESSING_KEY_D, - KeyCode::KeyE => PROCESSING_KEY_E, - KeyCode::KeyF => PROCESSING_KEY_F, - KeyCode::KeyG => PROCESSING_KEY_G, - KeyCode::KeyH => PROCESSING_KEY_H, - KeyCode::KeyI => PROCESSING_KEY_I, - KeyCode::KeyJ => PROCESSING_KEY_J, - KeyCode::KeyK => PROCESSING_KEY_K, - KeyCode::KeyL => PROCESSING_KEY_L, - KeyCode::KeyM => PROCESSING_KEY_M, - KeyCode::KeyN => PROCESSING_KEY_N, - KeyCode::KeyO => PROCESSING_KEY_O, - KeyCode::KeyP => PROCESSING_KEY_P, - KeyCode::KeyQ => PROCESSING_KEY_Q, - KeyCode::KeyR => PROCESSING_KEY_R, - KeyCode::KeyS => PROCESSING_KEY_S, - KeyCode::KeyT => PROCESSING_KEY_T, - KeyCode::KeyU => PROCESSING_KEY_U, - KeyCode::KeyV => PROCESSING_KEY_V, - KeyCode::KeyW => PROCESSING_KEY_W, - KeyCode::KeyX => PROCESSING_KEY_X, - KeyCode::KeyY => PROCESSING_KEY_Y, - KeyCode::KeyZ => PROCESSING_KEY_Z, - KeyCode::BracketLeft => PROCESSING_KEY_BRACKET_LEFT, - KeyCode::Backslash => PROCESSING_KEY_BACKSLASH, - KeyCode::BracketRight => PROCESSING_KEY_BRACKET_RIGHT, - KeyCode::Backquote => PROCESSING_KEY_BACKQUOTE, - KeyCode::Escape => PROCESSING_KEY_ESCAPE, - KeyCode::Enter => PROCESSING_KEY_ENTER, - KeyCode::Tab => PROCESSING_KEY_TAB, - KeyCode::Backspace => PROCESSING_KEY_BACKSPACE, - KeyCode::Insert => PROCESSING_KEY_INSERT, - KeyCode::Delete => PROCESSING_KEY_DELETE, - KeyCode::ArrowRight => PROCESSING_KEY_RIGHT, - KeyCode::ArrowLeft => PROCESSING_KEY_LEFT, - KeyCode::ArrowDown => PROCESSING_KEY_DOWN, - KeyCode::ArrowUp => PROCESSING_KEY_UP, - KeyCode::PageUp => PROCESSING_KEY_PAGE_UP, - KeyCode::PageDown => PROCESSING_KEY_PAGE_DOWN, - KeyCode::Home => PROCESSING_KEY_HOME, - KeyCode::End => PROCESSING_KEY_END, - KeyCode::CapsLock => PROCESSING_KEY_CAPS_LOCK, - KeyCode::ScrollLock => PROCESSING_KEY_SCROLL_LOCK, - KeyCode::NumLock => PROCESSING_KEY_NUM_LOCK, - KeyCode::PrintScreen => PROCESSING_KEY_PRINT_SCREEN, - KeyCode::Pause => PROCESSING_KEY_PAUSE, - KeyCode::F1 => PROCESSING_KEY_F1, - KeyCode::F2 => PROCESSING_KEY_F2, - KeyCode::F3 => PROCESSING_KEY_F3, - KeyCode::F4 => PROCESSING_KEY_F4, - KeyCode::F5 => PROCESSING_KEY_F5, - KeyCode::F6 => PROCESSING_KEY_F6, - KeyCode::F7 => PROCESSING_KEY_F7, - KeyCode::F8 => PROCESSING_KEY_F8, - KeyCode::F9 => PROCESSING_KEY_F9, - KeyCode::F10 => PROCESSING_KEY_F10, - KeyCode::F11 => PROCESSING_KEY_F11, - KeyCode::F12 => PROCESSING_KEY_F12, - KeyCode::Numpad0 => PROCESSING_KEY_NUMPAD_0, - KeyCode::Numpad1 => PROCESSING_KEY_NUMPAD_1, - KeyCode::Numpad2 => PROCESSING_KEY_NUMPAD_2, - KeyCode::Numpad3 => PROCESSING_KEY_NUMPAD_3, - KeyCode::Numpad4 => PROCESSING_KEY_NUMPAD_4, - KeyCode::Numpad5 => PROCESSING_KEY_NUMPAD_5, - KeyCode::Numpad6 => PROCESSING_KEY_NUMPAD_6, - KeyCode::Numpad7 => PROCESSING_KEY_NUMPAD_7, - KeyCode::Numpad8 => PROCESSING_KEY_NUMPAD_8, - KeyCode::Numpad9 => PROCESSING_KEY_NUMPAD_9, - KeyCode::NumpadDecimal => PROCESSING_KEY_NUMPAD_DECIMAL, - KeyCode::NumpadDivide => PROCESSING_KEY_NUMPAD_DIVIDE, - KeyCode::NumpadMultiply => PROCESSING_KEY_NUMPAD_MULTIPLY, - KeyCode::NumpadSubtract => PROCESSING_KEY_NUMPAD_SUBTRACT, - KeyCode::NumpadAdd => PROCESSING_KEY_NUMPAD_ADD, - KeyCode::NumpadEnter => PROCESSING_KEY_NUMPAD_ENTER, - KeyCode::NumpadEqual => PROCESSING_KEY_NUMPAD_EQUAL, - KeyCode::ShiftLeft => PROCESSING_KEY_SHIFT_LEFT, - KeyCode::ControlLeft => PROCESSING_KEY_CONTROL_LEFT, - KeyCode::AltLeft => PROCESSING_KEY_ALT_LEFT, - KeyCode::SuperLeft => PROCESSING_KEY_SUPER_LEFT, - KeyCode::ShiftRight => PROCESSING_KEY_SHIFT_RIGHT, - KeyCode::ControlRight => PROCESSING_KEY_CONTROL_RIGHT, - KeyCode::AltRight => PROCESSING_KEY_ALT_RIGHT, - KeyCode::SuperRight => PROCESSING_KEY_SUPER_RIGHT, - KeyCode::ContextMenu => PROCESSING_KEY_CONTEXT_MENU, - _ => 0, - } -} diff --git a/crates/processing_input/src/keys.rs b/crates/processing_input/src/keys.rs new file mode 100644 index 00000000..4f6c661d --- /dev/null +++ b/crates/processing_input/src/keys.rs @@ -0,0 +1,145 @@ +//! The numeric key codes front ends expose (GLFW values, see +//! `processing_core::constants`) and their Bevy `KeyCode`s. + +use bevy::input::keyboard::KeyCode; +use processing_core::constants as c; +use processing_core::error::{ProcessingError, Result}; + +macro_rules! key_table { + ($($code:ident => $key:ident),* $(,)?) => { + pub fn key_code_from_u32(code: u32) -> Result { + match code { + $(c::$code => Ok(KeyCode::$key),)* + _ => Err(ProcessingError::InvalidArgument(format!("unknown key code: {code}"))), + } + } + + pub fn key_code_to_u32(key: KeyCode) -> Option { + match key { + $(KeyCode::$key => Some(c::$code),)* + _ => None, + } + } + }; +} + +key_table! { + KEY_A => KeyA, + KEY_B => KeyB, + KEY_C => KeyC, + KEY_D => KeyD, + KEY_E => KeyE, + KEY_F => KeyF, + KEY_G => KeyG, + KEY_H => KeyH, + KEY_I => KeyI, + KEY_J => KeyJ, + KEY_K => KeyK, + KEY_L => KeyL, + KEY_M => KeyM, + KEY_N => KeyN, + KEY_O => KeyO, + KEY_P => KeyP, + KEY_Q => KeyQ, + KEY_R => KeyR, + KEY_S => KeyS, + KEY_T => KeyT, + KEY_U => KeyU, + KEY_V => KeyV, + KEY_W => KeyW, + KEY_X => KeyX, + KEY_Y => KeyY, + KEY_Z => KeyZ, + KEY_0 => Digit0, + KEY_1 => Digit1, + KEY_2 => Digit2, + KEY_3 => Digit3, + KEY_4 => Digit4, + KEY_5 => Digit5, + KEY_6 => Digit6, + KEY_7 => Digit7, + KEY_8 => Digit8, + KEY_9 => Digit9, + SPACE => Space, + QUOTE => Quote, + COMMA => Comma, + MINUS => Minus, + PERIOD => Period, + SLASH => Slash, + SEMICOLON => Semicolon, + EQUAL => Equal, + BRACKET_LEFT => BracketLeft, + BACKSLASH => Backslash, + BRACKET_RIGHT => BracketRight, + BACKQUOTE => Backquote, + ESCAPE => Escape, + ENTER => Enter, + TAB => Tab, + BACKSPACE => Backspace, + INSERT => Insert, + DELETE => Delete, + RIGHT_ARROW => ArrowRight, + LEFT_ARROW => ArrowLeft, + DOWN => ArrowDown, + UP => ArrowUp, + PAGE_UP => PageUp, + PAGE_DOWN => PageDown, + HOME => Home, + END => End, + CAPS_LOCK => CapsLock, + SCROLL_LOCK => ScrollLock, + NUM_LOCK => NumLock, + PRINT_SCREEN => PrintScreen, + PAUSE => Pause, + F1 => F1, + F2 => F2, + F3 => F3, + F4 => F4, + F5 => F5, + F6 => F6, + F7 => F7, + F8 => F8, + F9 => F9, + F10 => F10, + F11 => F11, + F12 => F12, + F13 => F13, + F14 => F14, + F15 => F15, + F16 => F16, + F17 => F17, + F18 => F18, + F19 => F19, + F20 => F20, + F21 => F21, + F22 => F22, + F23 => F23, + F24 => F24, + F25 => F25, + NUMPAD_0 => Numpad0, + NUMPAD_1 => Numpad1, + NUMPAD_2 => Numpad2, + NUMPAD_3 => Numpad3, + NUMPAD_4 => Numpad4, + NUMPAD_5 => Numpad5, + NUMPAD_6 => Numpad6, + NUMPAD_7 => Numpad7, + NUMPAD_8 => Numpad8, + NUMPAD_9 => Numpad9, + NUMPAD_DECIMAL => NumpadDecimal, + NUMPAD_DIVIDE => NumpadDivide, + NUMPAD_MULTIPLY => NumpadMultiply, + NUMPAD_SUBTRACT => NumpadSubtract, + NUMPAD_ADD => NumpadAdd, + NUMPAD_ENTER => NumpadEnter, + NUMPAD_EQUAL => NumpadEqual, + SHIFT => ShiftLeft, + CONTROL => ControlLeft, + ALT => AltLeft, + SUPER => SuperLeft, + SHIFT_RIGHT => ShiftRight, + CONTROL_RIGHT => ControlRight, + ALT_RIGHT => AltRight, + SUPER_RIGHT => SuperRight, + CONTEXT_MENU => ContextMenu, +} diff --git a/crates/processing_input/src/lib.rs b/crates/processing_input/src/lib.rs index 118a295f..5c758f33 100644 --- a/crates/processing_input/src/lib.rs +++ b/crates/processing_input/src/lib.rs @@ -1,3 +1,4 @@ +pub mod keys; pub mod state; use bevy::input::ButtonState; @@ -14,6 +15,7 @@ use bevy::window::{CursorMoved, WindowResized}; use processing_core::app_mut; use processing_core::error; +pub use keys::{key_code_from_u32, key_code_to_u32}; pub use state::{CursorPosition, LastKey, LastMouseButton}; pub struct InputPlugin; diff --git a/crates/processing_pyo3/mewnala/__init__.py b/crates/processing_pyo3/mewnala/__init__.py index f4fd0a28..c6c570ac 100644 --- a/crates/processing_pyo3/mewnala/__init__.py +++ b/crates/processing_pyo3/mewnala/__init__.py @@ -27,7 +27,6 @@ "width", "height", "focused", - "pixel_density", "pixel_width", "pixel_height", "mouse_x", @@ -36,12 +35,13 @@ "pmouse_y", "mouse_is_pressed", "mouse_button", - "mouse_wheel", "moved_x", "moved_y", "key", "key_code", "key_is_pressed", + "window_x", + "window_y", ) _DYNAMIC_TIME_ATTRS = ( @@ -54,7 +54,6 @@ "width": 100, "height": 100, "focused": False, - "pixel_density": 1.0, "pixel_width": 100, "pixel_height": 100, "mouse_x": 0.0, @@ -63,18 +62,19 @@ "pmouse_y": 0.0, "mouse_is_pressed": False, "mouse_button": None, - "mouse_wheel": 0.0, "moved_x": 0.0, "moved_y": 0.0, "key": None, "key_code": None, "key_is_pressed": False, + "window_x": 0, + "window_y": 0, } _DYNAMIC = ( _DYNAMIC_GRAPHICS_ATTRS + _DYNAMIC_TIME_ATTRS - + ("display_width", "display_height", "window_x", "window_y") + + ("display_width", "display_height") ) @@ -104,16 +104,6 @@ def __getattr__(name): if mon is None: return 0 return mon.width if name == "display_width" else mon.height - if name in ("window_x", "window_y"): - g = _get_graphics() - if g is None: - return 0 - # offscreen canvases (notebooks, embedding hosts) have no window position - try: - x, y = g.surface.position - except (AttributeError, RuntimeError): - return 0 - return x if name == "window_x" else y raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/crates/processing_pyo3/src/constants.rs b/crates/processing_pyo3/src/constants.rs index 99486f82..5a5673ce 100644 --- a/crates/processing_pyo3/src/constants.rs +++ b/crates/processing_pyo3/src/constants.rs @@ -135,8 +135,41 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { HOME, END ); + add!( + m, + CAPS_LOCK, + SCROLL_LOCK, + NUM_LOCK, + PRINT_SCREEN, + PAUSE, + CONTEXT_MENU + ); add!(m, SHIFT, CONTROL, ALT, SUPER); - add!(m, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12); + add!(m, SHIFT_RIGHT, CONTROL_RIGHT, ALT_RIGHT, SUPER_RIGHT); + add!( + m, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, F13, F14, F15, F16, F17, F18, F19, + F20, F21, F22, F23, F24, F25 + ); + add!( + m, + NUMPAD_0, + NUMPAD_1, + NUMPAD_2, + NUMPAD_3, + NUMPAD_4, + NUMPAD_5, + NUMPAD_6, + NUMPAD_7, + NUMPAD_8, + NUMPAD_9, + NUMPAD_DECIMAL, + NUMPAD_DIVIDE, + NUMPAD_MULTIPLY, + NUMPAD_SUBTRACT, + NUMPAD_ADD, + NUMPAD_ENTER, + NUMPAD_EQUAL + ); // Objects rather than strings, passed straight to blend_mode(). m.add("BLEND", PyBlendMode::from_preset(BlendMode::Blend))?; diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index 591fd911..c51052f4 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -971,6 +971,17 @@ impl Graphics { self.surface.pixel_width() } + /// 0 for offscreen canvases, which have no window. + #[getter] + pub fn window_x(&self) -> i32 { + self.surface.position().map_or(0, |(x, _)| x) + } + + #[getter] + pub fn window_y(&self) -> i32 { + self.surface.position().map_or(0, |(_, y)| y) + } + #[getter] pub fn pixel_height(&self) -> PyResult { self.surface.pixel_height() @@ -2610,11 +2621,6 @@ impl Graphics { input::moved_y() } - #[getter] - fn mouse_wheel(&self) -> PyResult { - input::mouse_wheel() - } - #[getter] fn key(&self) -> PyResult> { input::key() diff --git a/crates/processing_pyo3/src/input.rs b/crates/processing_pyo3/src/input.rs index 1126d3fb..8c811a99 100644 --- a/crates/processing_pyo3/src/input.rs +++ b/crates/processing_pyo3/src/input.rs @@ -64,12 +64,12 @@ pub fn key_is_pressed() -> PyResult { } pub fn key_is_down(key_code: u32) -> PyResult { - let kc = u32_to_key_code(key_code)?; + let kc = key_code_from_u32(key_code).map_err(|e| PyValueError::new_err(format!("{e}")))?; processing::prelude::input_key_is_down(kc).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } pub fn key_just_pressed(key_code: u32) -> PyResult { - let kc = u32_to_key_code(key_code)?; + let kc = key_code_from_u32(key_code).map_err(|e| PyValueError::new_err(format!("{e}")))?; processing::prelude::input_key_just_pressed(kc) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -82,7 +82,7 @@ pub fn key() -> PyResult> { pub fn key_code() -> PyResult> { processing::prelude::input_key_code() - .map(|opt| opt.map(key_code_to_u32)) + .map(|opt| opt.and_then(key_code_to_u32)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -101,129 +101,62 @@ pub fn sync_globals( set_tracked(globals, "mouse_button", mouse_button()?)?; set_tracked(globals, "moved_x", moved_x()?)?; set_tracked(globals, "moved_y", moved_y()?)?; - set_tracked(globals, "mouse_wheel", mouse_wheel()?)?; set_tracked(globals, "key", key()?)?; set_tracked(globals, "key_code", key_code()?)?; set_tracked(globals, "key_is_pressed", key_is_pressed()?)?; Ok(()) } -fn u32_to_key_code(val: u32) -> PyResult { - use bevy::input::keyboard::KeyCode; - match val { - 32 => Ok(KeyCode::Space), - 256 => Ok(KeyCode::Escape), - 257 => Ok(KeyCode::Enter), - 258 => Ok(KeyCode::Tab), - 259 => Ok(KeyCode::Backspace), - 261 => Ok(KeyCode::Delete), - 262 => Ok(KeyCode::ArrowRight), - 263 => Ok(KeyCode::ArrowLeft), - 264 => Ok(KeyCode::ArrowDown), - 265 => Ok(KeyCode::ArrowUp), - 340 => Ok(KeyCode::ShiftLeft), - 341 => Ok(KeyCode::ControlLeft), - 342 => Ok(KeyCode::AltLeft), - 343 => Ok(KeyCode::SuperLeft), - 65..=90 => Ok(match val { - 65 => KeyCode::KeyA, - 66 => KeyCode::KeyB, - 67 => KeyCode::KeyC, - 68 => KeyCode::KeyD, - 69 => KeyCode::KeyE, - 70 => KeyCode::KeyF, - 71 => KeyCode::KeyG, - 72 => KeyCode::KeyH, - 73 => KeyCode::KeyI, - 74 => KeyCode::KeyJ, - 75 => KeyCode::KeyK, - 76 => KeyCode::KeyL, - 77 => KeyCode::KeyM, - 78 => KeyCode::KeyN, - 79 => KeyCode::KeyO, - 80 => KeyCode::KeyP, - 81 => KeyCode::KeyQ, - 82 => KeyCode::KeyR, - 83 => KeyCode::KeyS, - 84 => KeyCode::KeyT, - 85 => KeyCode::KeyU, - 86 => KeyCode::KeyV, - 87 => KeyCode::KeyW, - 88 => KeyCode::KeyX, - 89 => KeyCode::KeyY, - 90 => KeyCode::KeyZ, - _ => unreachable!(), - }), - 48..=57 => Ok(match val { - 48 => KeyCode::Digit0, - 49 => KeyCode::Digit1, - 50 => KeyCode::Digit2, - 51 => KeyCode::Digit3, - 52 => KeyCode::Digit4, - 53 => KeyCode::Digit5, - 54 => KeyCode::Digit6, - 55 => KeyCode::Digit7, - 56 => KeyCode::Digit8, - 57 => KeyCode::Digit9, - _ => unreachable!(), - }), - _ => Err(PyValueError::new_err(format!("unknown key code: {val}"))), +/// Passed to mouse callbacks that take a parameter, e.g. `def mouse_wheel(e)`. +#[pyclass(frozen, get_all)] +pub struct MouseEvent { + pub x: f32, + pub y: f32, + pub button: Option, + /// Vertical scroll since the last frame. + pub wheel: f32, +} + +impl MouseEvent { + pub fn current(surface: Entity, width: u32, height: u32) -> PyResult { + Ok(Self { + x: mouse_x(surface, width)?, + y: mouse_y(surface, height)?, + button: mouse_button()?, + wheel: mouse_wheel()?, + }) + } +} + +#[pymethods] +impl MouseEvent { + fn __repr__(&self) -> String { + format!( + "MouseEvent(x={}, y={}, button={:?}, wheel={})", + self.x, self.y, self.button, self.wheel + ) + } +} + +/// Passed to key callbacks that take a parameter, e.g. `def key_pressed(e)`. +#[pyclass(frozen, get_all)] +pub struct KeyEvent { + pub key: Option, + pub key_code: Option, +} + +impl KeyEvent { + pub fn current() -> PyResult { + Ok(Self { + key: key()?, + key_code: key_code()?, + }) } } -fn key_code_to_u32(kc: bevy::input::keyboard::KeyCode) -> u32 { - use bevy::input::keyboard::KeyCode; - match kc { - KeyCode::Space => 32, - KeyCode::Escape => 256, - KeyCode::Enter => 257, - KeyCode::Tab => 258, - KeyCode::Backspace => 259, - KeyCode::Delete => 261, - KeyCode::ArrowRight => 262, - KeyCode::ArrowLeft => 263, - KeyCode::ArrowDown => 264, - KeyCode::ArrowUp => 265, - KeyCode::ShiftLeft => 340, - KeyCode::ControlLeft => 341, - KeyCode::AltLeft => 342, - KeyCode::SuperLeft => 343, - KeyCode::KeyA => 65, - KeyCode::KeyB => 66, - KeyCode::KeyC => 67, - KeyCode::KeyD => 68, - KeyCode::KeyE => 69, - KeyCode::KeyF => 70, - KeyCode::KeyG => 71, - KeyCode::KeyH => 72, - KeyCode::KeyI => 73, - KeyCode::KeyJ => 74, - KeyCode::KeyK => 75, - KeyCode::KeyL => 76, - KeyCode::KeyM => 77, - KeyCode::KeyN => 78, - KeyCode::KeyO => 79, - KeyCode::KeyP => 80, - KeyCode::KeyQ => 81, - KeyCode::KeyR => 82, - KeyCode::KeyS => 83, - KeyCode::KeyT => 84, - KeyCode::KeyU => 85, - KeyCode::KeyV => 86, - KeyCode::KeyW => 87, - KeyCode::KeyX => 88, - KeyCode::KeyY => 89, - KeyCode::KeyZ => 90, - KeyCode::Digit0 => 48, - KeyCode::Digit1 => 49, - KeyCode::Digit2 => 50, - KeyCode::Digit3 => 51, - KeyCode::Digit4 => 52, - KeyCode::Digit5 => 53, - KeyCode::Digit6 => 54, - KeyCode::Digit7 => 55, - KeyCode::Digit8 => 56, - KeyCode::Digit9 => 57, - _ => 0, +#[pymethods] +impl KeyEvent { + fn __repr__(&self) -> String { + format!("KeyEvent(key={:?}, key_code={:?})", self.key, self.key_code) } } diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 01012978..9f505cee 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -181,57 +181,99 @@ pub(crate) fn reset_tracked_globals() { LAST_GLOBALS.with(|cache| cache.borrow_mut().clear()); } +fn canvas_size(graphics: &Graphics) -> PyResult<(u32, u32)> { + let err = + |e: processing::prelude::error::ProcessingError| PyRuntimeError::new_err(format!("{e}")); + let width = ::processing::prelude::surface_width(graphics.surface.entity).map_err(err)?; + let height = ::processing::prelude::surface_height(graphics.surface.entity).map_err(err)?; + Ok(( + if width == 0 { graphics.width } else { width }, + if height == 0 { graphics.height } else { height }, + )) +} + fn sync_globals(module: &Bound<'_, PyModule>, globals: &Bound<'_, PyAny>) -> PyResult<()> { let graphics = get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; - let width = ::processing::prelude::surface_width(graphics.surface.entity) - .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; - let width = if width == 0 { graphics.width } else { width }; - let height = ::processing::prelude::surface_height(graphics.surface.entity) - .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; - let height = if height == 0 { graphics.height } else { height }; + let (width, height) = canvas_size(&graphics)?; input::sync_globals(globals, graphics.surface.entity, width, height)?; surface::sync_globals(globals, &graphics.surface, width, height)?; time::sync_globals(globals)?; Ok(()) } -fn try_call(locals: &Bound<'_, PyAny>, name: &str) -> PyResult<()> { - if let Ok(cb) = locals.get_item(name) - && cb.is_callable() - { - cb.call0() - .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; +fn mouse_event(module: &Bound<'_, PyModule>) -> PyResult> { + // the graphics borrow has to end before the callback runs, which may draw + let event = { + let graphics = + get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; + let (width, height) = canvas_size(&graphics)?; + input::MouseEvent::current(graphics.surface.entity, width, height)? + }; + Ok(Py::new(module.py(), event)?.into_any()) +} + +fn key_event(module: &Bound<'_, PyModule>) -> PyResult> { + Ok(Py::new(module.py(), input::KeyEvent::current()?)?.into_any()) +} + +/// Calls the sketch's `name` callback, with an event if it takes a parameter +/// (`def mouse_pressed(e)`), as py5 and Processing's `mousePressed(MouseEvent)` do. +fn call_event_callback( + module: &Bound<'_, PyModule>, + locals: &Bound<'_, PyAny>, + name: &str, + event: fn(&Bound<'_, PyModule>) -> PyResult>, +) -> PyResult<()> { + let Ok(cb) = locals.get_item(name) else { + return Ok(()); + }; + if !cb.is_callable() { + return Ok(()); + } + let inspect = PyModule::import(module.py(), "inspect")?; + // builtins without a signature can't take our event either + let takes_event = inspect + .call_method1("signature", (&cb,)) + .and_then(|sig| sig.getattr("parameters")?.len()) + .is_ok_and(|n| n > 0); + if takes_event { + cb.call1((event(module)?,))?; + } else { + cb.call0()?; } Ok(()) } -fn dispatch_event_callbacks(locals: &Bound<'_, PyAny>) -> PyResult<()> { +fn dispatch_event_callbacks( + module: &Bound<'_, PyModule>, + locals: &Bound<'_, PyAny>, +) -> PyResult<()> { use processing::prelude::*; let err = |e: processing::prelude::error::ProcessingError| PyRuntimeError::new_err(format!("{e}")); if input_mouse_any_just_pressed().map_err(err)? { - try_call(locals, "mouse_pressed")?; + call_event_callback(module, locals, "mouse_pressed", mouse_event)?; } if input_mouse_any_just_released().map_err(err)? { - try_call(locals, "mouse_released")?; + call_event_callback(module, locals, "mouse_released", mouse_event)?; } if input_mouse_moved().map_err(err)? { if input_mouse_is_pressed().map_err(err)? { - try_call(locals, "mouse_dragged")?; + call_event_callback(module, locals, "mouse_dragged", mouse_event)?; } else { - try_call(locals, "mouse_moved")?; + call_event_callback(module, locals, "mouse_moved", mouse_event)?; } } if input_mouse_scrolled().map_err(err)? { - try_call(locals, "mouse_wheel")?; + call_event_callback(module, locals, "mouse_wheel", mouse_event)?; } if input_key_any_just_pressed().map_err(err)? { - try_call(locals, "key_pressed")?; + call_event_callback(module, locals, "key_pressed", key_event)?; } if input_key_any_just_released().map_err(err)? { - try_call(locals, "key_released")?; + call_event_callback(module, locals, "key_released", key_event)?; } Ok(()) } @@ -360,18 +402,11 @@ fn get_asset_root() -> PyResult { // in ipython/jupyter argv[0] is weird so we use cwd // todo: what is the correct way to get notebook path if filename.is_empty() || !path.getattr("isfile")?.call1((filename,))?.is_truthy()? { - let cwd = os.getattr("getcwd")?.call0()?.to_string(); - let asset_root = path.getattr("join")?.call1((cwd, "assets"))?.to_string(); - return Ok(asset_root); + return Ok(os.getattr("getcwd")?.call0()?.to_string()); } let dirname = path.getattr("dirname")?.call1((filename,))?; - let abspath = path.getattr("abspath")?.call1((dirname,))?; - let asset_root = path - .getattr("join")? - .call1((abspath, "assets"))? - .to_string(); - Ok(asset_root) + Ok(path.getattr("abspath")?.call1((dirname,))?.to_string()) }) } @@ -471,6 +506,10 @@ pub mod mewnala { #[pymodule_export] use super::cuda::CudaImage; #[pymodule_export] + use super::input::KeyEvent; + #[pymodule_export] + use super::input::MouseEvent; + #[pymodule_export] use super::math::PyQuat; #[pymodule_export] use super::math::PyVec2; @@ -693,7 +732,7 @@ pub mod mewnala { return Ok(()); } sync_globals(module, ns)?; - dispatch_event_callbacks(ns)?; + dispatch_event_callbacks(module, ns)?; Ok(()) } @@ -1092,7 +1131,7 @@ pub mod mewnala { break; } } - dispatch_event_callbacks(&locals)?; + dispatch_event_callbacks(module, &locals)?; std::thread::sleep(std::time::Duration::from_millis(16)); } @@ -1134,7 +1173,7 @@ pub mod mewnala { } } - dispatch_event_callbacks(&locals)?; + dispatch_event_callbacks(module, &locals)?; let should_draw = first_frame || LOOP_STATE.with(|s| { @@ -1160,9 +1199,7 @@ pub mod mewnala { 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()?; } diff --git a/crates/processing_pyo3/src/surface.rs b/crates/processing_pyo3/src/surface.rs index 6aa70901..72828f58 100644 --- a/crates/processing_pyo3/src/surface.rs +++ b/crates/processing_pyo3/src/surface.rs @@ -187,7 +187,6 @@ pub fn sync_globals( set_tracked(globals, "width", canvas_width)?; set_tracked(globals, "height", canvas_height)?; set_tracked(globals, "focused", surface.focused()?)?; - set_tracked(globals, "pixel_density", surface.pixel_density()?)?; set_tracked(globals, "pixel_width", surface.pixel_width()?)?; set_tracked(globals, "pixel_height", surface.pixel_height()?)?; diff --git a/crates/processing_wasm/src/lib.rs b/crates/processing_wasm/src/lib.rs index 4131a7a8..dff9e0ab 100644 --- a/crates/processing_wasm/src/lib.rs +++ b/crates/processing_wasm/src/lib.rs @@ -1872,6 +1872,19 @@ pub const PROCESSING_KEY_F9: u32 = 298; pub const PROCESSING_KEY_F10: u32 = 299; pub const PROCESSING_KEY_F11: u32 = 300; pub const PROCESSING_KEY_F12: u32 = 301; +pub const PROCESSING_KEY_F13: u32 = 302; +pub const PROCESSING_KEY_F14: u32 = 303; +pub const PROCESSING_KEY_F15: u32 = 304; +pub const PROCESSING_KEY_F16: u32 = 305; +pub const PROCESSING_KEY_F17: u32 = 306; +pub const PROCESSING_KEY_F18: u32 = 307; +pub const PROCESSING_KEY_F19: u32 = 308; +pub const PROCESSING_KEY_F20: u32 = 309; +pub const PROCESSING_KEY_F21: u32 = 310; +pub const PROCESSING_KEY_F22: u32 = 311; +pub const PROCESSING_KEY_F23: u32 = 312; +pub const PROCESSING_KEY_F24: u32 = 313; +pub const PROCESSING_KEY_F25: u32 = 314; pub const PROCESSING_KEY_NUMPAD_0: u32 = 320; pub const PROCESSING_KEY_NUMPAD_1: u32 = 321; pub const PROCESSING_KEY_NUMPAD_2: u32 = 322; @@ -2028,7 +2041,7 @@ pub fn js_key() -> Result { #[wasm_bindgen(js_name = "keyCode")] pub fn js_key_code() -> Result { - check(input_key_code().map(|opt| opt.map(key_code_to_u32).unwrap_or(0))) + check(input_key_code().map(|opt| opt.and_then(key_code_to_u32).unwrap_or(0))) } #[wasm_bindgen(js_name = "movedX")] @@ -2046,230 +2059,6 @@ pub fn js_mouse_wheel() -> Result { check(input_mouse_wheel()) } -fn key_code_from_u32(val: u32) -> processing::prelude::error::Result { - match val { - PROCESSING_KEY_SPACE => Ok(KeyCode::Space), - PROCESSING_KEY_QUOTE => Ok(KeyCode::Quote), - PROCESSING_KEY_COMMA => Ok(KeyCode::Comma), - PROCESSING_KEY_MINUS => Ok(KeyCode::Minus), - PROCESSING_KEY_PERIOD => Ok(KeyCode::Period), - PROCESSING_KEY_SLASH => Ok(KeyCode::Slash), - PROCESSING_KEY_0 => Ok(KeyCode::Digit0), - PROCESSING_KEY_1 => Ok(KeyCode::Digit1), - PROCESSING_KEY_2 => Ok(KeyCode::Digit2), - PROCESSING_KEY_3 => Ok(KeyCode::Digit3), - PROCESSING_KEY_4 => Ok(KeyCode::Digit4), - PROCESSING_KEY_5 => Ok(KeyCode::Digit5), - PROCESSING_KEY_6 => Ok(KeyCode::Digit6), - PROCESSING_KEY_7 => Ok(KeyCode::Digit7), - PROCESSING_KEY_8 => Ok(KeyCode::Digit8), - PROCESSING_KEY_9 => Ok(KeyCode::Digit9), - PROCESSING_KEY_SEMICOLON => Ok(KeyCode::Semicolon), - PROCESSING_KEY_EQUAL => Ok(KeyCode::Equal), - PROCESSING_KEY_A => Ok(KeyCode::KeyA), - PROCESSING_KEY_B => Ok(KeyCode::KeyB), - PROCESSING_KEY_C => Ok(KeyCode::KeyC), - PROCESSING_KEY_D => Ok(KeyCode::KeyD), - PROCESSING_KEY_E => Ok(KeyCode::KeyE), - PROCESSING_KEY_F => Ok(KeyCode::KeyF), - PROCESSING_KEY_G => Ok(KeyCode::KeyG), - PROCESSING_KEY_H => Ok(KeyCode::KeyH), - PROCESSING_KEY_I => Ok(KeyCode::KeyI), - PROCESSING_KEY_J => Ok(KeyCode::KeyJ), - PROCESSING_KEY_K => Ok(KeyCode::KeyK), - PROCESSING_KEY_L => Ok(KeyCode::KeyL), - PROCESSING_KEY_M => Ok(KeyCode::KeyM), - PROCESSING_KEY_N => Ok(KeyCode::KeyN), - PROCESSING_KEY_O => Ok(KeyCode::KeyO), - PROCESSING_KEY_P => Ok(KeyCode::KeyP), - PROCESSING_KEY_Q => Ok(KeyCode::KeyQ), - PROCESSING_KEY_R => Ok(KeyCode::KeyR), - PROCESSING_KEY_S => Ok(KeyCode::KeyS), - PROCESSING_KEY_T => Ok(KeyCode::KeyT), - PROCESSING_KEY_U => Ok(KeyCode::KeyU), - PROCESSING_KEY_V => Ok(KeyCode::KeyV), - PROCESSING_KEY_W => Ok(KeyCode::KeyW), - PROCESSING_KEY_X => Ok(KeyCode::KeyX), - PROCESSING_KEY_Y => Ok(KeyCode::KeyY), - PROCESSING_KEY_Z => Ok(KeyCode::KeyZ), - PROCESSING_KEY_BRACKET_LEFT => Ok(KeyCode::BracketLeft), - PROCESSING_KEY_BACKSLASH => Ok(KeyCode::Backslash), - PROCESSING_KEY_BRACKET_RIGHT => Ok(KeyCode::BracketRight), - PROCESSING_KEY_BACKQUOTE => Ok(KeyCode::Backquote), - PROCESSING_KEY_ESCAPE => Ok(KeyCode::Escape), - PROCESSING_KEY_ENTER => Ok(KeyCode::Enter), - PROCESSING_KEY_TAB => Ok(KeyCode::Tab), - PROCESSING_KEY_BACKSPACE => Ok(KeyCode::Backspace), - PROCESSING_KEY_INSERT => Ok(KeyCode::Insert), - PROCESSING_KEY_DELETE => Ok(KeyCode::Delete), - PROCESSING_KEY_RIGHT => Ok(KeyCode::ArrowRight), - PROCESSING_KEY_LEFT => Ok(KeyCode::ArrowLeft), - PROCESSING_KEY_DOWN => Ok(KeyCode::ArrowDown), - PROCESSING_KEY_UP => Ok(KeyCode::ArrowUp), - PROCESSING_KEY_PAGE_UP => Ok(KeyCode::PageUp), - PROCESSING_KEY_PAGE_DOWN => Ok(KeyCode::PageDown), - PROCESSING_KEY_HOME => Ok(KeyCode::Home), - PROCESSING_KEY_END => Ok(KeyCode::End), - PROCESSING_KEY_CAPS_LOCK => Ok(KeyCode::CapsLock), - PROCESSING_KEY_SCROLL_LOCK => Ok(KeyCode::ScrollLock), - PROCESSING_KEY_NUM_LOCK => Ok(KeyCode::NumLock), - PROCESSING_KEY_PRINT_SCREEN => Ok(KeyCode::PrintScreen), - PROCESSING_KEY_PAUSE => Ok(KeyCode::Pause), - PROCESSING_KEY_F1 => Ok(KeyCode::F1), - PROCESSING_KEY_F2 => Ok(KeyCode::F2), - PROCESSING_KEY_F3 => Ok(KeyCode::F3), - PROCESSING_KEY_F4 => Ok(KeyCode::F4), - PROCESSING_KEY_F5 => Ok(KeyCode::F5), - PROCESSING_KEY_F6 => Ok(KeyCode::F6), - PROCESSING_KEY_F7 => Ok(KeyCode::F7), - PROCESSING_KEY_F8 => Ok(KeyCode::F8), - PROCESSING_KEY_F9 => Ok(KeyCode::F9), - PROCESSING_KEY_F10 => Ok(KeyCode::F10), - PROCESSING_KEY_F11 => Ok(KeyCode::F11), - PROCESSING_KEY_F12 => Ok(KeyCode::F12), - PROCESSING_KEY_NUMPAD_0 => Ok(KeyCode::Numpad0), - PROCESSING_KEY_NUMPAD_1 => Ok(KeyCode::Numpad1), - PROCESSING_KEY_NUMPAD_2 => Ok(KeyCode::Numpad2), - PROCESSING_KEY_NUMPAD_3 => Ok(KeyCode::Numpad3), - PROCESSING_KEY_NUMPAD_4 => Ok(KeyCode::Numpad4), - PROCESSING_KEY_NUMPAD_5 => Ok(KeyCode::Numpad5), - PROCESSING_KEY_NUMPAD_6 => Ok(KeyCode::Numpad6), - PROCESSING_KEY_NUMPAD_7 => Ok(KeyCode::Numpad7), - PROCESSING_KEY_NUMPAD_8 => Ok(KeyCode::Numpad8), - PROCESSING_KEY_NUMPAD_9 => Ok(KeyCode::Numpad9), - PROCESSING_KEY_NUMPAD_DECIMAL => Ok(KeyCode::NumpadDecimal), - PROCESSING_KEY_NUMPAD_DIVIDE => Ok(KeyCode::NumpadDivide), - PROCESSING_KEY_NUMPAD_MULTIPLY => Ok(KeyCode::NumpadMultiply), - PROCESSING_KEY_NUMPAD_SUBTRACT => Ok(KeyCode::NumpadSubtract), - PROCESSING_KEY_NUMPAD_ADD => Ok(KeyCode::NumpadAdd), - PROCESSING_KEY_NUMPAD_ENTER => Ok(KeyCode::NumpadEnter), - PROCESSING_KEY_NUMPAD_EQUAL => Ok(KeyCode::NumpadEqual), - PROCESSING_KEY_SHIFT_LEFT => Ok(KeyCode::ShiftLeft), - PROCESSING_KEY_CONTROL_LEFT => Ok(KeyCode::ControlLeft), - PROCESSING_KEY_ALT_LEFT => Ok(KeyCode::AltLeft), - PROCESSING_KEY_SUPER_LEFT => Ok(KeyCode::SuperLeft), - PROCESSING_KEY_SHIFT_RIGHT => Ok(KeyCode::ShiftRight), - PROCESSING_KEY_CONTROL_RIGHT => Ok(KeyCode::ControlRight), - PROCESSING_KEY_ALT_RIGHT => Ok(KeyCode::AltRight), - PROCESSING_KEY_SUPER_RIGHT => Ok(KeyCode::SuperRight), - PROCESSING_KEY_CONTEXT_MENU => Ok(KeyCode::ContextMenu), - _ => Err(ProcessingError::InvalidArgument(format!( - "unknown key code: {val}" - ))), - } -} - -fn key_code_to_u32(kc: KeyCode) -> u32 { - match kc { - KeyCode::Space => PROCESSING_KEY_SPACE, - KeyCode::Quote => PROCESSING_KEY_QUOTE, - KeyCode::Comma => PROCESSING_KEY_COMMA, - KeyCode::Minus => PROCESSING_KEY_MINUS, - KeyCode::Period => PROCESSING_KEY_PERIOD, - KeyCode::Slash => PROCESSING_KEY_SLASH, - KeyCode::Digit0 => PROCESSING_KEY_0, - KeyCode::Digit1 => PROCESSING_KEY_1, - KeyCode::Digit2 => PROCESSING_KEY_2, - KeyCode::Digit3 => PROCESSING_KEY_3, - KeyCode::Digit4 => PROCESSING_KEY_4, - KeyCode::Digit5 => PROCESSING_KEY_5, - KeyCode::Digit6 => PROCESSING_KEY_6, - KeyCode::Digit7 => PROCESSING_KEY_7, - KeyCode::Digit8 => PROCESSING_KEY_8, - KeyCode::Digit9 => PROCESSING_KEY_9, - KeyCode::Semicolon => PROCESSING_KEY_SEMICOLON, - KeyCode::Equal => PROCESSING_KEY_EQUAL, - KeyCode::KeyA => PROCESSING_KEY_A, - KeyCode::KeyB => PROCESSING_KEY_B, - KeyCode::KeyC => PROCESSING_KEY_C, - KeyCode::KeyD => PROCESSING_KEY_D, - KeyCode::KeyE => PROCESSING_KEY_E, - KeyCode::KeyF => PROCESSING_KEY_F, - KeyCode::KeyG => PROCESSING_KEY_G, - KeyCode::KeyH => PROCESSING_KEY_H, - KeyCode::KeyI => PROCESSING_KEY_I, - KeyCode::KeyJ => PROCESSING_KEY_J, - KeyCode::KeyK => PROCESSING_KEY_K, - KeyCode::KeyL => PROCESSING_KEY_L, - KeyCode::KeyM => PROCESSING_KEY_M, - KeyCode::KeyN => PROCESSING_KEY_N, - KeyCode::KeyO => PROCESSING_KEY_O, - KeyCode::KeyP => PROCESSING_KEY_P, - KeyCode::KeyQ => PROCESSING_KEY_Q, - KeyCode::KeyR => PROCESSING_KEY_R, - KeyCode::KeyS => PROCESSING_KEY_S, - KeyCode::KeyT => PROCESSING_KEY_T, - KeyCode::KeyU => PROCESSING_KEY_U, - KeyCode::KeyV => PROCESSING_KEY_V, - KeyCode::KeyW => PROCESSING_KEY_W, - KeyCode::KeyX => PROCESSING_KEY_X, - KeyCode::KeyY => PROCESSING_KEY_Y, - KeyCode::KeyZ => PROCESSING_KEY_Z, - KeyCode::BracketLeft => PROCESSING_KEY_BRACKET_LEFT, - KeyCode::Backslash => PROCESSING_KEY_BACKSLASH, - KeyCode::BracketRight => PROCESSING_KEY_BRACKET_RIGHT, - KeyCode::Backquote => PROCESSING_KEY_BACKQUOTE, - KeyCode::Escape => PROCESSING_KEY_ESCAPE, - KeyCode::Enter => PROCESSING_KEY_ENTER, - KeyCode::Tab => PROCESSING_KEY_TAB, - KeyCode::Backspace => PROCESSING_KEY_BACKSPACE, - KeyCode::Insert => PROCESSING_KEY_INSERT, - KeyCode::Delete => PROCESSING_KEY_DELETE, - KeyCode::ArrowRight => PROCESSING_KEY_RIGHT, - KeyCode::ArrowLeft => PROCESSING_KEY_LEFT, - KeyCode::ArrowDown => PROCESSING_KEY_DOWN, - KeyCode::ArrowUp => PROCESSING_KEY_UP, - KeyCode::PageUp => PROCESSING_KEY_PAGE_UP, - KeyCode::PageDown => PROCESSING_KEY_PAGE_DOWN, - KeyCode::Home => PROCESSING_KEY_HOME, - KeyCode::End => PROCESSING_KEY_END, - KeyCode::CapsLock => PROCESSING_KEY_CAPS_LOCK, - KeyCode::ScrollLock => PROCESSING_KEY_SCROLL_LOCK, - KeyCode::NumLock => PROCESSING_KEY_NUM_LOCK, - KeyCode::PrintScreen => PROCESSING_KEY_PRINT_SCREEN, - KeyCode::Pause => PROCESSING_KEY_PAUSE, - KeyCode::F1 => PROCESSING_KEY_F1, - KeyCode::F2 => PROCESSING_KEY_F2, - KeyCode::F3 => PROCESSING_KEY_F3, - KeyCode::F4 => PROCESSING_KEY_F4, - KeyCode::F5 => PROCESSING_KEY_F5, - KeyCode::F6 => PROCESSING_KEY_F6, - KeyCode::F7 => PROCESSING_KEY_F7, - KeyCode::F8 => PROCESSING_KEY_F8, - KeyCode::F9 => PROCESSING_KEY_F9, - KeyCode::F10 => PROCESSING_KEY_F10, - KeyCode::F11 => PROCESSING_KEY_F11, - KeyCode::F12 => PROCESSING_KEY_F12, - KeyCode::Numpad0 => PROCESSING_KEY_NUMPAD_0, - KeyCode::Numpad1 => PROCESSING_KEY_NUMPAD_1, - KeyCode::Numpad2 => PROCESSING_KEY_NUMPAD_2, - KeyCode::Numpad3 => PROCESSING_KEY_NUMPAD_3, - KeyCode::Numpad4 => PROCESSING_KEY_NUMPAD_4, - KeyCode::Numpad5 => PROCESSING_KEY_NUMPAD_5, - KeyCode::Numpad6 => PROCESSING_KEY_NUMPAD_6, - KeyCode::Numpad7 => PROCESSING_KEY_NUMPAD_7, - KeyCode::Numpad8 => PROCESSING_KEY_NUMPAD_8, - KeyCode::Numpad9 => PROCESSING_KEY_NUMPAD_9, - KeyCode::NumpadDecimal => PROCESSING_KEY_NUMPAD_DECIMAL, - KeyCode::NumpadDivide => PROCESSING_KEY_NUMPAD_DIVIDE, - KeyCode::NumpadMultiply => PROCESSING_KEY_NUMPAD_MULTIPLY, - KeyCode::NumpadSubtract => PROCESSING_KEY_NUMPAD_SUBTRACT, - KeyCode::NumpadAdd => PROCESSING_KEY_NUMPAD_ADD, - KeyCode::NumpadEnter => PROCESSING_KEY_NUMPAD_ENTER, - KeyCode::NumpadEqual => PROCESSING_KEY_NUMPAD_EQUAL, - KeyCode::ShiftLeft => PROCESSING_KEY_SHIFT_LEFT, - KeyCode::ControlLeft => PROCESSING_KEY_CONTROL_LEFT, - KeyCode::AltLeft => PROCESSING_KEY_ALT_LEFT, - KeyCode::SuperLeft => PROCESSING_KEY_SUPER_LEFT, - KeyCode::ShiftRight => PROCESSING_KEY_SHIFT_RIGHT, - KeyCode::ControlRight => PROCESSING_KEY_CONTROL_RIGHT, - KeyCode::AltRight => PROCESSING_KEY_ALT_RIGHT, - KeyCode::SuperRight => PROCESSING_KEY_SUPER_RIGHT, - KeyCode::ContextMenu => PROCESSING_KEY_CONTEXT_MENU, - _ => 0, - } -} - #[wasm_bindgen(js_name = "geometryAttributeRotation")] pub fn js_geometry_attribute_rotation() -> u64 { geometry_attribute_rotation().to_bits() From eef9c036d88ca2aec45db31fef0106337a27c6f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Fri, 2 Oct 2026 22:09:36 -0700 Subject: [PATCH 11/20] Processing core functions for Python. --- Cargo.lock | 1 + crates/processing_core/Cargo.toml | 1 + crates/processing_core/src/constants.rs | 10 + crates/processing_core/src/lib.rs | 1 + crates/processing_core/src/noise.rs | 101 +++++++++++ crates/processing_pyo3/mewnala/math.py | 39 ++++ crates/processing_pyo3/src/color.rs | 56 ++++++ crates/processing_pyo3/src/constants.rs | 2 + crates/processing_pyo3/src/graphics.rs | 91 ++++++++++ crates/processing_pyo3/src/lib.rs | 231 +++++++++++++++++++++++- crates/processing_render/src/lib.rs | 5 + crates/processing_render/src/time.rs | 24 ++- 12 files changed, 558 insertions(+), 4 deletions(-) create mode 100644 crates/processing_core/src/noise.rs diff --git a/Cargo.lock b/Cargo.lock index b1bc2097..967d9882 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5479,6 +5479,7 @@ name = "processing_core" version = "0.0.5" dependencies = [ "bevy", + "rand 0.10.2", "raw-window-handle", "thiserror 2.0.20", "tracing", diff --git a/crates/processing_core/Cargo.toml b/crates/processing_core/Cargo.toml index 93ad03a2..c2338688 100644 --- a/crates/processing_core/Cargo.toml +++ b/crates/processing_core/Cargo.toml @@ -8,6 +8,7 @@ workspace = true [dependencies] bevy = { workspace = true } +rand = { workspace = true } raw-window-handle = "0.6" thiserror = "2" tracing = "0.1" diff --git a/crates/processing_core/src/constants.rs b/crates/processing_core/src/constants.rs index f6771497..0cd5866a 100644 --- a/crates/processing_core/src/constants.rs +++ b/crates/processing_core/src/constants.rs @@ -54,6 +54,16 @@ pub const OKLCH: &str = "oklch"; pub const LAB: &str = "lab"; pub const LCH: &str = "lch"; pub const XYZ: &str = "xyz"; +// Processing's names for the same spaces +pub const RGB: &str = SRGB; +pub const HSB: &str = HSV; + +pub const ARROW: &str = "arrow"; +pub const CROSS: &str = "cross"; +pub const HAND: &str = "hand"; +pub const MOVE: &str = "move"; +pub const TEXT: &str = "text"; +pub const WAIT: &str = "wait"; pub const PBR_NEUTRAL: &str = "pbr_neutral"; // tonemapping pub const AGX: &str = "agx"; diff --git a/crates/processing_core/src/lib.rs b/crates/processing_core/src/lib.rs index 489754e7..29638048 100644 --- a/crates/processing_core/src/lib.rs +++ b/crates/processing_core/src/lib.rs @@ -1,6 +1,7 @@ pub mod config; pub mod constants; pub mod error; +pub mod noise; use std::cell::RefCell; use std::sync::OnceLock; diff --git a/crates/processing_core/src/noise.rs b/crates/processing_core/src/noise.rs new file mode 100644 index 00000000..1dc52b62 --- /dev/null +++ b/crates/processing_core/src/noise.rs @@ -0,0 +1,101 @@ +//! Processing's `noise()`: value noise over a table of 4096 random values with +//! cosine smoothing, summed over octaves. A port of `PApplet.noise`, including +//! its half-degree cosine table, so sketches look the way they do in Processing. + +use rand::rngs::StdRng; +use rand::{RngExt, SeedableRng}; + +const YWRAPB: u32 = 4; +const YWRAP: u32 = 1 << YWRAPB; +const ZWRAPB: u32 = 8; +const ZWRAP: u32 = 1 << ZWRAPB; +const SIZE: u32 = 4095; + +pub struct Noise { + table: Vec, + octaves: u32, + falloff: f32, +} + +impl Noise { + pub fn new(seed: u64) -> Self { + let mut rng = StdRng::seed_from_u64(seed); + Self { + table: (0..=SIZE).map(|_| rng.random::()).collect(), + octaves: 4, + falloff: 0.5, + } + } + + /// Reseeds the table, keeping the detail settings. + pub fn seed(&mut self, seed: u64) { + let mut rng = StdRng::seed_from_u64(seed); + self.table.iter_mut().for_each(|v| *v = rng.random()); + } + + /// Non-positive values leave the current setting unchanged, as in Processing. + pub fn detail(&mut self, octaves: i32, falloff: Option) { + if octaves > 0 { + self.octaves = octaves as u32; + } + if let Some(falloff) = falloff.filter(|f| *f > 0.0) { + self.falloff = falloff; + } + } + + pub fn get(&self, x: f32, y: f32, z: f32) -> f32 { + let (x, y, z) = (x.abs(), y.abs(), z.abs()); + let (mut xi, mut yi, mut zi) = (x as u32, y as u32, z as u32); + let (mut xf, mut yf, mut zf) = (x - xi as f32, y - yi as f32, z - zi as f32); + let at = |i: u32| self.table[(i & SIZE) as usize]; + + let mut r = 0.0; + let mut amplitude = 0.5; + for _ in 0..self.octaves { + let mut of = xi + .wrapping_add(yi.wrapping_shl(YWRAPB)) + .wrapping_add(zi.wrapping_shl(ZWRAPB)); + let rxf = smooth(xf); + let ryf = smooth(yf); + + let mut n1 = at(of); + n1 += rxf * (at(of.wrapping_add(1)) - n1); + let mut n2 = at(of.wrapping_add(YWRAP)); + n2 += rxf * (at(of.wrapping_add(YWRAP + 1)) - n2); + n1 += ryf * (n2 - n1); + + of = of.wrapping_add(ZWRAP); + n2 = at(of); + n2 += rxf * (at(of.wrapping_add(1)) - n2); + let mut n3 = at(of.wrapping_add(YWRAP)); + n3 += rxf * (at(of.wrapping_add(YWRAP + 1)) - n3); + n2 += ryf * (n3 - n2); + + n1 += smooth(zf) * (n2 - n1); + + r += n1 * amplitude; + amplitude *= self.falloff; + (xi, yi, zi) = (xi.wrapping_shl(1), yi.wrapping_shl(1), zi.wrapping_shl(1)); + (xf, yf, zf) = (xf * 2.0, yf * 2.0, zf * 2.0); + if xf >= 1.0 { + xi = xi.wrapping_add(1); + xf -= 1.0; + } + if yf >= 1.0 { + yi = yi.wrapping_add(1); + yf -= 1.0; + } + if zf >= 1.0 { + zi = zi.wrapping_add(1); + zf -= 1.0; + } + } + r + } +} + +/// `0.5 * (1 - cos(t * PI))`, quantized to half a degree like Processing's `cosLUT`. +fn smooth(t: f32) -> f32 { + let half_degrees = (t * 360.0) as u32 % 720; + 0.5 * (1.0 - (half_degrees as f32 * 0.5).to_radians().cos()) +} diff --git a/crates/processing_pyo3/mewnala/math.py b/crates/processing_pyo3/mewnala/math.py index b910500a..6ab3d9f0 100644 --- a/crates/processing_pyo3/mewnala/math.py +++ b/crates/processing_pyo3/mewnala/math.py @@ -1,5 +1,6 @@ """Processing math methods and vector/quaternion types.""" import math as _math +import random as _random from .mewnala import math as _native_math # extend native math so glob imports work @@ -131,3 +132,41 @@ def dist(*args): dx, dy, dz = x2 - x1, y2 - y1, z2 - z1 return sqrt(dx * dx + dy * dy + dz * dz) raise TypeError(f"dist() takes 4 or 6 arguments ({len(args)} given)") + + +def random(low, high=None): + """`random(high)` is in [0, high), `random(low, high)` in [low, high).""" + if high is None: + low, high = 0.0, low + return low + _random.random() * (high - low) + + +def random_gaussian(): + return _random.gauss(0.0, 1.0) + + +def random_seed(seed): + _random.seed(seed) + + +def bezier_point(a, b, c, d, t): + u = 1.0 - t + return a * u * u * u + 3 * b * t * u * u + 3 * c * t * t * u + d * t * t * t + + +def bezier_tangent(a, b, c, d, t): + return 3 * t * t * (-a + 3 * b - 3 * c + d) + 6 * t * (a - 2 * b + c) + 3 * (-a + b) + + +# Catmull-Rom, matching `curve()` (Processing's curveTightness(0)) +def curve_point(a, b, c, d, t): + return 0.5 * ( + 2 * b + + (-a + c) * t + + (2 * a - 5 * b + 4 * c - d) * t * t + + (-a + 3 * b - 3 * c + d) * t * t * t + ) + + +def curve_tangent(a, b, c, d, t): + return 0.5 * ((-a + c) + 2 * (2 * a - 5 * b + 4 * c - d) * t + 3 * (-a + 3 * b - 3 * c + d) * t * t) diff --git a/crates/processing_pyo3/src/color.rs b/crates/processing_pyo3/src/color.rs index 7d861d6b..bb2280e5 100644 --- a/crates/processing_pyo3/src/color.rs +++ b/crates/processing_pyo3/src/color.rs @@ -465,6 +465,62 @@ impl ColorLike { } } +/// A channel read back in the range `color_mode` set for its space, or the +/// space's native range when the mode is a different space. +fn channel_in_mode(mode: &ColorMode, space: ColorSpace, value: f32, ch: usize) -> f32 { + if mode.space == space { + value / space.default_maxes()[ch] * mode.max[ch] + } else { + value + } +} + +pub fn red(color: Color, mode: &ColorMode) -> f32 { + channel_in_mode(mode, ColorSpace::Srgb, Srgba::from(color).red, 0) +} + +pub fn green(color: Color, mode: &ColorMode) -> f32 { + channel_in_mode(mode, ColorSpace::Srgb, Srgba::from(color).green, 1) +} + +pub fn blue(color: Color, mode: &ColorMode) -> f32 { + channel_in_mode(mode, ColorSpace::Srgb, Srgba::from(color).blue, 2) +} + +pub fn alpha(color: Color, mode: &ColorMode) -> f32 { + color.alpha() * mode.max[3] +} + +pub fn hue(color: Color, mode: &ColorMode) -> f32 { + channel_in_mode(mode, ColorSpace::Hsv, Hsva::from(color).hue, 0) +} + +pub fn saturation(color: Color, mode: &ColorMode) -> f32 { + channel_in_mode(mode, ColorSpace::Hsv, Hsva::from(color).saturation, 1) +} + +pub fn brightness(color: Color, mode: &ColorMode) -> f32 { + channel_in_mode(mode, ColorSpace::Hsv, Hsva::from(color).value, 2) +} + +/// Interpolates in the `color_mode` space, as Processing's `lerpColor` does +/// for RGB and HSB. +pub fn lerp_color(from: Color, to: Color, amount: f32, mode: &ColorMode) -> Color { + let in_space = |c: Color| match mode.space { + ColorSpace::Srgb => Color::Srgba(c.into()), + ColorSpace::Linear => Color::LinearRgba(c.into()), + ColorSpace::Hsl => Color::Hsla(c.into()), + ColorSpace::Hsv => Color::Hsva(c.into()), + ColorSpace::Hwb => Color::Hwba(c.into()), + ColorSpace::Oklab => Color::Oklaba(c.into()), + ColorSpace::Oklch => Color::Oklcha(c.into()), + ColorSpace::Lab => Color::Laba(c.into()), + ColorSpace::Lch => Color::Lcha(c.into()), + ColorSpace::Xyz => Color::Xyza(c.into()), + }; + in_space(from).mix(&in_space(to), amount.clamp(0.0, 1.0)) +} + fn parse_hex(s: &str) -> PyResult { Srgba::hex(s) .map(Color::Srgba) diff --git a/crates/processing_pyo3/src/constants.rs b/crates/processing_pyo3/src/constants.rs index 5a5673ce..a29bbff6 100644 --- a/crates/processing_pyo3/src/constants.rs +++ b/crates/processing_pyo3/src/constants.rs @@ -44,6 +44,8 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { add!(m, WORD, CHAR); add!(m, NEAREST, CLAMP, REPEAT, MIRROR); add!(m, SRGB, LINEAR, HSL, HSV, HWB, OKLAB, OKLCH, LAB, LCH, XYZ); + add!(m, RGB, HSB); + add!(m, ARROW, CROSS, HAND, MOVE, TEXT, WAIT); add!( m, PBR_NEUTRAL, diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index c51052f4..0abb882b 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -830,6 +830,10 @@ impl Drop for Graphics { } impl Graphics { + pub(crate) fn current_color_mode(&self) -> PyResult { + graphics_get_color_mode(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + /// Create an offscreen graphics buffer in the already-running app (no window, /// no `init`). Backs `create_graphics()` / `new_offscreen()`. The caller must /// ensure the app exists (i.e. `size()` was called first). @@ -1135,6 +1139,93 @@ impl Graphics { .map(crate::color::PyColor::from) } + pub fn red(&self, color: crate::color::ColorLike) -> PyResult { + Ok(crate::color::red( + color.into_color()?, + &self.current_color_mode()?, + )) + } + + pub fn green(&self, color: crate::color::ColorLike) -> PyResult { + Ok(crate::color::green( + color.into_color()?, + &self.current_color_mode()?, + )) + } + + pub fn blue(&self, color: crate::color::ColorLike) -> PyResult { + Ok(crate::color::blue( + color.into_color()?, + &self.current_color_mode()?, + )) + } + + pub fn alpha(&self, color: crate::color::ColorLike) -> PyResult { + Ok(crate::color::alpha( + color.into_color()?, + &self.current_color_mode()?, + )) + } + + pub fn hue(&self, color: crate::color::ColorLike) -> PyResult { + Ok(crate::color::hue( + color.into_color()?, + &self.current_color_mode()?, + )) + } + + pub fn saturation(&self, color: crate::color::ColorLike) -> PyResult { + Ok(crate::color::saturation( + color.into_color()?, + &self.current_color_mode()?, + )) + } + + pub fn brightness(&self, color: crate::color::ColorLike) -> PyResult { + Ok(crate::color::brightness( + color.into_color()?, + &self.current_color_mode()?, + )) + } + + pub fn lerp_color( + &self, + from: crate::color::ColorLike, + to: crate::color::ColorLike, + amount: f32, + ) -> PyResult { + let color = crate::color::lerp_color( + from.into_color()?, + to.into_color()?, + amount, + &self.current_color_mode()?, + ); + Ok(color.into()) + } + + /// `ARROW`, `CROSS`, `HAND`, `MOVE`, `TEXT` or `WAIT`. Also shows a hidden cursor. + #[pyo3(signature = (kind=constants::ARROW))] + pub fn cursor(&self, kind: &str) -> PyResult<()> { + use bevy::window::SystemCursorIcon; + let icon = match kind { + constants::ARROW => SystemCursorIcon::Default, + constants::CROSS => SystemCursorIcon::Crosshair, + constants::HAND => SystemCursorIcon::Pointer, + constants::MOVE => SystemCursorIcon::Move, + constants::TEXT => SystemCursorIcon::Text, + constants::WAIT => SystemCursorIcon::Wait, + _ => return Err(PyValueError::new_err(format!("unknown cursor: {kind:?}"))), + }; + input_set_cursor_visible(self.surface.entity, true) + .and_then(|_| input_set_cursor_icon(self.surface.entity, icon)) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + pub fn no_cursor(&self) -> PyResult<()> { + input_set_cursor_visible(self.surface.entity, false) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + #[pyo3(signature = (*args))] pub fn background(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { let color = extract_color_with_mode( diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 9f505cee..3e4d29af 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -117,6 +117,9 @@ use std::env; pub(crate) struct LoopState { looping: bool, redraw_requested: bool, + exit_requested: bool, + /// Set by `frame_rate(fps)`. `run()` sleeps out the rest of each frame. + frame_interval: Option, } impl Default for LoopState { @@ -124,13 +127,31 @@ impl Default for LoopState { Self { looping: true, redraw_requested: false, + exit_requested: false, + frame_interval: None, } } } +/// Input history the derived callbacks need across frames. +#[derive(Clone, Copy, Default)] +struct EventState { + /// Pressed and not dragged since, so the release is a click. + click_pending: bool, + canvas_size: Option<(u32, u32)>, +} + thread_local! { pub(crate) static LAST_GLOBALS: RefCell>> = RefCell::new(HashMap::new()); pub(crate) static LOOP_STATE: Cell = Cell::new(LoopState::default()); + static EVENT_STATE: Cell = Cell::new(EventState::default()); + static NOISE: RefCell = RefCell::new( + processing_core::noise::Noise::new(if processing_render::ci::enabled() { + 0 + } else { + rand::random() + }), + ); } fn update_loop_state(f: impl FnOnce(&mut LoopState)) { @@ -202,6 +223,14 @@ fn sync_globals(module: &Bound<'_, PyModule>, globals: &Bound<'_, PyAny>) -> PyR Ok(()) } +/// The main canvas's color mode, or the default before `size()`. +fn current_color_mode(module: &Bound<'_, PyModule>) -> PyResult { + match get_graphics(module)? { + Some(graphics) => graphics.current_color_mode(), + None => Ok(color::ColorMode::default()), + } +} + fn mouse_event(module: &Bound<'_, PyModule>) -> PyResult> { // the graphics borrow has to end before the callback runs, which may draw let event = { @@ -217,6 +246,10 @@ fn key_event(module: &Bound<'_, PyModule>) -> PyResult> { Ok(Py::new(module.py(), input::KeyEvent::current()?)?.into_any()) } +fn no_event(module: &Bound<'_, PyModule>) -> PyResult> { + Ok(module.py().None()) +} + /// Calls the sketch's `name` callback, with an event if it takes a parameter /// (`def mouse_pressed(e)`), as py5 and Processing's `mousePressed(MouseEvent)` do. fn call_event_callback( @@ -253,24 +286,53 @@ fn dispatch_event_callbacks( let err = |e: processing::prelude::error::ProcessingError| PyRuntimeError::new_err(format!("{e}")); + let update_events = |f: &dyn Fn(&mut EventState)| { + EVENT_STATE.with(|s| { + let mut state = s.get(); + f(&mut state); + s.set(state); + }) + }; + + let size = { + let graphics = + get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; + canvas_size(&graphics)? + }; + let previous = EVENT_STATE.with(|s| s.get().canvas_size); + update_events(&|s| s.canvas_size = Some(size)); + if previous.is_some_and(|p| p != size) { + call_event_callback(module, locals, "window_resized", no_event)?; + } + if input_mouse_any_just_pressed().map_err(err)? { + update_events(&|s| s.click_pending = true); call_event_callback(module, locals, "mouse_pressed", mouse_event)?; } - if input_mouse_any_just_released().map_err(err)? { - call_event_callback(module, locals, "mouse_released", mouse_event)?; - } if input_mouse_moved().map_err(err)? { if input_mouse_is_pressed().map_err(err)? { + update_events(&|s| s.click_pending = false); call_event_callback(module, locals, "mouse_dragged", mouse_event)?; } else { call_event_callback(module, locals, "mouse_moved", mouse_event)?; } } + if input_mouse_any_just_released().map_err(err)? { + call_event_callback(module, locals, "mouse_released", mouse_event)?; + if EVENT_STATE.with(|s| s.get().click_pending) { + update_events(&|s| s.click_pending = false); + call_event_callback(module, locals, "mouse_clicked", mouse_event)?; + } + } if input_mouse_scrolled().map_err(err)? { call_event_callback(module, locals, "mouse_wheel", mouse_event)?; } if input_key_any_just_pressed().map_err(err)? { call_event_callback(module, locals, "key_pressed", key_event)?; + // Processing's keyTyped: only keys that produce a character + if input_key().map_err(err)?.is_some_and(|c| !c.is_control()) { + call_event_callback(module, locals, "key_typed", key_event)?; + } } if input_key_any_just_released().map_err(err)? { call_event_callback(module, locals, "key_released", key_event)?; @@ -920,6 +982,75 @@ pub mod mewnala { Ok(()) } + /// Ends `run()` once the current `draw()` returns. Processing's `exit()`, + /// renamed so it doesn't shadow Python's builtin. + #[pyfunction] + fn exit_sketch() -> PyResult<()> { + update_loop_state(|s| s.exit_requested = true); + Ok(()) + } + + /// `frame_rate()` returns the measured frames per second. `frame_rate(fps)` + /// caps how fast `run()` draws (the display's vsync still applies above it). + #[pyfunction] + #[pyo3(signature = (fps=None))] + fn frame_rate(py: Python<'_>, fps: Option) -> PyResult> { + match fps { + Some(fps) if fps > 0.0 => { + let interval = std::time::Duration::from_secs_f32(1.0 / fps); + update_loop_state(|s| s.frame_interval = Some(interval)); + Ok(py.None()) + } + Some(fps) => Err(pyo3::exceptions::PyValueError::new_err(format!( + "frame_rate must be positive, got {fps}" + ))), + None => { + let rate = processing::prelude::frame_rate() + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok(rate.into_pyobject(py)?.into_any().unbind()) + } + } + } + + /// Milliseconds since the sketch started. + #[pyfunction] + fn millis() -> PyResult { + let elapsed = processing::prelude::elapsed_time() + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + Ok((elapsed as f64 * 1000.0) as u64) + } + + /// Processing's Perlin noise, in [0, 1). + #[pyfunction] + #[pyo3(signature = (x, y=0.0, z=0.0))] + fn noise(x: f32, y: f32, z: f32) -> f32 { + NOISE.with(|n| n.borrow().get(x, y, z)) + } + + /// `octaves` (default 4) and `falloff` (default 0.5) per octave. + #[pyfunction] + #[pyo3(signature = (octaves, falloff=None))] + fn noise_detail(octaves: i32, falloff: Option) { + NOISE.with(|n| n.borrow_mut().detail(octaves, falloff)); + } + + #[pyfunction] + fn noise_seed(seed: u64) { + NOISE.with(|n| n.borrow_mut().seed(seed)); + } + + #[pyfunction] + #[pyo3(pass_module, signature = (kind=processing::prelude::constants::ARROW))] + fn cursor(module: &Bound<'_, PyModule>, kind: &str) -> PyResult<()> { + graphics!(module).cursor(kind) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn no_cursor(module: &Bound<'_, PyModule>) -> PyResult<()> { + graphics!(module).no_cursor() + } + #[pyfunction] #[pyo3(pass_module)] fn window_title(module: &Bound<'_, PyModule>, title: &str) -> PyResult<()> { @@ -1094,9 +1225,15 @@ pub mod mewnala { let setup_fn = locals.get_item("setup").ok(); let mut draw_fn = locals.get_item("draw").ok(); + update_loop_state(|s| s.exit_requested = false); + let exit_requested = || LOOP_STATE.with(|s| s.get().exit_requested); + if let Some(ref setup) = setup_fn { setup.call0()?; } + if exit_requested() { + return Ok(()); + } ensure_graphics(module)?; @@ -1132,6 +1269,9 @@ pub mod mewnala { } } dispatch_event_callbacks(module, &locals)?; + if exit_requested() { + break; + } std::thread::sleep(std::time::Duration::from_millis(16)); } @@ -1174,6 +1314,9 @@ pub mod mewnala { } dispatch_event_callbacks(module, &locals)?; + if exit_requested() { + break; + } let should_draw = first_frame || LOOP_STATE.with(|s| { @@ -1186,6 +1329,7 @@ pub mod mewnala { continue; } first_frame = false; + let frame_start = std::time::Instant::now(); processing::prelude::advance_frame_count() .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; @@ -1205,6 +1349,12 @@ pub mod mewnala { } update_loop_state(|s| s.redraw_requested = false); + if exit_requested() { + break; + } + if let Some(interval) = LOOP_STATE.with(|s| s.get().frame_interval) { + std::thread::sleep(interval.saturating_sub(frame_start.elapsed())); + } } Ok(()) @@ -1425,6 +1575,81 @@ pub mod mewnala { ) } + #[pyfunction] + #[pyo3(pass_module)] + fn red(module: &Bound<'_, PyModule>, color: super::color::ColorLike) -> PyResult { + Ok(super::color::red( + color.into_color()?, + ¤t_color_mode(module)?, + )) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn green(module: &Bound<'_, PyModule>, color: super::color::ColorLike) -> PyResult { + Ok(super::color::green( + color.into_color()?, + ¤t_color_mode(module)?, + )) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn blue(module: &Bound<'_, PyModule>, color: super::color::ColorLike) -> PyResult { + Ok(super::color::blue( + color.into_color()?, + ¤t_color_mode(module)?, + )) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn alpha(module: &Bound<'_, PyModule>, color: super::color::ColorLike) -> PyResult { + Ok(super::color::alpha( + color.into_color()?, + ¤t_color_mode(module)?, + )) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn hue(module: &Bound<'_, PyModule>, color: super::color::ColorLike) -> PyResult { + Ok(super::color::hue( + color.into_color()?, + ¤t_color_mode(module)?, + )) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn saturation(module: &Bound<'_, PyModule>, color: super::color::ColorLike) -> PyResult { + Ok(super::color::saturation( + color.into_color()?, + ¤t_color_mode(module)?, + )) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn brightness(module: &Bound<'_, PyModule>, color: super::color::ColorLike) -> PyResult { + Ok(super::color::brightness( + color.into_color()?, + ¤t_color_mode(module)?, + )) + } + + #[pyfunction] + #[pyo3(pass_module)] + fn lerp_color( + module: &Bound<'_, PyModule>, + from: super::color::ColorLike, + to: super::color::ColorLike, + amount: f32, + ) -> PyResult { + let mode = current_color_mode(module)?; + Ok(super::color::lerp_color(from.into_color()?, to.into_color()?, amount, &mode).into()) + } + #[pyfunction(name = "color")] #[pyo3(pass_module, signature = (*args))] fn create_color( diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 12f7a53c..97ba2426 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -81,6 +81,7 @@ impl Plugin for ProcessingRenderPlugin { let config = app.world().resource::().clone(); app.init_resource::(); + app.init_resource::(); let has_sketch_file = config .get(ConfigKey::SketchFileName) @@ -2244,6 +2245,10 @@ pub fn advance_frame_count() -> error::Result<()> { }) } +pub fn frame_rate() -> error::Result { + app_mut(|app| Ok(app.world_mut().run_system_cached(time::frame_rate).unwrap())) +} + pub fn delta_time() -> error::Result { app_mut(|app| Ok(app.world_mut().run_system_cached(time::delta_secs).unwrap())) } diff --git a/crates/processing_render/src/time.rs b/crates/processing_render/src/time.rs index 4c7d156e..72840eba 100644 --- a/crates/processing_render/src/time.rs +++ b/crates/processing_render/src/time.rs @@ -12,8 +12,30 @@ pub fn set_frame_count(In(n): In, mut frame: ResMut) { frame.0 = n; } -pub fn advance_frame_count(mut frame: ResMut) { +/// Frames per second, smoothed like Processing's `frameRate`: starts at 60 and +/// moves 10% toward the instantaneous rate each frame. +#[derive(Resource, Debug, Clone, Copy)] +pub struct ProcessingFrameRate(pub f32); + +impl Default for ProcessingFrameRate { + fn default() -> Self { + Self(60.0) + } +} + +pub fn advance_frame_count( + mut frame: ResMut, + mut rate: ResMut, + time: Option>, +) { frame.0 = frame.0.wrapping_add(1); + if let Some(dt) = time.map(|t| t.delta_secs()).filter(|dt| *dt > 0.0) { + rate.0 = rate.0 * 0.9 + 0.1 / dt; + } +} + +pub fn frame_rate(rate: Res) -> f32 { + rate.0 } pub fn delta_secs(time: Option>) -> f32 { From 22a0b4d80c15481a1ad1ea2789d707530830a9db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Fri, 2 Oct 2026 22:11:02 -0700 Subject: [PATCH 12/20] Camera, projection defaults and no_bloom for Python. --- crates/processing_pyo3/src/graphics.rs | 79 ++++++++++++++++++++---- crates/processing_pyo3/src/lib.rs | 33 ++++++++++ crates/processing_render/src/graphics.rs | 14 +++-- 3 files changed, 109 insertions(+), 17 deletions(-) diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index 0abb882b..a35e6040 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -829,7 +829,15 @@ impl Drop for Graphics { } } +/// `mode_3d()`'s field of view. +const DEFAULT_FOV: f32 = std::f32::consts::FRAC_PI_3; + impl Graphics { + /// How far `mode_3d()` puts the camera: where the canvas fills the view. + fn default_camera_z(&self) -> f32 { + (self.height as f32 / 2.0) / (DEFAULT_FOV / 2.0).tan() + } + pub(crate) fn current_color_mode(&self) -> PyResult { graphics_get_color_mode(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -2574,30 +2582,77 @@ impl Graphics { transform_look_at(self.entity, v).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } - pub fn perspective(&self, fov: f32, aspect: f32, near: f32, far: f32) -> PyResult<()> { + /// `camera()` restores the default `mode_3d()` view. `camera(eye, center, up)` + /// takes three vectors, or nine numbers as in Processing. + #[pyo3(signature = (*args))] + pub fn camera(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { + let (eye, center, up) = match args.len() { + 0 => ( + Vec3::new(0.0, 0.0, self.default_camera_z()), + Vec3::ZERO, + Vec3::Y, + ), + 3 | 9 => { + let n = args.len() / 3; + let part = |i: usize| extract_vec3(&args.get_slice(i * n, (i + 1) * n)); + (part(0)?, part(1)?, part(2)?) + } + n => { + return Err(pyo3::exceptions::PyTypeError::new_err(format!( + "camera() takes 0, 3 or 9 arguments ({n} given)" + ))); + } + }; + graphics_camera(self.entity, eye, center, up) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + + /// Arguments left out take the `mode_3d()` defaults: 60 degree `fov`, the + /// canvas aspect, `near` 1 and `far` ten times the camera distance. + #[pyo3(signature = (fov=None, aspect=None, near=None, far=None))] + pub fn perspective( + &self, + fov: Option, + aspect: Option, + near: Option, + far: Option, + ) -> PyResult<()> { + let near = near.unwrap_or(1.0); graphics_perspective( self.entity, - fov, - aspect, + fov.unwrap_or(DEFAULT_FOV), + aspect.unwrap_or(self.width as f32 / self.height as f32), near, - far, + far.unwrap_or(self.default_camera_z() * 10.0), Vec4::new(0.0, 0.0, -1.0, -near), ) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + /// Defaults to the canvas bounds, `near` 0 and `far` ten times the camera + /// distance, as Processing's `ortho()`. #[allow(clippy::too_many_arguments)] + #[pyo3(signature = (left=None, right=None, bottom=None, top=None, near=None, far=None))] pub fn ortho( &self, - left: f32, - right: f32, - bottom: f32, - top: f32, - near: f32, - far: f32, + left: Option, + right: Option, + bottom: Option, + top: Option, + near: Option, + far: Option, ) -> PyResult<()> { - graphics_ortho(self.entity, left, right, bottom, top, near, far) - .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + let (half_w, half_h) = (self.width as f32 / 2.0, self.height as f32 / 2.0); + graphics_ortho( + self.entity, + left.unwrap_or(-half_w), + right.unwrap_or(half_w), + bottom.unwrap_or(-half_h), + top.unwrap_or(half_h), + near.unwrap_or(0.0), + far.unwrap_or(self.default_camera_z() * 10.0), + ) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } pub fn orbit_camera(&self) -> PyResult<()> { diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 3e4d29af..25483103 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -1746,6 +1746,39 @@ pub mod mewnala { graphics!(module).blend_mode(&*mode.extract::>()?) } + #[pyfunction] + #[pyo3(pass_module, signature = (*args))] + fn camera(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> { + graphics!(module).camera(args) + } + + #[pyfunction] + #[pyo3(pass_module, signature = (fov=None, aspect=None, near=None, far=None))] + fn perspective( + module: &Bound<'_, PyModule>, + fov: Option, + aspect: Option, + near: Option, + far: Option, + ) -> PyResult<()> { + graphics!(module).perspective(fov, aspect, near, far) + } + + #[allow(clippy::too_many_arguments)] + #[pyfunction] + #[pyo3(pass_module, signature = (left=None, right=None, bottom=None, top=None, near=None, far=None))] + fn ortho( + module: &Bound<'_, PyModule>, + left: Option, + right: Option, + bottom: Option, + top: Option, + near: Option, + far: Option, + ) -> PyResult<()> { + graphics!(module).ortho(left, right, bottom, top, near, far) + } + #[pyfunction] #[pyo3(pass_module, signature = (intensity=processing_render::graphics::DEFAULT_BLOOM_INTENSITY, threshold=0.0))] fn bloom(module: &Bound<'_, PyModule>, intensity: f32, threshold: f32) -> PyResult<()> { diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index 7a88ec34..b4fc9014 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -412,13 +412,17 @@ pub fn ortho( .get_mut(entity) .map_err(|_| ProcessingError::GraphicsNotFound)?; - // we need a custom projection to support processing's coordinate system - // but this is in effect an orthographic projection with the given bounds - *projection = Projection::custom(ProcessingProjection { - width: right - left, - height: top - bottom, + // bounds are in the centered, Y-up view space of `mode_3d`. A fixed scaling + // mode keeps Bevy from rescaling them to the target size, and Bevy's own + // projection keeps the reverse-Z depth its 3D pipelines expect. + let (width, height) = (right - left, top - bottom); + *projection = Projection::Orthographic(OrthographicProjection { near, far, + viewport_origin: Vec2::new(-left / width, -bottom / height), + scaling_mode: bevy::camera::ScalingMode::Fixed { width, height }, + scale: 1.0, + area: Rect::new(left, bottom, right, top), }); Ok(()) From bd5940a60b5fe6c7c3f9ee49aff0162f20db1faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Fri, 2 Oct 2026 22:12:21 -0700 Subject: [PATCH 13/20] Image resize and Graphics as an image source. --- crates/processing_pyo3/src/graphics.rs | 103 +++++++++++++++---------- crates/processing_pyo3/src/lib.rs | 8 +- crates/processing_pyo3/src/material.rs | 2 +- 3 files changed, 66 insertions(+), 47 deletions(-) diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index a35e6040..a4adc57e 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -113,23 +113,6 @@ fn normalize_src_rect(entity: Entity, rect: Option<[f32; 4]>) -> PyResult<[f32; } } -/// Resolve a composite source (`Image`, `Webcam`, or `Graphics`) to a -/// texture-bearing entity, plus the graphics entity that must be flushed first -/// when the source is a graphics (so its latest content is what gets sampled). -/// A `Graphics` source uses its render-target surface entity, which carries an -/// `Image` component for offscreen graphics. -fn resolve_composite_source(src: &Bound<'_, PyAny>) -> PyResult<(Entity, Option)> { - if let Ok(img) = src.extract::() { - return Ok((img.entity, None)); - } - if let Ok(g) = src.extract::>() { - return Ok((g.surface.entity, Some(g.entity))); - } - Err(PyValueError::new_err( - "composite source must be an Image, Webcam, or Graphics", - )) -} - /// Run the built-in composite filter: blend the `src` texture into the `dst` /// graphics target using `mode`, with normalized src/dst rects. This is the /// shader-based composite pass shared by `copy`/`blend`/`mask`. @@ -536,8 +519,22 @@ impl Image { } } +/// Anything that can be sampled as an image: an `Image`, a `Webcam`, or an +/// offscreen `Graphics`. pub(crate) struct ImageRef { - pub entity: Entity, + entity: Entity, + graphics: Option, +} + +impl ImageRef { + /// The entity carrying the image. A `Graphics` source is flushed first, so + /// what it has drawn so far is what gets sampled. + pub(crate) fn texture(&self) -> PyResult { + if let Some(graphics) = self.graphics { + graphics_flush(graphics).map_err(rt_err)?; + } + Ok(self.entity) + } } impl<'a, 'py> FromPyObject<'a, 'py> for ImageRef { @@ -545,16 +542,27 @@ impl<'a, 'py> FromPyObject<'a, 'py> for ImageRef { fn extract(ob: pyo3::Borrowed<'a, 'py, PyAny>) -> PyResult { if let Ok(img) = ob.extract::>() { - return Ok(ImageRef { entity: img.entity }); + return Ok(ImageRef { + entity: img.entity, + graphics: None, + }); + } + // an offscreen canvas's render target is an image on its surface entity + if let Ok(g) = ob.extract::>() { + return Ok(ImageRef { + entity: g.surface.entity, + graphics: Some(g.entity), + }); } #[cfg(feature = "webcam")] if let Ok(cam) = ob.extract::>() { return Ok(ImageRef { entity: cam.image_entity()?, + graphics: None, }); } Err(pyo3::exceptions::PyTypeError::new_err( - "expected an Image or Webcam", + "expected an Image, Graphics, or Webcam", )) } } @@ -589,6 +597,26 @@ impl Image { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + /// Resize to `width` x `height` pixels. A 0 for either keeps the aspect ratio. + pub fn resize(&self, width: u32, height: u32) -> PyResult<()> { + let (w, h) = image_size(self.entity).map_err(rt_err)?; + let (width, height) = match (width, height) { + (0, 0) => return Err(PyValueError::new_err("resize() needs a non-zero size")), + (0, height) => ((w as f32 * height as f32 / h as f32).round() as u32, height), + (width, 0) => (width, (h as f32 * width as f32 / w as f32).round() as u32), + size => size, + }; + image_resize( + self.entity, + Extent3d { + width: width.max(1), + height: height.max(1), + depth_or_array_layers: 1, + }, + ) + .map_err(rt_err) + } + /// Image width in pixels. #[getter] fn width(&self) -> PyResult { @@ -1234,8 +1262,19 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + /// A color (same arguments as `fill()`), or an `Image`, `Graphics` or + /// `Webcam` stretched over the canvas. #[pyo3(signature = (*args))] pub fn background(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { + if args.len() == 1 + && let Ok(image) = args.get_item(0)?.extract::() + { + return graphics_record_command( + self.entity, + DrawCommand::BackgroundImage(image.texture()?), + ) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))); + } let color = extract_color_with_mode( args, &graphics_get_color_mode(self.entity) @@ -1245,11 +1284,6 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } - pub fn background_image(&self, image: &Image) -> PyResult<()> { - graphics_record_command(self.entity, DrawCommand::BackgroundImage(image.entity)) - .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) - } - pub fn clear(&self) -> PyResult<()> { graphics_record_command( self.entity, @@ -1927,7 +1961,7 @@ impl Graphics { graphics_record_command( self.entity, DrawCommand::Image { - entity: source.entity, + entity: source.texture()?, dx, dy, d_width, @@ -2316,7 +2350,7 @@ impl Graphics { } pub fn texture(&self, source: ImageRef) -> PyResult<()> { - graphics_record_command(self.entity, DrawCommand::Texture(source.entity)) + graphics_record_command(self.entity, DrawCommand::Texture(source.texture()?)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -2443,10 +2477,7 @@ impl Graphics { MULTIPLY, SCREEN, REPLACE)", ) })?; - let (src_entity, flush) = resolve_composite_source(src)?; - if let Some(g) = flush { - graphics_flush(g).map_err(rt_err)?; - } + let src_entity = src.extract::()?.texture()?; let sr = normalize_src_rect(src_entity, src_rect)?; let dr = normalize_rect(dst_rect, self.width as f32, self.height as f32); composite_apply(self.entity, src_entity, code, sr, dr, opacity) @@ -2462,10 +2493,7 @@ impl Graphics { src_rect: Option<[f32; 4]>, dst_rect: Option<[f32; 4]>, ) -> PyResult<()> { - let (src_entity, flush) = resolve_composite_source(src)?; - if let Some(g) = flush { - graphics_flush(g).map_err(rt_err)?; - } + let src_entity = src.extract::()?.texture()?; let sr = normalize_src_rect(src_entity, src_rect)?; let dr = normalize_rect(dst_rect, self.width as f32, self.height as f32); composite_apply(self.entity, src_entity, COMPOSITE_MODE_REPLACE, sr, dr, 1.0) @@ -2489,10 +2517,7 @@ impl Graphics { /// Applies a mask source's brightness as this graphics' alpha channel /// (Processing `mask`). `mask` may be an `Image`, webcam, or `Graphics`. pub fn mask(&self, mask: &Bound<'_, PyAny>) -> PyResult<()> { - let (src_entity, flush) = resolve_composite_source(mask)?; - if let Some(g) = flush { - graphics_flush(g).map_err(rt_err)?; - } + let src_entity = mask.extract::()?.texture()?; composite_apply( self.entity, src_entity, diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 25483103..457d37b5 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -1668,13 +1668,7 @@ pub mod mewnala { #[pyfunction] #[pyo3(pass_module, signature = (*args))] fn background(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> { - let graphics = graphics!(module); - let first = args.get_item(0)?; - if first.is_instance_of::() { - graphics.background_image(&*first.extract::>()?) - } else { - graphics.background(args) - } + graphics!(module).background(args) } #[pyfunction] diff --git a/crates/processing_pyo3/src/material.rs b/crates/processing_pyo3/src/material.rs index 95cf57a9..6b5ad25b 100644 --- a/crates/processing_pyo3/src/material.rs +++ b/crates/processing_pyo3/src/material.rs @@ -16,7 +16,7 @@ pub struct Material { pub(crate) fn py_to_shader_value(value: &Bound<'_, PyAny>) -> PyResult { if let Ok(img_ref) = value.extract::() { - return Ok(shader_value::ShaderValue::Texture(img_ref.entity)); + return Ok(shader_value::ShaderValue::Texture(img_ref.texture()?)); } if let Ok(int_val) = value.cast::() { if let Ok(v) = int_val.extract::() { From 53e802b8557ecbf4bbfb7b49dc0be1e5f3015bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Fri, 2 Oct 2026 22:19:02 -0700 Subject: [PATCH 14/20] Material setters, light transforms and mat4 uniforms for Python. --- crates/processing_pyo3/src/graphics.rs | 30 ++++++++ crates/processing_pyo3/src/material.rs | 101 +++++++++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index a4adc57e..f13c8670 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -418,6 +418,36 @@ impl Light { let v = extract_vec3(args)?; transform_look_at(self.entity, v).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + + #[pyo3(signature = (*args))] + pub fn translate(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { + let v = extract_vec3(args)?; + transform_translate(self.entity, v).map_err(rt_err) + } + + pub fn rotate_x(&self, angle: f32) -> PyResult<()> { + transform_rotate_x(self.entity, angle).map_err(rt_err) + } + + pub fn rotate_y(&self, angle: f32) -> PyResult<()> { + transform_rotate_y(self.entity, angle).map_err(rt_err) + } + + pub fn rotate_z(&self, angle: f32) -> PyResult<()> { + transform_rotate_z(self.entity, angle).map_err(rt_err) + } + + /// Rotate by `angle` radians around `axis`. + #[pyo3(signature = (angle, *axis))] + pub fn rotate_axis(&self, angle: f32, axis: &Bound<'_, PyTuple>) -> PyResult<()> { + let axis = extract_vec3(axis)?; + transform_rotate_axis(self.entity, angle, axis).map_err(rt_err) + } + + /// Back to the origin with no rotation. + pub fn reset(&self) -> PyResult<()> { + transform_reset(self.entity).map_err(rt_err) + } } // TODO: implement `light_destroy` diff --git a/crates/processing_pyo3/src/material.rs b/crates/processing_pyo3/src/material.rs index 6b5ad25b..2a1c4fe0 100644 --- a/crates/processing_pyo3/src/material.rs +++ b/crates/processing_pyo3/src/material.rs @@ -47,6 +47,16 @@ pub(crate) fn py_to_shader_value(value: &Bound<'_, PyAny>) -> PyResult() { + let m = bevy::math::Mat4::from_cols_array(&rows).transpose(); + return Ok(shader_value::ShaderValue::Mat4(m.to_cols_array())); + } + if let Ok(rows) = value.extract::<[[f32; 4]; 4]>() { + let m = bevy::math::Mat4::from_cols_array_2d(&rows).transpose(); + return Ok(shader_value::ShaderValue::Mat4(m.to_cols_array())); + } + if let Ok(v) = value.extract::<[f32; 4]>() { return Ok(shader_value::ShaderValue::Float4(v)); } @@ -88,6 +98,32 @@ fn apply_albedo(entity: Entity, value: &Bound<'_, PyAny>) -> PyResult<()> { ))) } +fn apply_emissive(entity: Entity, value: &Bound<'_, PyAny>) -> PyResult<()> { + let rt = |e| PyRuntimeError::new_err(format!("{e}")); + if let Ok(buf) = value.extract::>() { + return material_set_emissive_buffer(entity, buf.entity).map_err(rt); + } + // emissive is a linear radiance, not an sRGB color + let linear: bevy::color::LinearRgba = if let Ok(c) = value.extract::>() { + c.0.into() + } else if let Ok([r, g, b, a]) = value.extract::<[f32; 4]>() { + bevy::color::LinearRgba::new(r, g, b, a) + } else if let Ok([r, g, b]) = value.extract::<[f32; 3]>() { + bevy::color::LinearRgba::new(r, g, b, 1.0) + } else { + return Err(PyRuntimeError::new_err(format!( + "unsupported emissive type: {} (expected Color, Buffer, or [r,g,b,(a)])", + value.get_type().name()? + ))); + }; + material_set( + entity, + "emissive", + shader_value::ShaderValue::Float4([linear.red, linear.green, linear.blue, linear.alpha]), + ) + .map_err(rt) +} + fn py_truthy(value: &Bound<'_, PyAny>) -> PyResult { value .extract::() @@ -100,6 +136,7 @@ fn apply_kwargs(entity: Entity, kwargs: &Bound<'_, PyDict>) -> PyResult<()> { let rt = |e| PyRuntimeError::new_err(format!("{e}")); match name.as_str() { "albedo" => apply_albedo(entity, &value)?, + "emissive" => apply_emissive(entity, &value)?, "unlit" => material_set_unlit(entity, py_truthy(&value)?).map_err(rt)?, "double_sided" => material_set_double_sided(entity, py_truthy(&value)?).map_err(rt)?, "depth_write" => material_set_depth_write(entity, py_truthy(&value)?).map_err(rt)?, @@ -166,6 +203,70 @@ impl Material { }; apply_kwargs(self.entity, kwargs) } + + /// Base color: a `Color`, `[r, g, b, (a)]`, or a per-instance `Buffer`. + pub fn albedo(&self, value: &Bound<'_, PyAny>) -> PyResult<()> { + apply_albedo(self.entity, value) + } + + pub fn metalness(&self, value: f32) -> PyResult<()> { + self.set_pbr("metallic", value) + } + + pub fn roughness(&self, value: f32) -> PyResult<()> { + self.set_pbr("roughness", value) + } + + pub fn reflectance(&self, value: f32) -> PyResult<()> { + self.set_pbr("reflectance", value) + } + + /// Emitted light: a `Color`, linear `[r, g, b, (a)]`, or a per-instance `Buffer`. + pub fn emissive(&self, value: &Bound<'_, PyAny>) -> PyResult<()> { + apply_emissive(self.entity, value) + } + + pub fn opaque(&self) -> PyResult<()> { + material_set_alpha_mode(self.entity, 0, 0.0).map_err(rt) + } + + /// Discards fragments with alpha below `cutoff`. + #[pyo3(signature = (cutoff=0.5))] + pub fn mask(&self, cutoff: f32) -> PyResult<()> { + material_set_alpha_mode(self.entity, 1, cutoff).map_err(rt) + } + + pub fn transparent(&self) -> PyResult<()> { + material_set_alpha_mode(self.entity, 2, 0.0).map_err(rt) + } + + #[pyo3(signature = (on=true))] + pub fn double_sided(&self, on: bool) -> PyResult<()> { + material_set_double_sided(self.entity, on).map_err(rt) + } + + #[pyo3(signature = (on=true))] + pub fn depth_write(&self, on: bool) -> PyResult<()> { + material_set_depth_write(self.entity, on).map_err(rt) + } + + /// Blends with what's behind it using a `BlendMode` (`ADD`, `MULTIPLY`, a custom one, ...). + pub fn blend_mode(&self, mode: &crate::graphics::PyBlendMode) -> PyResult<()> { + let state = mode + .blend_state + .unwrap_or(bevy::render::render_resource::BlendState::ALPHA_BLENDING); + material_set_custom_blend(self.entity, state).map_err(rt) + } +} + +fn rt(e: impl std::fmt::Display) -> PyErr { + PyRuntimeError::new_err(format!("{e}")) +} + +impl Material { + fn set_pbr(&self, name: &str, value: f32) -> PyResult<()> { + material_set(self.entity, name, shader_value::ShaderValue::Float(value)).map_err(rt) + } } impl Drop for Material { From 81a29ca6f9998662ec49ccddf81e669ff8e8b689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Fri, 2 Oct 2026 23:17:30 -0700 Subject: [PATCH 15/20] publish alpha. --- .github/workflows/release.yml | 16 ++++++++++++++++ Cargo.lock | 4 ++-- Cargo.toml | 2 +- crates/processing_pyo3/Cargo.toml | 2 +- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4368fa31..10fc3109 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,8 +9,23 @@ permissions: contents: read jobs: + check-version: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - name: Check tag version + run: | + version=$(grep -m1 '^version = ' crates/processing_pyo3/Cargo.toml | cut -d'"' -f2) + if [ "v$version" != "${{ github.ref_name }}" ]; then + echo "::error::tag ${{ github.ref_name }} does not match processing_pyo3 version $version" + exit 1 + fi + generate-stubs: runs-on: ubuntu-22.04 + needs: check-version + if: ${{ !failure() && !cancelled() }} steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: @@ -264,3 +279,4 @@ jobs: with: files: release-assets/* generate_release_notes: true + prerelease: ${{ contains(github.ref_name, '-') }} diff --git a/Cargo.lock b/Cargo.lock index 967d9882..1be0301d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5458,7 +5458,7 @@ dependencies = [ [[package]] name = "processing" -version = "0.0.8" +version = "0.0.9-alpha.1" dependencies = [ "bevy", "js-sys", @@ -5527,7 +5527,7 @@ dependencies = [ [[package]] name = "processing_pyo3" -version = "0.0.8" +version = "0.0.9-alpha.1" dependencies = [ "bevy", "png", diff --git a/Cargo.toml b/Cargo.toml index 6b00c786..0775f0e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "processing" -version = "0.0.8" +version = "0.0.9-alpha.1" edition = "2024" autoexamples = false diff --git a/crates/processing_pyo3/Cargo.toml b/crates/processing_pyo3/Cargo.toml index 1c1926dc..e61b29c0 100644 --- a/crates/processing_pyo3/Cargo.toml +++ b/crates/processing_pyo3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "processing_pyo3" -version = "0.0.8" +version = "0.0.9-alpha.1" edition = "2024" [lints] From 51d648f32a00c7436b1140e23d6d1b34d2d0e6e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Fri, 2 Oct 2026 23:50:38 -0700 Subject: [PATCH 16/20] alpha.1 fixes --- .github/workflows/release.yml | 6 ++-- Cargo.lock | 4 +-- Cargo.toml | 2 +- crates/processing_core/src/error.rs | 2 ++ crates/processing_core/src/lib.rs | 2 +- crates/processing_pyo3/Cargo.toml | 2 +- crates/processing_pyo3/src/compute.rs | 10 ++++++ crates/processing_pyo3/src/material.rs | 4 +++ crates/processing_pyo3/src/particles.rs | 33 ++++++++++--------- crates/processing_render/src/lib.rs | 1 + .../processing_render/src/material/custom.rs | 1 + crates/processing_render/src/material/pbr.rs | 27 +++++++++------ .../src/particles/particles.wesl | 7 +++- crates/processing_render/src/shader_value.rs | 18 +++++++++- crates/processing_render/src/time.rs | 20 +++++++++-- 15 files changed, 100 insertions(+), 39 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 10fc3109..884a5666 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -178,7 +178,7 @@ jobs: submodules: true - uses: ./.github/actions/setup - name: Build FFI shared library - run: cargo build --release -p processing_ffi --features wayland,x11,webcam + run: cargo build --release -p processing_ffi --features wayland,x11 - name: Stage release files run: | mkdir -p staging/lib staging/include @@ -201,7 +201,7 @@ jobs: submodules: true - uses: dtolnay/rust-toolchain@stable - name: Build FFI shared library - run: cargo build --release -p processing_ffi --features webcam + run: cargo build --release -p processing_ffi - name: Stage release files run: | mkdir -p staging/lib staging/include @@ -224,7 +224,7 @@ jobs: submodules: true - uses: dtolnay/rust-toolchain@stable - name: Build FFI shared library - run: cargo build --release -p processing_ffi --features webcam + run: cargo build --release -p processing_ffi - name: Stage release files shell: pwsh run: | diff --git a/Cargo.lock b/Cargo.lock index 1be0301d..350c2fa6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5458,7 +5458,7 @@ dependencies = [ [[package]] name = "processing" -version = "0.0.9-alpha.1" +version = "0.0.9-alpha.2" dependencies = [ "bevy", "js-sys", @@ -5527,7 +5527,7 @@ dependencies = [ [[package]] name = "processing_pyo3" -version = "0.0.9-alpha.1" +version = "0.0.9-alpha.2" dependencies = [ "bevy", "png", diff --git a/Cargo.toml b/Cargo.toml index 0775f0e1..368dbb1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "processing" -version = "0.0.9-alpha.1" +version = "0.0.9-alpha.2" edition = "2024" autoexamples = false diff --git a/crates/processing_core/src/error.rs b/crates/processing_core/src/error.rs index 59265767..4374337f 100644 --- a/crates/processing_core/src/error.rs +++ b/crates/processing_core/src/error.rs @@ -6,6 +6,8 @@ pub type Result = std::result::Result; 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")] diff --git a/crates/processing_core/src/lib.rs b/crates/processing_core/src/lib.rs index 29638048..70c3cc72 100644 --- a/crates/processing_core/src/lib.rs +++ b/crates/processing_core/src/lib.rs @@ -24,7 +24,7 @@ pub fn app_mut(cb: impl FnOnce(&mut App) -> error::Result) -> 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 { diff --git a/crates/processing_pyo3/Cargo.toml b/crates/processing_pyo3/Cargo.toml index e61b29c0..20f3d424 100644 --- a/crates/processing_pyo3/Cargo.toml +++ b/crates/processing_pyo3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "processing_pyo3" -version = "0.0.9-alpha.1" +version = "0.0.9-alpha.2" edition = "2024" [lints] diff --git a/crates/processing_pyo3/src/compute.rs b/crates/processing_pyo3/src/compute.rs index 9f7208fb..d0e38fe4 100644 --- a/crates/processing_pyo3/src/compute.rs +++ b/crates/processing_pyo3/src/compute.rs @@ -17,6 +17,8 @@ pub struct Buffer { element_type: Option, size: u64, borrowed: bool, + /// Keeps the object that owns a borrowed buffer alive. + owner: Option>, } impl Buffer { @@ -27,9 +29,15 @@ impl Buffer { element_type, size, borrowed: true, + owner: None, } } + pub(crate) fn with_owner(mut self, owner: Py) -> Self { + self.owner = Some(owner); + self + } + pub(crate) fn components(&self) -> Option { self.element_type .as_ref() @@ -57,6 +65,7 @@ impl Buffer { element_type, size, borrowed: false, + owner: None, }) } } @@ -288,6 +297,7 @@ fn shader_value_to_py<'py>(py: Python<'py>, sv: &ShaderValue) -> PyResult 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(..) diff --git a/crates/processing_pyo3/src/material.rs b/crates/processing_pyo3/src/material.rs index 2a1c4fe0..8a1f06e5 100644 --- a/crates/processing_pyo3/src/material.rs +++ b/crates/processing_pyo3/src/material.rs @@ -39,6 +39,10 @@ pub(crate) fn py_to_shader_value(value: &Bound<'_, PyAny>) -> PyResult>() { 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::>() { + return Ok(shader_value::ShaderValue::Color(c.0)); + } if let Ok(buf) = value.extract::>() { return Ok(shader_value::ShaderValue::Buffer(buf.entity)); diff --git a/crates/processing_pyo3/src/particles.rs b/crates/processing_pyo3/src/particles.rs index 788f0d6d..7205e548 100644 --- a/crates/processing_pyo3/src/particles.rs +++ b/crates/processing_pyo3/src/particles.rs @@ -900,9 +900,10 @@ impl Particles { /// built-in name (`"position"`, `"velocity"`, `"color"`, `"scale"`, `"life"`, /// `"age"`, `"normal"`, `"uv"`, `"rotation"`), the name of any created /// attribute, or an `Attribute`. - pub fn buffer(&self, attribute: &Bound<'_, PyAny>) -> PyResult { - let attr_entity = self.resolve_attribute(attribute)?; - let buf = particles_ensure_attribute(self.entity, attr_entity) + pub fn buffer(slf: &Bound<'_, Self>, attribute: &Bound<'_, PyAny>) -> PyResult { + let this = slf.borrow(); + let attr_entity = this.resolve_attribute(attribute)?; + let buf = particles_ensure_attribute(this.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}")))?; @@ -912,25 +913,25 @@ impl Particles { AttributeFormat::Float3 => shader_value::ShaderValue::Float3([0.0; 3]), AttributeFormat::Float4 => shader_value::ShaderValue::Float4([0.0; 4]), }; - Ok(Buffer::from_entity(buf, Some(element_type))) + Ok(Buffer::from_entity(buf, Some(element_type)).with_owner(slf.clone().into_any().unbind())) } - pub fn index_buffer(&self, index_count: u32) -> PyResult { - let entity = particles_set_connectivity(self.entity, index_count) + pub fn index_buffer(slf: &Bound<'_, Self>, index_count: u32) -> PyResult { + let entity = particles_set_connectivity(slf.borrow().entity, index_count) .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; - Ok(Buffer::from_entity( - entity, - Some(shader_value::ShaderValue::UInt(0)), - )) + Ok( + Buffer::from_entity(entity, Some(shader_value::ShaderValue::UInt(0))) + .with_owner(slf.clone().into_any().unbind()), + ) } - pub fn draw_args(&self) -> PyResult { - let entity = particles_connectivity_indirect(self.entity) + pub fn draw_args(slf: &Bound<'_, Self>) -> PyResult { + let entity = particles_connectivity_indirect(slf.borrow().entity) .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; - Ok(Buffer::from_entity( - entity, - Some(shader_value::ShaderValue::UInt(0)), - )) + Ok( + Buffer::from_entity(entity, Some(shader_value::ShaderValue::UInt(0))) + .with_owner(slf.clone().into_any().unbind()), + ) } pub fn reset_indices(&self) -> PyResult<()> { diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 97ba2426..049de8ab 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -82,6 +82,7 @@ impl Plugin for ProcessingRenderPlugin { app.init_resource::(); app.init_resource::(); + app.init_resource::(); let has_sketch_file = config .get(ConfigKey::SketchFileName) diff --git a/crates/processing_render/src/material/custom.rs b/crates/processing_render/src/material/custom.rs index 50f04b75..f993da85 100644 --- a/crates/processing_render/src/material/custom.rs +++ b/crates/processing_render/src/material/custom.rs @@ -512,6 +512,7 @@ pub(crate) fn shader_value_to_reflect(value: &ShaderValue) -> Result Box::new(UVec3::from_array(*v)), ShaderValue::UInt4(v) => Box::new(UVec4::from_array(*v)), ShaderValue::Mat4(v) => Box::new(Mat4::from_cols_array(v)), + ShaderValue::Color(c) => Box::new(Vec4::from_array(c.to_linear().to_f32_array())), ShaderValue::Texture(_) | ShaderValue::Buffer(_) | ShaderValue::MeshAttribute(..) diff --git a/crates/processing_render/src/material/pbr.rs b/crates/processing_render/src/material/pbr.rs index f4fd86f8..abd7cacc 100644 --- a/crates/processing_render/src/material/pbr.rs +++ b/crates/processing_render/src/material/pbr.rs @@ -11,12 +11,16 @@ pub fn set_property( ) -> Result<()> { match name { "base_color" | "color" => { - let ShaderValue::Float4(c) = value else { - return Err(ProcessingError::InvalidArgument(format!( - "'{name}' expects Float4, got {value:?}" - ))); + material.base_color = match value { + // plain numbers are colors as written, i.e. sRGB + ShaderValue::Float4(c) => Color::srgba(c[0], c[1], c[2], c[3]), + ShaderValue::Color(c) => *c, + _ => { + return Err(ProcessingError::InvalidArgument(format!( + "'{name}' expects a color, got {value:?}" + ))); + } }; - material.base_color = Color::srgba(c[0], c[1], c[2], c[3]); } "metallic" => { let ShaderValue::Float(v) = value else { @@ -43,12 +47,15 @@ pub fn set_property( material.reflectance = *v; } "emissive" => { - let ShaderValue::Float4(c) = value else { - return Err(ProcessingError::InvalidArgument(format!( - "'{name}' expects Float4, got {value:?}" - ))); + material.emissive = match value { + ShaderValue::Float4(c) => LinearRgba::new(c[0], c[1], c[2], c[3]), + ShaderValue::Color(c) => c.to_linear(), + _ => { + return Err(ProcessingError::InvalidArgument(format!( + "'{name}' expects a color, got {value:?}" + ))); + } }; - material.emissive = LinearRgba::new(c[0], c[1], c[2], c[3]); } "base_color_texture" | "texture" => { let Some(handle) = texture_handle else { diff --git a/crates/processing_render/src/particles/particles.wesl b/crates/processing_render/src/particles/particles.wesl index ed8f5438..1c630ea9 100644 --- a/crates/processing_render/src/particles/particles.wesl +++ b/crates/processing_render/src/particles/particles.wesl @@ -1,6 +1,7 @@ import bevy_pbr::render::{ pbr_fragment::pbr_input_from_standard_material, pbr_functions::alpha_discard, + pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT, mesh_functions, }; @@ -53,7 +54,11 @@ fn fragment( @if(PREPASS_PIPELINE) { out = deferred_output(in, pbr_input); } @else { - out.color = apply_pbr_lighting(pbr_input); + if (pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u { + out.color = apply_pbr_lighting(pbr_input); + } else { + out.color = pbr_input.material.base_color; + } out.color = main_pass_post_lighting_processing(pbr_input, out.color); } diff --git a/crates/processing_render/src/shader_value.rs b/crates/processing_render/src/shader_value.rs index 5e1f7ce8..5967997e 100644 --- a/crates/processing_render/src/shader_value.rs +++ b/crates/processing_render/src/shader_value.rs @@ -15,6 +15,8 @@ pub enum ShaderValue { UInt3([u32; 3]), UInt4([u32; 4]), Mat4([f32; 16]), + /// Linear RGBA in a shader; PBR color properties take it as-is. + Color(Color), Texture(Entity), Buffer(Entity), MeshAttribute(Entity, Entity), @@ -39,6 +41,13 @@ impl ShaderValue { ShaderValue::UInt3(v) => Some(v.iter().flat_map(|u| u.to_le_bytes()).collect()), ShaderValue::UInt4(v) => Some(v.iter().flat_map(|u| u.to_le_bytes()).collect()), ShaderValue::Mat4(v) => Some(v.iter().flat_map(|f| f.to_le_bytes()).collect()), + ShaderValue::Color(c) => Some( + c.to_linear() + .to_f32_array() + .iter() + .flat_map(|f| f.to_le_bytes()) + .collect(), + ), ShaderValue::Texture(_) | ShaderValue::Buffer(_) | ShaderValue::MeshAttribute(..) @@ -52,7 +61,10 @@ impl ShaderValue { ShaderValue::Float(_) | ShaderValue::Int(_) | ShaderValue::UInt(_) => Some(4), ShaderValue::Float2(_) | ShaderValue::Int2(_) | ShaderValue::UInt2(_) => Some(8), ShaderValue::Float3(_) | ShaderValue::Int3(_) | ShaderValue::UInt3(_) => Some(12), - ShaderValue::Float4(_) | ShaderValue::Int4(_) | ShaderValue::UInt4(_) => Some(16), + ShaderValue::Float4(_) + | ShaderValue::Int4(_) + | ShaderValue::UInt4(_) + | ShaderValue::Color(_) => Some(16), ShaderValue::Mat4(_) => Some(64), ShaderValue::Texture(_) | ShaderValue::Buffer(_) @@ -104,6 +116,10 @@ impl ShaderValue { ShaderValue::UInt3(_) => Some(ShaderValue::UInt3(u32s::<3>(bytes)?)), ShaderValue::UInt4(_) => Some(ShaderValue::UInt4(u32s::<4>(bytes)?)), ShaderValue::Mat4(_) => Some(ShaderValue::Mat4(f32s::<16>(bytes)?)), + ShaderValue::Color(_) => { + let [r, g, b, a] = f32s::<4>(bytes)?; + Some(ShaderValue::Color(Color::linear_rgba(r, g, b, a))) + } ShaderValue::Texture(_) | ShaderValue::Buffer(_) | ShaderValue::MeshAttribute(..) diff --git a/crates/processing_render/src/time.rs b/crates/processing_render/src/time.rs index 72840eba..33e49271 100644 --- a/crates/processing_render/src/time.rs +++ b/crates/processing_render/src/time.rs @@ -23,13 +23,27 @@ impl Default for ProcessingFrameRate { } } +/// Time between Processing frames; `Time::delta` only spans the last of a frame's `app.update()`s. +#[derive(Resource, Default, Debug, Clone, Copy)] +pub struct ProcessingFrameTime { + last_elapsed: Option, + delta: f32, +} + pub fn advance_frame_count( mut frame: ResMut, mut rate: ResMut, + mut frame_time: ResMut, time: Option>, ) { frame.0 = frame.0.wrapping_add(1); - if let Some(dt) = time.map(|t| t.delta_secs()).filter(|dt| *dt > 0.0) { + let Some(elapsed) = time.map(|t| t.elapsed_secs_f64()) else { + return; + }; + let dt = frame_time.last_elapsed.map_or(0.0, |last| (elapsed - last) as f32); + frame_time.last_elapsed = Some(elapsed); + frame_time.delta = dt; + if dt > 0.0 { rate.0 = rate.0 * 0.9 + 0.1 / dt; } } @@ -38,8 +52,8 @@ pub fn frame_rate(rate: Res) -> f32 { rate.0 } -pub fn delta_secs(time: Option>) -> f32 { - time.map(|t| t.delta_secs()).unwrap_or(0.0) +pub fn delta_secs(frame_time: Res) -> f32 { + frame_time.delta } pub fn elapsed_secs(time: Option>) -> f32 { From 37982e89a82243a00b107ff728ee72cb42727082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sat, 3 Oct 2026 00:40:14 -0700 Subject: [PATCH 17/20] alpha.2 fixes --- Cargo.lock | 4 +- Cargo.toml | 5 +- crates/processing_pyo3/Cargo.toml | 2 +- crates/processing_pyo3/src/lib.rs | 92 ++++++---- crates/processing_pyo3/src/particles.rs | 32 +++- crates/processing_render/src/graphics.rs | 16 +- crates/processing_render/src/lib.rs | 23 ++- crates/processing_render/src/render/mod.rs | 158 ++++++++++-------- .../src/render/primitive/text.rs | 114 ++++++++----- crates/processing_render/src/time.rs | 4 +- src/lib.rs | 16 +- 11 files changed, 306 insertions(+), 160 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 350c2fa6..830e9562 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5458,7 +5458,7 @@ dependencies = [ [[package]] name = "processing" -version = "0.0.9-alpha.2" +version = "0.0.9-alpha.3" dependencies = [ "bevy", "js-sys", @@ -5527,7 +5527,7 @@ dependencies = [ [[package]] name = "processing_pyo3" -version = "0.0.9-alpha.2" +version = "0.0.9-alpha.3" dependencies = [ "bevy", "png", diff --git a/Cargo.toml b/Cargo.toml index 368dbb1b..e9c7ec7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "processing" -version = "0.0.9-alpha.2" +version = "0.0.9-alpha.3" edition = "2024" autoexamples = false @@ -37,6 +37,9 @@ bevy = { git = "https://github.com/processing/bevy", branch = "processing-batche # 3d "3d_bevy_render", + # glTF textures and load_image() are often JPEG + "jpeg", + # ui "ui_api", "ui_bevy_render", diff --git a/crates/processing_pyo3/Cargo.toml b/crates/processing_pyo3/Cargo.toml index 20f3d424..82647807 100644 --- a/crates/processing_pyo3/Cargo.toml +++ b/crates/processing_pyo3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "processing_pyo3" -version = "0.0.9-alpha.2" +version = "0.0.9-alpha.3" edition = "2024" [lints] diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 457d37b5..8bbff321 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -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. @@ -198,10 +198,6 @@ where Ok(()) } -pub(crate) fn reset_tracked_globals() { - LAST_GLOBALS.with(|cache| cache.borrow_mut().clear()); -} - fn canvas_size(graphics: &Graphics) -> PyResult<(u32, u32)> { let err = |e: processing::prelude::error::ProcessingError| PyRuntimeError::new_err(format!("{e}")); @@ -350,7 +346,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") @@ -472,6 +472,40 @@ fn get_asset_root() -> PyResult { }) } +/// 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::>>()?, + )?; + 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, "", "exec"))?; + builtins.getattr("exec")?.call1((code, globals))?; + Ok(()) +} + fn get_sketch_info() -> PyResult<(String, String)> { Python::attach(|py| { let sys = PyModule::import(py, "sys")?; @@ -480,7 +514,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())); } @@ -1237,7 +1276,7 @@ pub mod mewnala { 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__")? @@ -1285,31 +1324,24 @@ pub mod mewnala { 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; } + 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}"), } } diff --git a/crates/processing_pyo3/src/particles.rs b/crates/processing_pyo3/src/particles.rs index 7205e548..35dc89ab 100644 --- a/crates/processing_pyo3/src/particles.rs +++ b/crates/processing_pyo3/src/particles.rs @@ -118,6 +118,26 @@ fn parse_falloff(s: &str) -> PyResult { } } +/// `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> { + 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::() { + 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, @@ -913,7 +933,10 @@ impl Particles { AttributeFormat::Float3 => shader_value::ShaderValue::Float3([0.0; 3]), AttributeFormat::Float4 => shader_value::ShaderValue::Float4([0.0; 4]), }; - Ok(Buffer::from_entity(buf, Some(element_type)).with_owner(slf.clone().into_any().unbind())) + Ok( + Buffer::from_entity(buf, Some(element_type)) + .with_owner(slf.clone().into_any().unbind()), + ) } pub fn index_buffer(slf: &Bound<'_, Self>, index_count: u32) -> PyResult { @@ -981,16 +1004,15 @@ impl Particles { let (_, fmt) = geometry_attribute_info(attr_entity) .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; let fmt = AttributeFormat::from_inner(fmt); - let floats: Vec = 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 = 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}"))) diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index b4fc9014..a4dcb0a3 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -8,7 +8,7 @@ use bevy::{ CameraMainTextureUsages, CameraOutputMode, CameraProjection, ClearColorConfig, Hdr, ImageRenderTarget, MsaaWriteback, Projection, RenderTarget, visibility::RenderLayers, }, - core_pipeline::tonemapping::Tonemapping, + core_pipeline::tonemapping::{DebandDither, Tonemapping}, ecs::query::QueryEntityError, math::{Mat4, Vec3A}, prelude::*, @@ -210,6 +210,8 @@ pub fn create( }, target, Tonemapping::None, + // dithering only applies after tonemapping + DebandDither::Disabled, // we need to be able to write to the texture CameraMainTextureUsages::default().with(TextureUsages::COPY_DST), Projection::custom(ProcessingProjection::new(width as f32, height as f32)), @@ -510,11 +512,17 @@ pub fn tonemapping_from_u8(v: u8) -> Option { pub fn set_tonemapping( In((entity, tonemapping)): In<(Entity, Tonemapping)>, - mut query: Query<&mut Tonemapping>, + mut query: Query<(&mut Tonemapping, &mut DebandDither)>, ) -> Result<()> { - *query + let (mut current, mut dither) = query .get_mut(entity) - .map_err(|_| ProcessingError::GraphicsNotFound)? = tonemapping; + .map_err(|_| ProcessingError::GraphicsNotFound)?; + *dither = if tonemapping == Tonemapping::None { + DebandDither::Disabled + } else { + DebandDither::Enabled + }; + *current = tonemapping; Ok(()) } diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 049de8ab..8a977162 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -2816,12 +2816,27 @@ fn text_query_state( render::primitive::text::OwnedTextParams, text::font::TextContext, )> { - let state = app - .world() + let world = app.world(); + let state = world .get::(graphics_entity) .ok_or(error::ProcessingError::GraphicsNotFound)?; - let params = render::primitive::text::OwnedTextParams::from_render_state(state, max_w, max_h); - let text_cx = app.world().resource::().clone(); + // text_size() and friends are queued until the next flush, so replay them + let mut style = state.style.clone(); + if let Some(buffer) = world.get::(graphics_entity) { + for cmd in &buffer.commands { + match cmd { + DrawCommand::PushStyle => style.push(), + DrawCommand::PopStyle => style.pop(), + cmd => render::apply_text_style_command(&mut style, cmd.clone(), |entity| { + world + .get::(entity) + .map(|f| f.family_name.clone()) + }), + } + } + } + let params = render::primitive::text::OwnedTextParams::from_style(&style, max_w, max_h); + let text_cx = world.resource::().clone(); Ok((params, text_cx)) } diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index 0e797032..a3df0f54 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -8,6 +8,7 @@ pub mod transform; use bevy::{ camera::{ + RenderTarget, primitives::Aabb, visibility::{NoFrustumCulling, RenderLayers}, }, @@ -16,6 +17,7 @@ use bevy::{ pbr::gpu_instance_batch::GpuBatchedMesh3d, prelude::*, render::render_resource::BlendState, + window::WindowRef, }; use command::{CommandBuffer, DrawCommand, ShapeMode}; use material::{MaterialKey, ProcessingExtendedMaterial}; @@ -82,6 +84,57 @@ impl BatchState { } } +/// Applies a text-style command to `style`; shared by the flush and by text queries, which +/// replay the frame's pending commands. `font_family` resolves a font entity. +pub(crate) fn apply_text_style_command( + style: &mut StyleStack, + cmd: DrawCommand, + font_family: impl Fn(Entity) -> Option, +) { + match cmd { + DrawCommand::TextFont(font_entity) => { + if let Some(entity) = font_entity { + if let Some(family) = font_family(entity) { + style.text_font_family = Some(family); + } + } else { + style.text_font_family = None; + } + } + DrawCommand::TextStyle(text_style) => style.text_style = text_style, + DrawCommand::TextWeight(weight) => style.text_weight = Some(weight), + DrawCommand::TextVariation { tag, value } => { + if let Some(existing) = style.text_variations.iter_mut().find(|(t, _)| *t == tag) { + existing.1 = value; + } else { + style.text_variations.push((tag, value)); + } + } + DrawCommand::ClearTextVariations => style.text_variations.clear(), + DrawCommand::TextFeature { tag, value } => { + if let Some(existing) = style.text_features.iter_mut().find(|(t, _)| *t == tag) { + existing.1 = value; + } else { + style.text_features.push((tag, value)); + } + } + DrawCommand::NoTextFeature { tag } => style.text_features.retain(|(t, _)| *t != tag), + DrawCommand::ClearTextFeatures => style.text_features.clear(), + DrawCommand::TextSize(size) => { + style.text_size = size; + style.text_leading = None; + } + DrawCommand::TextAlign { h, v } => { + style.text_align_h = h; + style.text_align_v = v; + } + DrawCommand::TextLeading(leading) => style.text_leading = Some(leading), + DrawCommand::TextWrap(mode) => style.text_wrap = mode, + DrawCommand::TextGlyphColors(colors) => style.text_glyph_colors = Some(colors), + _ => {} + } +} + #[derive(Debug, Component)] pub struct RenderState { pub style: StyleStack, @@ -142,9 +195,11 @@ pub fn flush_draw_commands( &RenderLayers, &Projection, &Transform, + Option<&RenderTarget>, ), With, >, + p_windows: Query<&Window>, p_images: Query<&Image>, p_geometries: Query<(&Geometry, Option<&GltfNodeTransform>)>, p_material_handles: Query<&UntypedMaterial>, @@ -161,9 +216,23 @@ pub fn flush_draw_commands( )>, >, ) { - for (graphics_entity, mut cmd_buffer, mut state, render_layers, projection, camera_transform) in - graphics.iter_mut() + for ( + graphics_entity, + mut cmd_buffer, + mut state, + render_layers, + projection, + camera_transform, + target, + ) in graphics.iter_mut() { + // an opaque window ignores background alpha, as in Processing + let opaque_window = matches!( + target, + Some(RenderTarget::Window(WindowRef::Entity(window))) + if p_windows.get(*window).is_ok_and(|w| !w.transparent) + ); + for (raster_entity, raster_layers) in p_raster_draws.iter() { if raster_layers.intersects(render_layers) { res.commands @@ -848,6 +917,11 @@ pub fn flush_draw_commands( } DrawCommand::BackgroundColor(color) => { flush_batch(&mut res, &mut batch, &p_material_handles); + let color = if opaque_window { + color.with_alpha(1.0) + } else { + color + }; let mesh = create_ndc_background_quad(world_from_clip, color, false); let mesh_handle = res.meshes.add(mesh); @@ -1255,70 +1329,22 @@ pub fn flush_draw_commands( &p_material_handles, ); } - DrawCommand::TextFont(font_entity) => { - if let Some(entity) = font_entity { - if let Ok(font) = p_fonts.get(entity) { - state.style.text_font_family = Some(font.family_name.clone()); - } - } else { - state.style.text_font_family = None; - } - } - DrawCommand::TextStyle(style) => { - state.style.text_style = style; - } - DrawCommand::TextWeight(weight) => { - state.style.text_weight = Some(weight); - } - DrawCommand::TextVariation { tag, value } => { - if let Some(existing) = state - .style - .text_variations - .iter_mut() - .find(|(t, _)| *t == tag) - { - existing.1 = value; - } else { - state.style.text_variations.push((tag, value)); - } - } - DrawCommand::ClearTextVariations => { - state.style.text_variations.clear(); - } - DrawCommand::TextFeature { tag, value } => { - if let Some(existing) = state - .style - .text_features - .iter_mut() - .find(|(t, _)| *t == tag) - { - existing.1 = value; - } else { - state.style.text_features.push((tag, value)); - } - } - DrawCommand::NoTextFeature { tag } => { - state.style.text_features.retain(|(t, _)| *t != tag); - } - DrawCommand::ClearTextFeatures => { - state.style.text_features.clear(); - } - DrawCommand::TextSize(size) => { - state.style.text_size = size; - state.style.text_leading = None; - } - DrawCommand::TextAlign { h, v } => { - state.style.text_align_h = h; - state.style.text_align_v = v; - } - DrawCommand::TextLeading(leading) => { - state.style.text_leading = Some(leading); - } - DrawCommand::TextWrap(mode) => { - state.style.text_wrap = mode; - } - DrawCommand::TextGlyphColors(colors) => { - state.style.text_glyph_colors = Some(colors); + cmd @ (DrawCommand::TextFont(_) + | DrawCommand::TextStyle(_) + | DrawCommand::TextWeight(_) + | DrawCommand::TextVariation { .. } + | DrawCommand::ClearTextVariations + | DrawCommand::TextFeature { .. } + | DrawCommand::NoTextFeature { .. } + | DrawCommand::ClearTextFeatures + | DrawCommand::TextSize(_) + | DrawCommand::TextAlign { .. } + | DrawCommand::TextLeading(_) + | DrawCommand::TextWrap(_) + | DrawCommand::TextGlyphColors(_)) => { + apply_text_style_command(&mut state.style, cmd, |entity| { + p_fonts.get(entity).ok().map(|f| f.family_name.clone()) + }); } DrawCommand::Text { content, diff --git a/crates/processing_render/src/render/primitive/text.rs b/crates/processing_render/src/render/primitive/text.rs index 5cc4ab73..bc45bd12 100644 --- a/crates/processing_render/src/render/primitive/text.rs +++ b/crates/processing_render/src/render/primitive/text.rs @@ -28,6 +28,7 @@ use crate::render::{ RenderState, command::{TextAlignH, TextAlignV, TextStyle, TextWrapMode}, mesh_builder::MeshBuilder, + style::Style, }; use crate::text::font::{DEFAULT_FONT_FAMILY, TextContext}; @@ -91,19 +92,23 @@ impl OwnedTextParams { /// Snapshot a `RenderState`'s text state. `glyph_colors` is left unset; the /// draw path fills it in, measurement queries don't need it. pub fn from_render_state(state: &RenderState, max_w: Option, max_h: Option) -> Self { + Self::from_style(&state.style, max_w, max_h) + } + + pub fn from_style(style: &Style, max_w: Option, max_h: Option) -> Self { Self { - text_size: state.style.text_size, - align_h: state.style.text_align_h, - align_v: state.style.text_align_v, - leading: state.style.text_leading, + text_size: style.text_size, + align_h: style.text_align_h, + align_v: style.text_align_v, + leading: style.text_leading, max_w, max_h, - wrap: state.style.text_wrap, - font_family: state.style.text_font_family.clone(), - text_style: state.style.text_style, - text_weight: state.style.text_weight, - text_variations: state.style.text_variations.clone(), - text_features: state.style.text_features.clone(), + wrap: style.text_wrap, + font_family: style.text_font_family.clone(), + text_style: style.text_style, + text_weight: style.text_weight, + text_variations: style.text_variations.clone(), + text_features: style.text_features.clone(), glyph_colors: None, } } @@ -394,6 +399,27 @@ pub fn text_to_paths( /// Default `sample_factor` for [`text_to_points`]. pub const DEFAULT_SAMPLE_FACTOR: f32 = 0.1; +/// Samples a segment at `step` points per unit of length, like p5's `textToPoints`. +fn sample_outline( + points: &mut Vec<[f32; 2]>, + step: f32, + point: &dyn Fn(f32) -> [f32; 2], + include_end: bool, +) { + let mut len = 0.0; + let mut prev = point(0.0); + for i in 1..=16 { + let p = point(i as f32 / 16.0); + len += ((p[0] - prev[0]).powi(2) + (p[1] - prev[1]).powi(2)).sqrt(); + prev = p; + } + let steps = (len * step).ceil().max(1.0) as usize; + let last = if include_end { steps } else { steps - 1 }; + for i in 1..=last { + points.push(point(i as f32 / steps as f32)); + } +} + /// Sample points along text outlines. Higher `sample_factor` = more points; /// see [`DEFAULT_SAMPLE_FACTOR`]. pub fn text_to_points( @@ -416,6 +442,10 @@ pub fn text_to_points( let step = sample_factor.max(0.001); let mut points = Vec::new(); + let lerp = |a: lyon::math::Point, b: lyon::math::Point| { + move |t: f32| [a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t] + }; + for path in &glyph_paths { for event in path.iter() { use lyon::path::Event; @@ -424,47 +454,49 @@ pub fn text_to_points( points.push([at.x, at.y]); } Event::Line { from, to } => { - let dx = to.x - from.x; - let dy = to.y - from.y; - let len = (dx * dx + dy * dy).sqrt(); - let steps = (len * step).max(1.0) as usize; - for i in 1..=steps { - let t = i as f32 / steps as f32; - points.push([from.x + dx * t, from.y + dy * t]); - } + sample_outline(&mut points, step, &lerp(from, to), true) } - Event::Quadratic { from, ctrl, to } => { - let steps = (20.0 * step).max(2.0) as usize; - for i in 1..=steps { - let t = i as f32 / steps as f32; + Event::Quadratic { from, ctrl, to } => sample_outline( + &mut points, + step, + &|t| { let inv = 1.0 - t; - let px = inv * inv * from.x + 2.0 * inv * t * ctrl.x + t * t * to.x; - let py = inv * inv * from.y + 2.0 * inv * t * ctrl.y + t * t * to.y; - points.push([px, py]); - } - } + [ + inv * inv * from.x + 2.0 * inv * t * ctrl.x + t * t * to.x, + inv * inv * from.y + 2.0 * inv * t * ctrl.y + t * t * to.y, + ] + }, + true, + ), Event::Cubic { from, ctrl1, ctrl2, to, - } => { - let steps = (30.0 * step).max(2.0) as usize; - for i in 1..=steps { - let t = i as f32 / steps as f32; + } => sample_outline( + &mut points, + step, + &|t| { let inv = 1.0 - t; - let px = inv * inv * inv * from.x - + 3.0 * inv * inv * t * ctrl1.x - + 3.0 * inv * t * t * ctrl2.x - + t * t * t * to.x; - let py = inv * inv * inv * from.y - + 3.0 * inv * inv * t * ctrl1.y - + 3.0 * inv * t * t * ctrl2.y - + t * t * t * to.y; - points.push([px, py]); + [ + inv * inv * inv * from.x + + 3.0 * inv * inv * t * ctrl1.x + + 3.0 * inv * t * t * ctrl2.x + + t * t * t * to.x, + inv * inv * inv * from.y + + 3.0 * inv * inv * t * ctrl1.y + + 3.0 * inv * t * t * ctrl2.y + + t * t * t * to.y, + ] + }, + true, + ), + // the closing edge back to the start; its endpoint is the Begin point + Event::End { last, first, close } => { + if close && last != first { + sample_outline(&mut points, step, &lerp(last, first), false); } } - Event::End { .. } => {} } } } diff --git a/crates/processing_render/src/time.rs b/crates/processing_render/src/time.rs index 33e49271..57d9c726 100644 --- a/crates/processing_render/src/time.rs +++ b/crates/processing_render/src/time.rs @@ -40,7 +40,9 @@ pub fn advance_frame_count( let Some(elapsed) = time.map(|t| t.elapsed_secs_f64()) else { return; }; - let dt = frame_time.last_elapsed.map_or(0.0, |last| (elapsed - last) as f32); + let dt = frame_time + .last_elapsed + .map_or(0.0, |last| (elapsed - last) as f32); frame_time.last_elapsed = Some(elapsed); frame_time.delta = dt; if dt > 0.0 { diff --git a/src/lib.rs b/src/lib.rs index fed331f1..33c6f5be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,11 +21,11 @@ fn create_app(config: Config) -> App { AssetSourceBuilder::platform_default(asset_path, None), ); } - if config + // a sketch file is only set for live reload, the one user of file watching + let live_reload = config .get(ConfigKey::SketchFileName) - .is_some_and(|f| !f.is_empty()) - && let Some(sketch_path) = config.get(ConfigKey::SketchRootPath) - { + .is_some_and(|f| !f.is_empty()); + if live_reload && let Some(sketch_path) = config.get(ConfigKey::SketchRootPath) { app.register_asset_source( "sketch_directory", AssetSourceBuilder::platform_default(sketch_path, None), @@ -35,6 +35,10 @@ fn create_app(config: Config) -> App { #[cfg(not(target_arch = "wasm32"))] let plugins = DefaultPlugins .build() + .set(AssetPlugin { + watch_for_changes_override: Some(live_reload), + ..default() + }) .set(RenderPlugin { synchronous_pipeline_compilation: true, ..default() @@ -160,7 +164,9 @@ fn setup_tracing(log_level: Option<&str>) -> error::Result<()> { use tracing_subscriber::EnvFilter; use tracing_subscriber::util::SubscriberInitExt; - let filter = EnvFilter::try_new(log_level.unwrap_or("info")) + // RUST_LOG wins so a quiet frontend can still be debugged + let filter = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new(log_level.unwrap_or("info"))) .unwrap_or_else(|_| EnvFilter::new("info")); let subscriber = tracing_subscriber::FmtSubscriber::builder() .with_env_filter(filter) From 1c3a88ab293a8b197404dbd3f9f50bfade354624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sat, 3 Oct 2026 01:03:51 -0700 Subject: [PATCH 18/20] alpha.3 fixes --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/processing_pyo3/Cargo.toml | 2 +- crates/processing_pyo3/src/gltf.rs | 9 ++- crates/processing_render/src/gltf.rs | 76 +++++++++++++++---- crates/processing_render/src/lib.rs | 9 +++ .../processing_render/src/particles/emit.rs | 11 +++ .../src/particles/kernels/mod.rs | 2 +- .../particles/kernels/scatter_surface.wgsl | 9 +++ .../src/particles/scatter.rs | 16 +++- 10 files changed, 115 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 830e9562..c247b14d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5458,7 +5458,7 @@ dependencies = [ [[package]] name = "processing" -version = "0.0.9-alpha.3" +version = "0.0.9-alpha.4" dependencies = [ "bevy", "js-sys", @@ -5527,7 +5527,7 @@ dependencies = [ [[package]] name = "processing_pyo3" -version = "0.0.9-alpha.3" +version = "0.0.9-alpha.4" dependencies = [ "bevy", "png", diff --git a/Cargo.toml b/Cargo.toml index e9c7ec7c..323f9c72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "processing" -version = "0.0.9-alpha.3" +version = "0.0.9-alpha.4" edition = "2024" autoexamples = false diff --git a/crates/processing_pyo3/Cargo.toml b/crates/processing_pyo3/Cargo.toml index 82647807..b93f66ee 100644 --- a/crates/processing_pyo3/Cargo.toml +++ b/crates/processing_pyo3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "processing_pyo3" -version = "0.0.9-alpha.3" +version = "0.0.9-alpha.4" edition = "2024" [lints] diff --git a/crates/processing_pyo3/src/gltf.rs b/crates/processing_pyo3/src/gltf.rs index 081c213d..c90d806b 100644 --- a/crates/processing_pyo3/src/gltf.rs +++ b/crates/processing_pyo3/src/gltf.rs @@ -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)] @@ -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 { + 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> { gltf_mesh_names(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } diff --git a/crates/processing_render/src/gltf.rs b/crates/processing_render/src/gltf.rs index 7b9397be..67786164 100644 --- a/crates/processing_render/src/gltf.rs +++ b/crates/processing_render/src/gltf.rs @@ -8,7 +8,7 @@ use bevy::{ }, camera::visibility::RenderLayers, ecs::system::RunSystemOnce, - gltf::{Gltf, GltfMaterial, GltfMeshName}, + gltf::{Gltf, GltfMaterial, GltfMaterialName, GltfMeshName}, pbr::ExtendedMaterial, prelude::*, world_serialization::WorldInstanceSpawner, @@ -138,21 +138,7 @@ pub fn geometry( let instance_id = gltf_handle.instance_id; let (mesh_handle, global_transform) = { - let spawner = world.resource::(); - - // find the mesh with the given name component that bevy added post-spawn - // name is derived from gltf node or computed - let mesh_entity = spawner - .iter_instance_entities(instance_id) - .find(|&e| { - world - .get::(e) - .map(|n| n.0 == name) - .unwrap_or(false) - }) - .ok_or_else(|| { - ProcessingError::GltfLoadError(format!("Mesh '{}' not found in GLTF scene", name)) - })?; + let mesh_entity = find_mesh_entity(world, instance_id, &name)?; let mesh3d = world.get::(mesh_entity).ok_or_else(|| { ProcessingError::GltfLoadError(format!( @@ -182,6 +168,64 @@ pub fn geometry( Ok(entity) } +/// The scene entity bevy spawned for a mesh, by the name it gave that mesh (from the +/// glTF node, or computed). +fn find_mesh_entity( + world: &World, + instance_id: bevy::world_serialization::InstanceId, + name: &str, +) -> Result { + world + .resource::() + .iter_instance_entities(instance_id) + .find(|&e| world.get::(e).is_some_and(|n| n.0 == name)) + .ok_or_else(|| { + ProcessingError::GltfLoadError(format!("Mesh '{}' not found in GLTF scene", name)) + }) +} + +/// The base color texture of a mesh's material, as an [`Image`](crate::image::Image). +pub fn texture(In((gltf_entity, name)): In<(Entity, String)>, world: &mut World) -> Result { + let gltf_handle = world + .get::(gltf_entity) + .ok_or(ProcessingError::InvalidEntity)?; + let (instance_id, handle) = (gltf_handle.instance_id, gltf_handle.handle.clone()); + + let mesh_entity = find_mesh_entity(world, instance_id, &name)?; + let material_name = world + .get::(mesh_entity) + .ok_or_else(|| { + ProcessingError::GltfLoadError(format!("Mesh '{}' has no material", name)) + })? + .0 + .clone(); + + let texture = { + let gltf = world + .resource::>() + .get(&handle) + .ok_or_else(|| ProcessingError::GltfLoadError("GLTF asset not found".into()))?; + let material = gltf + .named_materials + .get(material_name.as_str()) + .and_then(|h| world.resource::>().get(h)) + .ok_or_else(|| { + ProcessingError::GltfLoadError(format!( + "Material '{}' not found in GLTF", + material_name + )) + })?; + material.base_color_texture.clone().ok_or_else(|| { + ProcessingError::GltfLoadError(format!("Mesh '{}' has no texture", name)) + })? + }; + + block_on_load(world, |w| w.get_asset_server().load_state(&texture))?; + world + .run_system_once_with(crate::image::from_handle, texture) + .unwrap() +} + /// Translate a bevy [`GltfMaterial`] into the [`StandardMaterial`] base of a /// [`ProcessingExtendedMaterial`]. `GltfMaterial` mirrors `StandardMaterial`'s /// fields, so we copy across the ones that drive shading and let the rest fall diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 8a977162..a240b68c 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -2290,6 +2290,15 @@ pub fn gltf_material(gltf_entity: Entity, name: &str) -> error::Result { }) } +#[cfg(not(target_arch = "wasm32"))] +pub fn gltf_texture(gltf_entity: Entity, name: &str) -> error::Result { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(gltf::texture, (gltf_entity, name.to_string())) + .unwrap() + }) +} + #[cfg(not(target_arch = "wasm32"))] pub fn gltf_mesh_names(gltf_entity: Entity) -> error::Result> { app_mut(|app| { diff --git a/crates/processing_render/src/particles/emit.rs b/crates/processing_render/src/particles/emit.rs index 51cd91a9..e609abf1 100644 --- a/crates/processing_render/src/particles/emit.rs +++ b/crates/processing_render/src/particles/emit.rs @@ -23,6 +23,17 @@ pub fn particles_emit_gpu( return Ok(()); } + let required: Vec = app_mut(|app| { + Ok(app + .world() + .get::(compute_entity) + .map(|r| r.0.clone()) + .unwrap_or_default()) + })?; + for attr_entity in required { + particles_ensure_attribute(particles_entity, attr_entity)?; + } + let (capacity, head, buffers) = app_mut(|app| { let world = app.world(); let field = world diff --git a/crates/processing_render/src/particles/kernels/mod.rs b/crates/processing_render/src/particles/kernels/mod.rs index 7cb8c83a..83bbd851 100644 --- a/crates/processing_render/src/particles/kernels/mod.rs +++ b/crates/processing_render/src/particles/kernels/mod.rs @@ -15,7 +15,7 @@ use crate::{compute_create, compute_set, shader_load}; #[derive(Component, Default, Clone)] pub struct KernelRequires(pub Vec); -fn set_requires(compute: Entity, names: &[&str]) -> error::Result<()> { +pub(crate) fn set_requires(compute: Entity, names: &[&str]) -> error::Result<()> { app_mut(|app| { let world = app.world_mut(); let attrs: Vec = { diff --git a/crates/processing_render/src/particles/kernels/scatter_surface.wgsl b/crates/processing_render/src/particles/kernels/scatter_surface.wgsl index a9fa3b60..ab86240b 100644 --- a/crates/processing_render/src/particles/kernels/scatter_surface.wgsl +++ b/crates/processing_render/src/particles/kernels/scatter_surface.wgsl @@ -21,6 +21,8 @@ struct EmitRange { @group(0) @binding(6) var life: array; @group(0) @binding(7) var params: Params; @group(0) @binding(8) var emit_range: EmitRange; +@group(0) @binding(9) var source_uv: array; +@group(0) @binding(10) var uv: array; fn hash(n: u32) -> u32 { var x = n; @@ -91,6 +93,13 @@ fn main(@builtin(global_invocation_id) gid: vec3) { position[slot * 3u + 0u] = p.x; position[slot * 3u + 1u] = p.y; position[slot * 3u + 2u] = p.z; + // texture coordinate at the same spot, for LOOKUP + let t0 = vec2(source_uv[i0 * 2u], source_uv[i0 * 2u + 1u]); + let t1 = vec2(source_uv[i1 * 2u], source_uv[i1 * 2u + 1u]); + let t2 = vec2(source_uv[i2 * 2u], source_uv[i2 * 2u + 1u]); + let t = (1.0 - u - v) * t0 + u * t1 + v * t2; + uv[slot * 2u + 0u] = t.x; + uv[slot * 2u + 1u] = t.y; scale[slot * 3u + 0u] = 1.0; scale[slot * 3u + 1u] = 1.0; scale[slot * 3u + 2u] = 1.0; diff --git a/crates/processing_render/src/particles/scatter.rs b/crates/processing_render/src/particles/scatter.rs index fb917589..b6a40e05 100644 --- a/crates/processing_render/src/particles/scatter.rs +++ b/crates/processing_render/src/particles/scatter.rs @@ -5,13 +5,14 @@ use processing_core::app_mut; use processing_core::error::{self, ProcessingError, Result}; use crate::geometry::Geometry; +use crate::particles::kernels::set_requires; use crate::shader_value::ShaderValue; use crate::{ buffer_create_with_data, compute_create, compute_set, geometry_attribute_position, shader_load, }; pub fn particles_scatter_create(source_geometry: Entity) -> error::Result { - let (cdf_bytes, indices_bytes, face_count) = app_mut(|app| { + let (cdf_bytes, indices_bytes, uv_bytes, face_count) = app_mut(|app| { app.world_mut() .run_system_cached_with(prepare_scatter_source, source_geometry) .unwrap() @@ -19,10 +20,12 @@ pub fn particles_scatter_create(source_geometry: Entity) -> error::Result error::Result, geometries: Query<&Geometry>, mut meshes: ResMut>, -) -> Result<(Vec, Vec, u32)> { +) -> Result<(Vec, Vec, Vec, u32)> { let geom = geometries .get(geom_entity) .map_err(|_| ProcessingError::GeometryNotFound)?; @@ -130,6 +134,11 @@ pub fn prepare_scatter_source( let (positions, dense_indices) = extract_scatter_geometry(mesh)?; let face_count = (dense_indices.len() / 3) as u32; + // a mesh without UVs scatters with uv (0, 0) + let uvs = match mesh.attribute(Mesh::ATTRIBUTE_UV_0) { + Some(VertexAttributeValues::Float32x2(uv)) => uv.clone(), + _ => vec![[0.0, 0.0]; positions.len()], + }; let mut cum = Vec::with_capacity(face_count as usize); let mut total = 0.0_f32; @@ -156,7 +165,8 @@ pub fn prepare_scatter_source( let cdf_bytes: Vec = cum.iter().flat_map(|f| f.to_le_bytes()).collect(); let indices_bytes: Vec = dense_indices.iter().flat_map(|i| i.to_le_bytes()).collect(); - Ok((cdf_bytes, indices_bytes, face_count)) + let uv_bytes: Vec = uvs.iter().flatten().flat_map(|f| f.to_le_bytes()).collect(); + Ok((cdf_bytes, indices_bytes, uv_bytes, face_count)) } pub fn prepare_scatter_volume_source( From 848cd9b28de7d9baf0b993445bec3ba9a2065ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sat, 3 Oct 2026 10:06:58 -0700 Subject: [PATCH 19/20] alpha.4 fixes --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- crates/processing_pyo3/Cargo.toml | 2 +- crates/processing_pyo3/src/lib.rs | 14 +++++++++++ crates/processing_pyo3/src/particles.rs | 2 ++ crates/processing_render/src/particles/mod.rs | 24 ++++++++++++++++--- 6 files changed, 41 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c247b14d..068fac04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5458,7 +5458,7 @@ dependencies = [ [[package]] name = "processing" -version = "0.0.9-alpha.4" +version = "0.0.9-alpha.5" dependencies = [ "bevy", "js-sys", @@ -5527,7 +5527,7 @@ dependencies = [ [[package]] name = "processing_pyo3" -version = "0.0.9-alpha.4" +version = "0.0.9-alpha.5" dependencies = [ "bevy", "png", diff --git a/Cargo.toml b/Cargo.toml index 323f9c72..55f04121 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "processing" -version = "0.0.9-alpha.4" +version = "0.0.9-alpha.5" edition = "2024" autoexamples = false diff --git a/crates/processing_pyo3/Cargo.toml b/crates/processing_pyo3/Cargo.toml index b93f66ee..e79f2030 100644 --- a/crates/processing_pyo3/Cargo.toml +++ b/crates/processing_pyo3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "processing_pyo3" -version = "0.0.9-alpha.4" +version = "0.0.9-alpha.5" edition = "2024" [lints] diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 8bbff321..e9edd37a 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -1300,6 +1300,7 @@ pub mod mewnala { .end_draw()?; loop { + py.check_signals()?; { let mut graphics = get_graphics_mut(module)? .ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; @@ -1320,6 +1321,8 @@ pub mod mewnala { 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"))?; @@ -1390,6 +1393,17 @@ pub mod mewnala { } 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::(py) { + Ok(()) + } else { + Err(e) + } + }) }); // tear down the app while the thread-local is still alive; the eager diff --git a/crates/processing_pyo3/src/particles.rs b/crates/processing_pyo3/src/particles.rs index 35dc89ab..98b85037 100644 --- a/crates/processing_pyo3/src/particles.rs +++ b/crates/processing_pyo3/src/particles.rs @@ -1001,6 +1001,8 @@ 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); diff --git a/crates/processing_render/src/particles/mod.rs b/crates/processing_render/src/particles/mod.rs index 2e131085..827343a4 100644 --- a/crates/processing_render/src/particles/mod.rs +++ b/crates/processing_render/src/particles/mod.rs @@ -100,6 +100,9 @@ pub struct Particles { /// Ring-buffer write cursor; wraps at `capacity`. pub emit_head: u32, pub neighbor_lists: Option, + /// The mesh a system made from a `Geometry` came from. Attributes created + /// later start from its data when it has them, so only what's used is copied. + pub source_mesh: Option>, } /// The `neighbors` (up to `max` indices per particle) and `neighbor_count` attributes. @@ -163,6 +166,7 @@ pub fn create( connectivity: None, emit_head: 0, neighbor_lists: None, + source_mesh: None, }) .id(); Ok(entity) @@ -245,6 +249,7 @@ pub fn create_from_geometry( connectivity, emit_head: 0, neighbor_lists: None, + source_mesh: Some(geom.handle.clone()), }) .id(); Ok(entity) @@ -434,6 +439,7 @@ pub fn materialize_attribute( mut commands: Commands, mut particles_q: Query<&mut Particles>, attributes: Query<&Attribute>, + meshes: Res>, mut shader_buffers: ResMut>, render_device: Res, ) -> Result { @@ -442,7 +448,7 @@ pub fn materialize_attribute( .map_err(|_| ProcessingError::InvalidEntity)? .clone(); - let capacity = { + let (capacity, from_mesh) = { let particles = particles_q .get(particles_entity) .map_err(|_| ProcessingError::ParticlesNotFound)?; @@ -477,7 +483,15 @@ pub fn materialize_attribute( ))), }; } - particles.capacity as usize + let capacity = particles.capacity as usize; + let from_mesh = particles + .source_mesh + .as_ref() + .and_then(|handle| meshes.get(handle)) + .and_then(|mesh| mesh.attribute(attr.inner)) + .and_then(|values| attribute_values_to_bytes(values, attr.format)) + .filter(|bytes| bytes.len() == capacity * attr.format.byte_size()); + (capacity, from_mesh) }; let elem_size = attr.format.byte_size(); @@ -506,7 +520,11 @@ pub fn materialize_attribute( } }; - let initial = tile_seed(&per_element, capacity); + // an explicit default wins over the mesh's data + let initial = match (&seed, from_mesh) { + (AttributeSeed::Declare(Some(_)), _) | (_, None) => tile_seed(&per_element, capacity), + (_, Some(bytes)) => bytes, + }; let buffer_entity = make_buffer(&mut commands, &mut shader_buffers, &render_device, &initial); particles_q .get_mut(particles_entity) From 4454209c9ea0da4bc24715471a0e95fb2966791b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sat, 3 Oct 2026 11:14:32 -0700 Subject: [PATCH 20/20] alpha.5 fixes --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/processing_pyo3/Cargo.toml | 2 +- crates/processing_render/src/compute.rs | 10 +++ .../processing_render/src/particles/emit.rs | 84 ++++++++++++++----- .../src/particles/kernels/mod.rs | 22 +++-- 6 files changed, 92 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 068fac04..1ad705e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5458,7 +5458,7 @@ dependencies = [ [[package]] name = "processing" -version = "0.0.9-alpha.5" +version = "0.0.9-alpha.6" dependencies = [ "bevy", "js-sys", @@ -5527,7 +5527,7 @@ dependencies = [ [[package]] name = "processing_pyo3" -version = "0.0.9-alpha.5" +version = "0.0.9-alpha.6" dependencies = [ "bevy", "png", diff --git a/Cargo.toml b/Cargo.toml index 55f04121..9e8eff05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "processing" -version = "0.0.9-alpha.5" +version = "0.0.9-alpha.6" edition = "2024" autoexamples = false diff --git a/crates/processing_pyo3/Cargo.toml b/crates/processing_pyo3/Cargo.toml index e79f2030..3200761c 100644 --- a/crates/processing_pyo3/Cargo.toml +++ b/crates/processing_pyo3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "processing_pyo3" -version = "0.0.9-alpha.5" +version = "0.0.9-alpha.6" edition = "2024" [lints] diff --git a/crates/processing_render/src/compute.rs b/crates/processing_render/src/compute.rs index a4b24433..c595c0db 100644 --- a/crates/processing_render/src/compute.rs +++ b/crates/processing_render/src/compute.rs @@ -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; @@ -385,6 +386,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(); diff --git a/crates/processing_render/src/particles/emit.rs b/crates/processing_render/src/particles/emit.rs index e609abf1..7c43b82b 100644 --- a/crates/processing_render/src/particles/emit.rs +++ b/crates/processing_render/src/particles/emit.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::sync::Mutex; use bevy::prelude::*; @@ -5,15 +6,76 @@ use bevy::prelude::*; use processing_core::app_mut; use processing_core::error; +use crate::compute::Compute; use crate::geometry; use crate::particles::grid::grid_build; use crate::particles::kernels::KernelRequires; use crate::particles::{Particles, particles_ensure_attribute}; use crate::shader_value::ShaderValue; use crate::{buffer_write_element, compute_create, compute_dispatch, compute_set, shader_load}; +use bevy_naga_reflect::reflect::ParameterCategory; const WORKGROUP_SIZE: u32 = 64; +/// Creates every column `compute_entity` reads or writes that the system doesn't +/// have yet: the kernel's declared requirements, plus any storage array the shader +/// names after a known attribute. A storage array left unbound would make the +/// bind group incomplete, which wgpu treats as fatal, so an unknown name is an error. +fn ensure_columns(particles_entity: Entity, compute_entity: Entity) -> error::Result<()> { + let (required, missing) = app_mut(|app| { + let world = app.world(); + let required = world + .get::(compute_entity) + .map(|r| r.0.clone()) + .unwrap_or_default(); + let compute = world + .get::(compute_entity) + .ok_or(error::ProcessingError::ComputeNotFound)?; + let particles = world + .get::(particles_entity) + .ok_or(error::ProcessingError::ParticlesNotFound)?; + let have: HashSet<&str> = particles + .buffers + .keys() + .filter_map(|&e| world.get::(e).map(|a| a.name)) + .chain( + particles + .neighbor_lists + .map(|_| ["neighbors", "neighbor_count"]) + .into_iter() + .flatten(), + ) + .collect(); + let missing: Vec = compute + .shader + .reflection() + .parameters() + .filter(|p| matches!(p.category(), ParameterCategory::Storage { .. })) + .filter_map(|p| p.name().map(str::to_string)) + .filter(|name| { + !have.contains(name.as_str()) + && compute.shader.buffer_handle(name).is_none() + && !compute.mesh_bindings.contains_key(name) + }) + .collect(); + Ok((required, missing)) + })?; + + for attr_entity in required { + particles_ensure_attribute(particles_entity, attr_entity)?; + } + for name in missing { + let attr = crate::geometry_attribute_find(name.as_str())?.ok_or_else(|| { + error::ProcessingError::InvalidArgument(format!( + "the shader uses a column named `{name}` that these particles don't have. \ + Create the column before applying it" + )) + })?; + particles_ensure_attribute(particles_entity, attr)?; + } + Ok(()) +} + pub fn particles_emit_gpu( particles_entity: Entity, count: u32, @@ -23,16 +85,7 @@ pub fn particles_emit_gpu( return Ok(()); } - let required: Vec = app_mut(|app| { - Ok(app - .world() - .get::(compute_entity) - .map(|r| r.0.clone()) - .unwrap_or_default()) - })?; - for attr_entity in required { - particles_ensure_attribute(particles_entity, attr_entity)?; - } + ensure_columns(particles_entity, compute_entity)?; let (capacity, head, buffers) = app_mut(|app| { let world = app.world(); @@ -219,16 +272,7 @@ pub fn particles_gather( } pub fn particles_apply(particles_entity: Entity, compute_entity: Entity) -> error::Result<()> { - let required: Vec = app_mut(|app| { - Ok(app - .world() - .get::(compute_entity) - .map(|r| r.0.clone()) - .unwrap_or_default()) - })?; - for attr_entity in required { - particles_ensure_attribute(particles_entity, attr_entity)?; - } + ensure_columns(particles_entity, compute_entity)?; let (capacity, buffers) = app_mut(|app| { let world = app.world(); diff --git a/crates/processing_render/src/particles/kernels/mod.rs b/crates/processing_render/src/particles/kernels/mod.rs index 83bbd851..a3600aec 100644 --- a/crates/processing_render/src/particles/kernels/mod.rs +++ b/crates/processing_render/src/particles/kernels/mod.rs @@ -8,7 +8,7 @@ use bevy::prelude::*; use processing_core::app_mut; use processing_core::error::{self, ProcessingError}; -use crate::geometry::{BuiltinAttributes, Geometry}; +use crate::geometry::Geometry; use crate::shader_value::ShaderValue; use crate::{compute_create, compute_set, shader_load}; @@ -16,13 +16,18 @@ use crate::{compute_create, compute_set, shader_load}; pub struct KernelRequires(pub Vec); pub(crate) fn set_requires(compute: Entity, names: &[&str]) -> error::Result<()> { + let attrs = names + .iter() + .map(|name| { + crate::geometry_attribute_find(*name)?.ok_or_else(|| { + ProcessingError::InvalidArgument(format!("no attribute named `{name}`")) + }) + }) + .collect::>>()?; app_mut(|app| { - let world = app.world_mut(); - let attrs: Vec = { - let builtins = world.resource::(); - names.iter().filter_map(|n| builtins.by_name(n)).collect() - }; - world.entity_mut(compute).insert(KernelRequires(attrs)); + app.world_mut() + .entity_mut(compute) + .insert(KernelRequires(attrs)); Ok(()) }) } @@ -266,7 +271,8 @@ pub fn particles_kernel_orient() -> error::Result { pub fn particles_kernel_field() -> error::Result { let shader = shader_load("embedded://processing_render/particles/kernels/field.wgsl")?; let entity = compute_create(shader)?; - set_requires(entity, &["position"])?; + crate::geometry_attribute_create("weight", crate::geometry::AttributeFormat::Float)?; + set_requires(entity, &["position", "weight"])?; compute_set(entity, "center", ShaderValue::Float3([0.0; 3]))?; compute_set(entity, "radius", ShaderValue::Float(1.0))?; compute_set(