From a4e4e1e255704c0083045dabcf1370615842ca35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Thu, 18 Dec 2025 11:50:10 -0800 Subject: [PATCH 1/3] Move away from _inner pattern. --- crates/processing_render/src/graphics.rs | 525 ++++++++++------------ crates/processing_render/src/image.rs | 528 ++++++++++------------- crates/processing_render/src/lib.rs | 196 +++++++-- crates/processing_render/src/surface.rs | 442 ++++++++----------- docs/principles.md | 26 +- 5 files changed, 806 insertions(+), 911 deletions(-) diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index 7e54931..1727e47 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -181,151 +181,121 @@ impl CameraProjection for ProcessingProjection { } pub fn create( - world: &mut World, - surface_entity: Entity, - width: u32, - height: u32, + In((width, height, surface_entity)): In<(u32, u32, Entity)>, + mut commands: Commands, + mut layer_manager: ResMut, + p_images: Query<&Image, With>, + render_device: Res, ) -> Result { - fn create_inner( - In((width, height, surface_entity)): In<(u32, u32, Entity)>, - mut commands: Commands, - mut layer_manager: ResMut, - p_images: Query<&Image, With>, - render_device: Res, - ) -> Result { - // find the surface entity, if it is an image, we will render to that image - // otherwise we will render to the window - let target = match p_images.get(surface_entity) { - Ok(p_image) => RenderTarget::Image(ImageRenderTarget::from(p_image.handle.clone())), - Err(QueryEntityError::QueryDoesNotMatch(..)) => { - RenderTarget::Window(WindowRef::Entity(surface_entity)) - } - Err(_) => return Err(ProcessingError::SurfaceNotFound), - }; - // allocate a new render layer for this graphics entity, which ensures that anything - // drawn to this camera will only be visible to this camera - let render_layer = layer_manager.allocate(); - - // TODO: make this configurable, right now we are hard-coding hdr camera - let texture_format = TextureFormat::Rgba16Float; - let size = Extent3d { - width, - height, - depth_or_array_layers: 1, - }; - let readback_buffer = create_readback_buffer( - &render_device, - width, - height, - texture_format, - "Graphics Readback Buffer", - ) - .expect("Failed to create readback buffer"); - - let entity = commands - .spawn(( - Camera3d::default(), - Camera { - target, - // always load the previous frame (provides sketch like behavior) - clear_color: ClearColorConfig::None, - // TODO: toggle this conditionally based on whether we need to write back MSAA - // when doing manual pixel udpates - msaa_writeback: MsaaWriteback::Always, - ..default() - }, - // default to floating point texture format - Hdr, - // tonemapping prevents color accurate readback, so we disable it - Tonemapping::None, - // we need to be able to write to the texture - CameraMainTextureUsages::default().with(TextureUsages::COPY_DST), - Projection::custom(ProcessingProjection { - width: width as f32, - height: height as f32, - near: 0.0, - far: 1000.0, - }), - Transform::from_xyz(0.0, 0.0, 999.9), - render_layer, - CommandBuffer::new(), - RenderState::default(), - SurfaceSize(width, height), - Graphics { - readback_buffer, - texture_format, - size, - }, - )) - .id(); - - Ok(entity) - } - - world - .run_system_cached_with(create_inner, (width, height, surface_entity)) - .unwrap() -} - -#[allow(dead_code)] -pub fn resize(world: &mut World, entity: Entity, width: u32, height: u32) -> Result<()> { - fn resize_inner( - In((entity, width, height)): In<(Entity, u32, u32)>, - mut graphics_query: Query<&mut Projection>, - ) -> Result<()> { - let mut projection = graphics_query - .get_mut(entity) - .map_err(|_| ProcessingError::GraphicsNotFound)?; - - if let Projection::Custom(ref mut custom_proj) = *projection { - custom_proj.update(width as f32, height as f32); - Ok(()) - } else { - panic!( - "Expected custom projection for Processing graphics entity, this should not happen. If you are seeing this message, please report a bug." - ); + // find the surface entity, if it is an image, we will render to that image + // otherwise we will render to the window + let target = match p_images.get(surface_entity) { + Ok(p_image) => RenderTarget::Image(ImageRenderTarget::from(p_image.handle.clone())), + Err(QueryEntityError::QueryDoesNotMatch(..)) => { + RenderTarget::Window(WindowRef::Entity(surface_entity)) } - } + Err(_) => return Err(ProcessingError::SurfaceNotFound), + }; + // allocate a new render layer for this graphics entity, which ensures that anything + // drawn to this camera will only be visible to this camera + let render_layer = layer_manager.allocate(); + + // TODO: make this configurable, right now we are hard-coding hdr camera + let texture_format = TextureFormat::Rgba16Float; + let size = Extent3d { + width, + height, + depth_or_array_layers: 1, + }; + let readback_buffer = create_readback_buffer( + &render_device, + width, + height, + texture_format, + "Graphics Readback Buffer", + ) + .expect("Failed to create readback buffer"); + + let entity = commands + .spawn(( + Camera3d::default(), + Camera { + target, + // always load the previous frame (provides sketch like behavior) + clear_color: ClearColorConfig::None, + // TODO: toggle this conditionally based on whether we need to write back MSAA + // when doing manual pixel udpates + msaa_writeback: MsaaWriteback::Always, + ..default() + }, + // default to floating point texture format + Hdr, + // tonemapping prevents color accurate readback, so we disable it + Tonemapping::None, + // we need to be able to write to the texture + CameraMainTextureUsages::default().with(TextureUsages::COPY_DST), + Projection::custom(ProcessingProjection { + width: width as f32, + height: height as f32, + near: 0.0, + far: 1000.0, + }), + Transform::from_xyz(0.0, 0.0, 999.9), + render_layer, + CommandBuffer::new(), + RenderState::default(), + SurfaceSize(width, height), + Graphics { + readback_buffer, + texture_format, + size, + }, + )) + .id(); - world - .run_system_cached_with(resize_inner, (entity, width, height)) - .unwrap() + Ok(entity) } -pub fn destroy(world: &mut World, entity: Entity) -> Result<()> { - fn destroy_inner( - In(entity): In, - mut commands: Commands, - mut layer_manager: ResMut, - graphics_query: Query<&RenderLayers>, - ) -> Result<()> { - let Ok(render_layers) = graphics_query.get(entity) else { - return Err(ProcessingError::GraphicsNotFound); - }; +#[allow(dead_code)] +pub fn resize( + In((entity, width, height)): In<(Entity, u32, u32)>, + mut graphics_query: Query<&mut Projection>, +) -> Result<()> { + let mut projection = graphics_query + .get_mut(entity) + .map_err(|_| ProcessingError::GraphicsNotFound)?; - layer_manager.free(render_layers.clone()); - commands.entity(entity).despawn(); + if let Projection::Custom(ref mut custom_proj) = *projection { + custom_proj.update(width as f32, height as f32); Ok(()) + } else { + panic!( + "Expected custom projection for Processing graphics entity, this should not happen. If you are seeing this message, please report a bug." + ); } - - world.run_system_cached_with(destroy_inner, entity).unwrap() } -pub fn begin_draw(world: &mut World, entity: Entity) -> Result<()> { - fn begin_draw_inner( - In(entity): In, - mut state_query: Query<&mut RenderState>, - ) -> Result<()> { - let mut state = state_query - .get_mut(entity) - .map_err(|_| ProcessingError::GraphicsNotFound)?; - state.reset(); - Ok(()) - } +pub fn destroy( + In(entity): In, + mut commands: Commands, + mut layer_manager: ResMut, + graphics_query: Query<&RenderLayers>, +) -> Result<()> { + let Ok(render_layers) = graphics_query.get(entity) else { + return Err(ProcessingError::GraphicsNotFound); + }; + + layer_manager.free(render_layers.clone()); + commands.entity(entity).despawn(); + Ok(()) +} - world - .run_system_cached_with(begin_draw_inner, entity) - .unwrap() +pub fn begin_draw(In(entity): In, mut state_query: Query<&mut RenderState>) -> Result<()> { + let mut state = state_query + .get_mut(entity) + .map_err(|_| ProcessingError::GraphicsNotFound)?; + state.reset(); + Ok(()) } pub fn flush(app: &mut App, entity: Entity) -> Result<()> { @@ -359,203 +329,170 @@ pub fn end_draw(app: &mut App, entity: Entity) -> Result<()> { Ok(()) } -pub fn record_command(world: &mut World, window_entity: Entity, cmd: DrawCommand) -> Result<()> { - fn record_command_inner( - In((graphics_entity, cmd)): In<(Entity, DrawCommand)>, - mut graphics_query: Query<&mut CommandBuffer>, - ) -> Result<()> { - let mut command_buffer = graphics_query - .get_mut(graphics_entity) - .map_err(|_| ProcessingError::GraphicsNotFound)?; - - command_buffer.push(cmd); - Ok(()) - } +pub fn record_command( + In((graphics_entity, cmd)): In<(Entity, DrawCommand)>, + mut graphics_query: Query<&mut CommandBuffer>, +) -> Result<()> { + let mut command_buffer = graphics_query + .get_mut(graphics_entity) + .map_err(|_| ProcessingError::GraphicsNotFound)?; - world - .run_system_cached_with(record_command_inner, (window_entity, cmd)) - .unwrap() + command_buffer.push(cmd); + Ok(()) } -pub fn readback(world: &mut World, entity: Entity) -> Result> { - fn readback_inner( - In(entity): In, - graphics_query: Query<&Graphics>, - graphics_targets: Res, - render_device: Res, - render_queue: Res, - ) -> Result> { - let graphics = graphics_query - .get(entity) - .map_err(|_| ProcessingError::GraphicsNotFound)?; - - let view_target = graphics_targets - .get(&entity) - .ok_or(ProcessingError::GraphicsNotFound)?; - - let texture = view_target.main_texture(); - eprintln!("readback: reading from texture {:p}", texture as *const _); - - let mut encoder = - render_device.create_command_encoder(&CommandEncoderDescriptor::default()); - - let px_size = pixel_size(graphics.texture_format)?; - let padded_bytes_per_row = - RenderDevice::align_copy_bytes_per_row(graphics.size.width as usize * px_size); - - encoder.copy_texture_to_buffer( - texture.as_image_copy(), - TexelCopyBufferInfo { - buffer: &graphics.readback_buffer, - layout: TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some( - std::num::NonZero::::new(padded_bytes_per_row as u32) - .unwrap() - .into(), - ), - rows_per_image: None, - }, +pub fn readback( + In(entity): In, + graphics_query: Query<&Graphics>, + graphics_targets: Res, + render_device: Res, + render_queue: Res, +) -> Result> { + let graphics = graphics_query + .get(entity) + .map_err(|_| ProcessingError::GraphicsNotFound)?; + + let view_target = graphics_targets + .get(&entity) + .ok_or(ProcessingError::GraphicsNotFound)?; + + let texture = view_target.main_texture(); + + let mut encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor::default()); + + let px_size = pixel_size(graphics.texture_format)?; + let padded_bytes_per_row = + RenderDevice::align_copy_bytes_per_row(graphics.size.width as usize * px_size); + + encoder.copy_texture_to_buffer( + texture.as_image_copy(), + TexelCopyBufferInfo { + buffer: &graphics.readback_buffer, + layout: TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some( + std::num::NonZero::::new(padded_bytes_per_row as u32) + .unwrap() + .into(), + ), + rows_per_image: None, }, - graphics.size, - ); + }, + graphics.size, + ); - render_queue.submit(std::iter::once(encoder.finish())); + render_queue.submit(std::iter::once(encoder.finish())); - let buffer_slice = graphics.readback_buffer.slice(..); + let buffer_slice = graphics.readback_buffer.slice(..); - let (s, r) = crossbeam_channel::bounded(1); + let (s, r) = crossbeam_channel::bounded(1); - buffer_slice.map_async(MapMode::Read, move |r| match r { - Ok(r) => s.send(r).expect("Failed to send map update"), - Err(err) => panic!("Failed to map buffer {err}"), - }); + buffer_slice.map_async(MapMode::Read, move |r| match r { + Ok(r) => s.send(r).expect("Failed to send map update"), + Err(err) => panic!("Failed to map buffer {err}"), + }); - render_device - .poll(PollType::Wait) - .expect("Failed to poll device for map async"); + render_device + .poll(PollType::Wait) + .expect("Failed to poll device for map async"); - r.recv().expect("Failed to receive the map_async message"); + 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().to_vec(); - graphics.readback_buffer.unmap(); + graphics.readback_buffer.unmap(); - bytes_to_pixels( - &data, - graphics.texture_format, - graphics.size.width, - graphics.size.height, - padded_bytes_per_row, - ) + bytes_to_pixels( + &data, + graphics.texture_format, + graphics.size.width, + graphics.size.height, + padded_bytes_per_row, + ) +} + +pub fn update_region_write( + In((entity, x, y, width, height, data, px_size)): In<(Entity, u32, u32, u32, u32, Vec, u32)>, + graphics_query: Query<&Graphics>, + graphics_targets: Res, + render_queue: Res, +) -> Result<()> { + let graphics = graphics_query + .get(entity) + .map_err(|_| ProcessingError::GraphicsNotFound)?; + + // bounds check + if x + width > graphics.size.width || y + height > graphics.size.height { + return Err(ProcessingError::InvalidArgument(format!( + "Region ({}, {}, {}, {}) exceeds graphics bounds ({}, {})", + x, y, width, height, graphics.size.width, graphics.size.height + ))); } - world - .run_system_cached_with(readback_inner, entity) - .expect("Failed to run readback system") + let view_target = graphics_targets + .get(&entity) + .ok_or(ProcessingError::GraphicsNotFound)?; + + let texture = view_target.main_texture(); + eprintln!( + "update_region: writing to texture {:p} at ({}, {}) size {}x{}", + texture as *const _, + x, + y, + width, + height + ); + let bytes_per_row = width * px_size; + + render_queue.write_texture( + TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: Origin3d { x, y, z: 0 }, + aspect: Default::default(), + }, + &data, + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(bytes_per_row), + rows_per_image: None, + }, + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + + Ok(()) } -pub fn update_region( - world: &mut World, +pub fn prepare_update_region( + world: &World, entity: Entity, - x: u32, - y: u32, width: u32, height: u32, pixels: &[LinearRgba], -) -> Result<()> { +) -> Result<(Vec, u32)> { let expected_count = (width * height) as usize; if pixels.len() != expected_count { return Err(ProcessingError::InvalidArgument(format!( "Expected {} pixels for {}x{} region, got {}", - expected_count, - width, - height, + expected_count, width, height, pixels.len() ))); } - fn update_region_inner( - In((entity, x, y, width, height, data, px_size)): In<( - Entity, - u32, - u32, - u32, - u32, - Vec, - u32, - )>, - graphics_query: Query<&Graphics>, - graphics_targets: Res, - render_queue: Res, - ) -> Result<()> { - let graphics = graphics_query - .get(entity) - .map_err(|_| ProcessingError::GraphicsNotFound)?; - - // bounds check - if x + width > graphics.size.width || y + height > graphics.size.height { - return Err(ProcessingError::InvalidArgument(format!( - "Region ({}, {}, {}, {}) exceeds graphics bounds ({}, {})", - x, y, width, height, graphics.size.width, graphics.size.height - ))); - } - - let view_target = graphics_targets - .get(&entity) - .ok_or(ProcessingError::GraphicsNotFound)?; - - let texture = view_target.main_texture(); - eprintln!( - "update_region: writing to texture {:p} at ({}, {}) size {}x{}", - texture as *const _, x, y, width, height - ); - let bytes_per_row = width * px_size; - - render_queue.write_texture( - TexelCopyTextureInfo { - texture, - mip_level: 0, - origin: Origin3d { x, y, z: 0 }, - aspect: Default::default(), - }, - &data, - TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(bytes_per_row), - rows_per_image: None, - }, - Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - ); - - Ok(()) - } - let graphics = world .get::(entity) .ok_or(ProcessingError::GraphicsNotFound)?; let px_size = pixel_size(graphics.texture_format)? as u32; let data = pixels_to_bytes(pixels, graphics.texture_format)?; - world - .run_system_cached_with( - update_region_inner, - (entity, x, y, width, height, data, px_size), - ) - .expect("Failed to run update_region system") + Ok((data, px_size)) } -pub fn update(world: &mut World, entity: Entity, pixels: &[LinearRgba]) -> Result<()> { - let size = world - .get::(entity) - .ok_or(ProcessingError::GraphicsNotFound)? - .size; - update_region(world, entity, 0, 0, size.width, size.height, pixels) -} #[derive(Resource, Debug, Clone, Reflect)] pub struct RenderLayersManager { diff --git a/crates/processing_render/src/image.rs b/crates/processing_render/src/image.rs index f0b59b6..4d492a6 100644 --- a/crates/processing_render/src/image.rs +++ b/crates/processing_render/src/image.rs @@ -63,48 +63,37 @@ fn sync_textures(mut main_world: ResMut, gpu_images: Res, - texture_format: TextureFormat, + In((size, data, texture_format)): In<(Extent3d, Vec, TextureFormat)>, + mut commands: Commands, + mut images: ResMut>, + render_device: Res, ) -> Entity { - fn create_inner( - In((size, data, texture_format)): In<(Extent3d, Vec, TextureFormat)>, - mut commands: Commands, - mut images: ResMut>, - render_device: Res, - ) -> Entity { - let image = bevy::image::Image::new( - size, - TextureDimension::D2, - data, - texture_format, - RenderAssetUsages::all(), - ); - - let handle = images.add(image); - let readback_buffer = create_readback_buffer( - &render_device, - size.width, - size.height, - texture_format, - "Image Readback Buffer", - ) - .expect("Failed to create readback buffer"); - - commands - .spawn((Image { - handle: handle.clone(), - readback_buffer, - texture_format, - size, - },)) - .id() - } + let image = bevy::image::Image::new( + size, + TextureDimension::D2, + data, + texture_format, + RenderAssetUsages::all(), + ); + + let handle = images.add(image); + let readback_buffer = create_readback_buffer( + &render_device, + size.width, + size.height, + texture_format, + "Image Readback Buffer", + ) + .expect("Failed to create readback buffer"); - world - .run_system_cached_with(create_inner, (size, data, texture_format)) - .unwrap() + commands + .spawn((Image { + handle: handle.clone(), + readback_buffer, + texture_format, + size, + },)) + .id() } pub fn load_start(world: &mut World, path: PathBuf) -> Handle { @@ -119,307 +108,252 @@ pub fn is_loaded(world: &World, handle: &Handle) -> bool { } #[cfg(target_arch = "wasm32")] -pub fn from_handle(world: &mut World, handle: Handle) -> Result { - fn from_handle_inner( - In(handle): In>, - world: &mut World, - ) -> Result { - let images = world.resource::>(); - let image = images.get(&handle).ok_or(ProcessingError::ImageNotFound)?; - - let size = image.texture_descriptor.size; - let texture_format = image.texture_descriptor.format; - - let render_device = world.resource::(); - let readback_buffer = create_readback_buffer( - render_device, - size.width, - size.height, +pub fn from_handle( + In(handle): In>, + world: &mut World, +) -> Result { + let images = world.resource::>(); + let image = images.get(&handle).ok_or(ProcessingError::ImageNotFound)?; + + let size = image.texture_descriptor.size; + let texture_format = image.texture_descriptor.format; + + let render_device = world.resource::(); + let readback_buffer = create_readback_buffer( + render_device, + size.width, + size.height, + texture_format, + "Image Readback Buffer", + )?; + + Ok(world + .spawn(Image { + handle: handle.clone(), + readback_buffer, texture_format, - "Image Readback Buffer", - )?; - - Ok(world - .spawn(Image { - handle: handle.clone(), - readback_buffer, - texture_format, - size, - }) - .id()) - } - - world - .run_system_cached_with(from_handle_inner, handle) - .expect("Failed to run from_handle system") + size, + }) + .id()) } -pub fn load(world: &mut World, path: PathBuf) -> Result { - fn load_inner(In(path): In, world: &mut World) -> Result { - let handle: Handle = world.get_asset_server().load(path); - while let LoadState::Loading = world.get_asset_server().load_state(&handle) { - world.run_system_once(handle_internal_asset_events).unwrap(); - } - let images = world.resource::>(); - let image = images.get(&handle).ok_or(ProcessingError::ImageNotFound)?; - - let size = image.texture_descriptor.size; - let texture_format = image.texture_descriptor.format; - - let render_device = world.resource::(); - let readback_buffer = create_readback_buffer( - render_device, - size.width, - size.height, - texture_format, - "Image Readback Buffer", - )?; - - Ok(world - .spawn(Image { - handle: handle.clone(), - readback_buffer, - texture_format, - size, - }) - .id()) +pub fn load(In(path): In, world: &mut World) -> Result { + let handle: Handle = world.get_asset_server().load(path); + while let LoadState::Loading = world.get_asset_server().load_state(&handle) { + world.run_system_once(handle_internal_asset_events).unwrap(); } - - world - .run_system_cached_with(load_inner, path.to_path_buf()) - .expect("Failed to run load system") + let images = world.resource::>(); + let image = images.get(&handle).ok_or(ProcessingError::ImageNotFound)?; + + let size = image.texture_descriptor.size; + let texture_format = image.texture_descriptor.format; + + let render_device = world.resource::(); + let readback_buffer = create_readback_buffer( + render_device, + size.width, + size.height, + texture_format, + "Image Readback Buffer", + )?; + + Ok(world + .spawn(Image { + handle: handle.clone(), + readback_buffer, + texture_format, + size, + }) + .id()) } -pub fn resize(world: &mut World, entity: Entity, new_size: Extent3d) -> Result<()> { - fn resize_inner( - In((entity, new_size)): In<(Entity, Extent3d)>, - mut p_images: Query<&mut Image>, - mut images: ResMut>, - render_device: Res, - ) -> Result<()> { - let mut p_image = p_images - .get_mut(entity) - .map_err(|_| ProcessingError::ImageNotFound)?; - - images - .get_mut(&p_image.handle) - .ok_or(ProcessingError::ImageNotFound)? - .resize_in_place(new_size); - - p_image.readback_buffer = create_readback_buffer( - &render_device, - new_size.width, - new_size.height, - p_image.texture_format, - "Image Readback Buffer", - )?; - p_image.size = new_size; - - Ok(()) - } +pub fn resize( + In((entity, new_size)): In<(Entity, Extent3d)>, + mut p_images: Query<&mut Image>, + mut images: ResMut>, + render_device: Res, +) -> Result<()> { + let mut p_image = p_images + .get_mut(entity) + .map_err(|_| ProcessingError::ImageNotFound)?; - world - .run_system_cached_with(resize_inner, (entity, new_size)) - .expect("Failed to run resize system") + images + .get_mut(&p_image.handle) + .ok_or(ProcessingError::ImageNotFound)? + .resize_in_place(new_size); + + p_image.readback_buffer = create_readback_buffer( + &render_device, + new_size.width, + new_size.height, + p_image.texture_format, + "Image Readback Buffer", + )?; + p_image.size = new_size; + + Ok(()) } -pub fn readback(world: &mut World, entity: Entity) -> Result> { - fn readback_inner( - In(entity): In, - p_images: Query<&Image>, - p_image_textures: Res, - mut images: ResMut>, - render_device: Res, - render_queue: ResMut, - ) -> Result> { - let p_image = p_images - .get(entity) - .map_err(|_| ProcessingError::ImageNotFound)?; - let texture = p_image_textures - .get(&entity) - .ok_or(ProcessingError::ImageNotFound)?; - - let mut encoder = - render_device.create_command_encoder(&CommandEncoderDescriptor::default()); - - let px_size = pixel_size(p_image.texture_format)?; - let padded_bytes_per_row = - RenderDevice::align_copy_bytes_per_row(p_image.size.width as usize * px_size); - - encoder.copy_texture_to_buffer( - texture.as_image_copy(), - TexelCopyBufferInfo { - buffer: &p_image.readback_buffer, - layout: TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some( - std::num::NonZero::::new(padded_bytes_per_row as u32) - .unwrap() - .into(), - ), - rows_per_image: None, - }, +pub fn readback( + In(entity): In, + p_images: Query<&Image>, + p_image_textures: Res, + mut images: ResMut>, + render_device: Res, + render_queue: ResMut, +) -> Result> { + let p_image = p_images + .get(entity) + .map_err(|_| ProcessingError::ImageNotFound)?; + let texture = p_image_textures + .get(&entity) + .ok_or(ProcessingError::ImageNotFound)?; + + let mut encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor::default()); + + let px_size = pixel_size(p_image.texture_format)?; + let padded_bytes_per_row = + RenderDevice::align_copy_bytes_per_row(p_image.size.width as usize * px_size); + + encoder.copy_texture_to_buffer( + texture.as_image_copy(), + TexelCopyBufferInfo { + buffer: &p_image.readback_buffer, + layout: TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some( + std::num::NonZero::::new(padded_bytes_per_row as u32) + .unwrap() + .into(), + ), + rows_per_image: None, }, - p_image.size, - ); + }, + p_image.size, + ); - render_queue.submit(std::iter::once(encoder.finish())); + render_queue.submit(std::iter::once(encoder.finish())); - let buffer_slice = p_image.readback_buffer.slice(..); + let buffer_slice = p_image.readback_buffer.slice(..); - let (s, r) = crossbeam_channel::bounded(1); + let (s, r) = crossbeam_channel::bounded(1); - buffer_slice.map_async(MapMode::Read, move |r| match r { - Ok(r) => s.send(r).expect("Failed to send map update"), - Err(err) => panic!("Failed to map buffer {err}"), - }); + buffer_slice.map_async(MapMode::Read, move |r| match r { + Ok(r) => s.send(r).expect("Failed to send map update"), + Err(err) => panic!("Failed to map buffer {err}"), + }); - render_device - .poll(PollType::Wait) - .expect("Failed to poll device for map async"); + render_device + .poll(PollType::Wait) + .expect("Failed to poll device for map async"); - r.recv().expect("Failed to receive the map_async message"); + 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().to_vec(); - let image = images - .get_mut(&p_image.handle) - .ok_or(ProcessingError::ImageNotFound)?; - image.data = Some(data.clone()); + let image = images + .get_mut(&p_image.handle) + .ok_or(ProcessingError::ImageNotFound)?; + image.data = Some(data.clone()); + + p_image.readback_buffer.unmap(); - p_image.readback_buffer.unmap(); + bytes_to_pixels( + &data, + p_image.texture_format, + p_image.size.width, + p_image.size.height, + padded_bytes_per_row, + ) +} - bytes_to_pixels( - &data, - p_image.texture_format, - p_image.size.width, - p_image.size.height, - padded_bytes_per_row, - ) +pub fn update_region_write( + In((entity, x, y, width, height, data, px_size)): In<(Entity, u32, u32, u32, u32, Vec, u32)>, + p_images: Query<&Image>, + p_image_textures: Res, + render_queue: Res, +) -> Result<()> { + let p_image = p_images + .get(entity) + .map_err(|_| ProcessingError::ImageNotFound)?; + + if x + width > p_image.size.width || y + height > p_image.size.height { + return Err(ProcessingError::InvalidArgument(format!( + "Region ({}, {}, {}, {}) exceeds image bounds ({}, {})", + x, y, width, height, p_image.size.width, p_image.size.height + ))); } - world - .run_system_cached_with(readback_inner, entity) - .expect("Failed to run readback system") + let texture = p_image_textures + .get(&entity) + .ok_or(ProcessingError::ImageNotFound)?; + + let bytes_per_row = width * px_size; + + render_queue.write_texture( + TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: Origin3d { x, y, z: 0 }, + aspect: Default::default(), + }, + &data, + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(bytes_per_row), + rows_per_image: None, + }, + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + + Ok(()) } -pub fn update_region( - world: &mut World, +pub fn prepare_update_region( + world: &World, entity: Entity, - x: u32, - y: u32, width: u32, height: u32, pixels: &[LinearRgba], -) -> Result<()> { +) -> Result<(Vec, u32)> { let expected_count = (width * height) as usize; if pixels.len() != expected_count { return Err(ProcessingError::InvalidArgument(format!( "Expected {} pixels for {}x{} region, got {}", - expected_count, - width, - height, + expected_count, width, height, pixels.len() ))); } - fn update_region_inner( - In((entity, x, y, width, height, data, px_size)): In<( - Entity, - u32, - u32, - u32, - u32, - Vec, - u32, - )>, - p_images: Query<&Image>, - p_image_textures: Res, - render_queue: Res, - ) -> Result<()> { - let p_image = p_images - .get(entity) - .map_err(|_| ProcessingError::ImageNotFound)?; - - if x + width > p_image.size.width || y + height > p_image.size.height { - return Err(ProcessingError::InvalidArgument(format!( - "Region ({}, {}, {}, {}) exceeds image bounds ({}, {})", - x, y, width, height, p_image.size.width, p_image.size.height - ))); - } - - let texture = p_image_textures - .get(&entity) - .ok_or(ProcessingError::ImageNotFound)?; - - let bytes_per_row = width * px_size; - - render_queue.write_texture( - TexelCopyTextureInfo { - texture, - mip_level: 0, - origin: Origin3d { x, y, z: 0 }, - aspect: Default::default(), - }, - &data, - TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(bytes_per_row), - rows_per_image: None, - }, - Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - ); - - Ok(()) - } - let p_image = world .get::(entity) .ok_or(ProcessingError::ImageNotFound)?; let px_size = pixel_size(p_image.texture_format)? as u32; let data = pixels_to_bytes(pixels, p_image.texture_format)?; - world - .run_system_cached_with( - update_region_inner, - (entity, x, y, width, height, data, px_size), - ) - .expect("Failed to run update_region system") + Ok((data, px_size)) } -pub fn update(world: &mut World, entity: Entity, pixels: &[LinearRgba]) -> Result<()> { - let size = world - .get::(entity) - .ok_or(ProcessingError::ImageNotFound)? - .size; - update_region(world, entity, 0, 0, size.width, size.height, pixels) -} - -pub fn destroy(world: &mut World, entity: Entity) -> Result<()> { - fn destroy_inner( - In(entity): In, - mut commands: Commands, - mut p_images: Query<&mut Image>, - mut images: ResMut>, - mut p_image_textures: ResMut, - ) -> Result<()> { - let p_image = p_images - .get_mut(entity) - .map_err(|_| ProcessingError::ImageNotFound)?; - - images.remove(&p_image.handle); - p_image_textures.remove(&entity); - commands.entity(entity).despawn(); - Ok(()) - } - - world - .run_system_cached_with(destroy_inner, entity) - .expect("Failed to run destroy system") +pub fn destroy( + In(entity): In, + mut commands: Commands, + mut p_images: Query<&mut Image>, + mut images: ResMut>, + mut p_image_textures: ResMut, +) -> Result<()> { + let p_image = p_images + .get_mut(entity) + .map_err(|_| ProcessingError::ImageNotFound)?; + + images.remove(&p_image.handle); + p_image_textures.remove(&entity); + commands.entity(entity).despawn(); + Ok(()) } /// Get the size in bytes of a single pixel for the given texture format. diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index c5611e4..00d3efb 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -51,7 +51,12 @@ pub fn surface_create_macos( scale_factor: f32, ) -> error::Result { app_mut(|app| { - surface::create_surface_macos(app.world_mut(), window_handle, width, height, scale_factor) + app.world_mut() + .run_system_cached_with( + surface::create_surface_macos, + (window_handle, width, height, scale_factor), + ) + .unwrap() }) } @@ -64,7 +69,12 @@ pub fn surface_create_windows( scale_factor: f32, ) -> error::Result { app_mut(|app| { - surface::create_surface_windows(app.world_mut(), window_handle, width, height, scale_factor) + app.world_mut() + .run_system_cached_with( + surface::create_surface_windows, + (window_handle, width, height, scale_factor), + ) + .unwrap() }) } @@ -78,14 +88,12 @@ pub fn surface_create_wayland( scale_factor: f32, ) -> error::Result { app_mut(|app| { - surface::create_surface_wayland( - app.world_mut(), - window_handle, - display_handle, - width, - height, - scale_factor, - ) + app.world_mut() + .run_system_cached_with( + surface::create_surface_wayland, + (window_handle, display_handle, width, height, scale_factor), + ) + .unwrap() }) } @@ -99,14 +107,12 @@ pub fn surface_create_x11( scale_factor: f32, ) -> error::Result { app_mut(|app| { - surface::create_surface_x11( - app.world_mut(), - window_handle, - display_handle, - width, - height, - scale_factor, - ) + app.world_mut() + .run_system_cached_with( + surface::create_surface_x11, + (window_handle, display_handle, width, height, scale_factor), + ) + .unwrap() }) } @@ -119,7 +125,12 @@ pub fn surface_create_web( scale_factor: f32, ) -> error::Result { app_mut(|app| { - surface::create_surface_web(app.world_mut(), window_handle, width, height, scale_factor) + app.world_mut() + .run_system_cached_with( + surface::create_surface_web, + (window_handle, width, height, scale_factor), + ) + .unwrap() }) } @@ -130,7 +141,14 @@ pub fn surface_create_offscreen( texture_format: TextureFormat, ) -> error::Result { app_mut(|app| { - surface::create_offscreen(app.world_mut(), width, height, scale_factor, texture_format) + let (size, data, texture_format) = + surface::prepare_offscreen(width, height, scale_factor, texture_format)?; + let world = app.world_mut(); + let image_entity = world + .run_system_cached_with(image::create, (size, data, texture_format)) + .unwrap(); + world.entity_mut(image_entity).insert(surface::Surface); + Ok(image_entity) }) } @@ -167,12 +185,20 @@ pub fn surface_create_from_canvas( } pub fn surface_destroy(graphics_entity: Entity) -> error::Result<()> { - app_mut(|app| surface::destroy(app.world_mut(), graphics_entity)) + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::destroy, graphics_entity) + .unwrap() + }) } /// Update window size when resized. pub fn surface_resize(graphics_entity: Entity, width: u32, height: u32) -> error::Result<()> { - app_mut(|app| surface::resize(app.world_mut(), graphics_entity, width, height)) + app_mut(|app| { + app.world_mut() + .run_system_cached_with(surface::resize, (graphics_entity, width, height)) + .unwrap() + }) } fn create_app() -> App { @@ -279,12 +305,20 @@ pub async fn init() -> error::Result<()> { /// Create a new graphics surface for rendering. pub fn graphics_create(surface_entity: Entity, width: u32, height: u32) -> error::Result { - app_mut(|app| graphics::create(app.world_mut(), surface_entity, width, height)) + app_mut(|app| { + app.world_mut() + .run_system_cached_with(graphics::create, (width, height, surface_entity)) + .unwrap() + }) } /// Begin a new draw pass for the graphics surface. pub fn graphics_begin_draw(graphics_entity: Entity) -> error::Result<()> { - app_mut(|app| graphics::begin_draw(app.world_mut(), graphics_entity)) + app_mut(|app| { + app.world_mut() + .run_system_cached_with(graphics::begin_draw, graphics_entity) + .unwrap() + }) } /// Flush current pending draw commands to the graphics surface. @@ -299,20 +333,40 @@ pub fn graphics_end_draw(graphics_entity: Entity) -> error::Result<()> { /// Destroy the graphics surface and free its resources. pub fn graphics_destroy(graphics_entity: Entity) -> error::Result<()> { - app_mut(|app| graphics::destroy(app.world_mut(), graphics_entity)) + app_mut(|app| { + app.world_mut() + .run_system_cached_with(graphics::destroy, graphics_entity) + .unwrap() + }) } /// Read back pixel data from the graphics surface. pub fn graphics_readback(graphics_entity: Entity) -> error::Result> { app_mut(|app| { graphics::flush(app, graphics_entity)?; - graphics::readback(app.world_mut(), graphics_entity) + app.world_mut() + .run_system_cached_with(graphics::readback, graphics_entity) + .unwrap() }) } /// Update the graphics surface with new pixel data. pub fn graphics_update(graphics_entity: Entity, pixels: &[LinearRgba]) -> error::Result<()> { - app_mut(|app| graphics::update(app.world_mut(), graphics_entity, pixels)) + app_mut(|app| { + let world = app.world_mut(); + let size = world + .get::(graphics_entity) + .ok_or(error::ProcessingError::GraphicsNotFound)? + .size; + let (data, px_size) = + graphics::prepare_update_region(world, graphics_entity, size.width, size.height, pixels)?; + world + .run_system_cached_with( + graphics::update_region_write, + (graphics_entity, 0, 0, size.width, size.height, data, px_size), + ) + .unwrap() + }) } /// Update a region of the graphics surface with new pixel data. @@ -325,15 +379,15 @@ pub fn graphics_update_region( pixels: &[LinearRgba], ) -> error::Result<()> { app_mut(|app| { - graphics::update_region( - app.world_mut(), - graphics_entity, - x, - y, - width, - height, - pixels, - ) + let world = app.world_mut(); + let (data, px_size) = + graphics::prepare_update_region(world, graphics_entity, width, height, pixels)?; + world + .run_system_cached_with( + graphics::update_region_write, + (graphics_entity, x, y, width, height, data, px_size), + ) + .unwrap() }) } @@ -371,7 +425,11 @@ fn setup_tracing() -> error::Result<()> { /// Record a drawing command for a window pub fn graphics_record_command(graphics_entity: Entity, cmd: DrawCommand) -> error::Result<()> { - app_mut(|app| graphics::record_command(app.world_mut(), graphics_entity, cmd)) + app_mut(|app| { + app.world_mut() + .run_system_cached_with(graphics::record_command, (graphics_entity, cmd)) + .unwrap() + }) } /// Create a new image with given size and data. @@ -380,14 +438,23 @@ pub fn image_create( data: Vec, texture_format: TextureFormat, ) -> error::Result { - app_mut(|app| Ok(image::create(app.world_mut(), size, data, texture_format))) + app_mut(|app| { + Ok(app + .world_mut() + .run_system_cached_with(image::create, (size, data, texture_format)) + .unwrap()) + }) } /// Load an image from disk. #[cfg(not(target_arch = "wasm32"))] pub fn image_load(path: &str) -> error::Result { let path = PathBuf::from(path); - app_mut(|app| image::load(app.world_mut(), path)) + app_mut(|app| { + app.world_mut() + .run_system_cached_with(image::load, path) + .unwrap() + }) } #[cfg(target_arch = "wasm32")] @@ -422,22 +489,48 @@ pub async fn image_load(path: &str) -> error::Result { })?; } - app_mut(|app| image::from_handle(app.world_mut(), handle)) + app_mut(|app| { + app.world_mut() + .run_system_cached_with(image::from_handle, handle) + .unwrap() + }) } /// Resize an existing image to new size. pub fn image_resize(entity: Entity, new_size: Extent3d) -> error::Result<()> { - app_mut(|app| image::resize(app.world_mut(), entity, new_size)) + app_mut(|app| { + app.world_mut() + .run_system_cached_with(image::resize, (entity, new_size)) + .unwrap() + }) } /// Read back image data from GPU to CPU. pub fn image_readback(entity: Entity) -> error::Result> { - app_mut(|app| image::readback(app.world_mut(), entity)) + app_mut(|app| { + app.world_mut() + .run_system_cached_with(image::readback, entity) + .unwrap() + }) } /// Update an existing image with new pixel data. pub fn image_update(entity: Entity, pixels: &[LinearRgba]) -> error::Result<()> { - app_mut(|app| image::update(app.world_mut(), entity, pixels)) + app_mut(|app| { + let world = app.world_mut(); + let size = world + .get::(entity) + .ok_or(error::ProcessingError::ImageNotFound)? + .size; + let (data, px_size) = + image::prepare_update_region(world, entity, size.width, size.height, pixels)?; + world + .run_system_cached_with( + image::update_region_write, + (entity, 0, 0, size.width, size.height, data, px_size), + ) + .unwrap() + }) } /// Update a region of an existing image with new pixel data. @@ -449,10 +542,23 @@ pub fn image_update_region( height: u32, pixels: &[LinearRgba], ) -> error::Result<()> { - app_mut(|app| image::update_region(app.world_mut(), entity, x, y, width, height, pixels)) + app_mut(|app| { + let world = app.world_mut(); + let (data, px_size) = image::prepare_update_region(world, entity, width, height, pixels)?; + world + .run_system_cached_with( + image::update_region_write, + (entity, x, y, width, height, data, px_size), + ) + .unwrap() + }) } /// Destroy an existing image and free its resources. pub fn image_destroy(entity: Entity) -> error::Result<()> { - app_mut(|app| image::destroy(app.world_mut(), entity)) + app_mut(|app| { + app.world_mut() + .run_system_cached_with(image::destroy, entity) + .unwrap() + }) } diff --git a/crates/processing_render/src/surface.rs b/crates/processing_render/src/surface.rs index ae5de56..42eefb4 100644 --- a/crates/processing_render/src/surface.rs +++ b/crates/processing_render/src/surface.rs @@ -25,7 +25,7 @@ use bevy::{ app::{App, Plugin}, asset::Assets, ecs::query::QueryEntityError, - prelude::{Commands, Component, Entity, In, Query, ResMut, Window, With, World, default}, + prelude::{Commands, Component, Entity, In, Query, ResMut, Window, With, default}, render::render_resource::{Extent3d, TextureFormat}, window::{RawHandleWrapper, WindowResolution, WindowWrapper}, }; @@ -116,60 +116,48 @@ fn spawn_surface( /// * `window_handle` - A pointer to the NSWindow (from GLFW's `get_cocoa_window()`) #[cfg(target_os = "macos")] pub fn create_surface_macos( - world: &mut World, - window_handle: u64, - width: u32, - height: u32, - scale_factor: f32, + In((window_handle, width, height, scale_factor)): In<(u64, u32, u32, f32)>, + mut commands: Commands, ) -> Result { - fn create_inner( - In((window_handle, width, height, scale_factor)): In<(u64, u32, u32, f32)>, - mut commands: Commands, - ) -> Result { - use raw_window_handle::{AppKitDisplayHandle, AppKitWindowHandle}; - - // GLFW gives us NSWindow*, but AppKitWindowHandle needs NSView* - // so we have to do some objc magic to grab the right pointer - let ns_view_ptr = { - use objc2::rc::Retained; - use objc2_app_kit::{NSView, NSWindow}; - - // SAFETY: - // - window_handle is a valid NSWindow pointer from the GLFW window - let ns_window = window_handle as *mut NSWindow; - if ns_window.is_null() { - return Err(error::ProcessingError::InvalidWindowHandle); - } + use raw_window_handle::{AppKitDisplayHandle, AppKitWindowHandle}; - // SAFETY: - // - The contentView is owned by NSWindow and remains valid as long as the window exists - let ns_window_ref = unsafe { &*ns_window }; - let content_view: Option> = ns_window_ref.contentView(); + // GLFW gives us NSWindow*, but AppKitWindowHandle needs NSView* + // so we have to do some objc magic to grab the right pointer + let ns_view_ptr = { + use objc2::rc::Retained; + use objc2_app_kit::{NSView, NSWindow}; - match content_view { - Some(view) => Retained::as_ptr(&view) as *mut std::ffi::c_void, - None => { - return Err(error::ProcessingError::InvalidWindowHandle); - } + // SAFETY: + // - window_handle is a valid NSWindow pointer from the GLFW window + let ns_window = window_handle as *mut NSWindow; + if ns_window.is_null() { + return Err(error::ProcessingError::InvalidWindowHandle); + } + + // SAFETY: + // - The contentView is owned by NSWindow and remains valid as long as the window exists + let ns_window_ref = unsafe { &*ns_window }; + let content_view: Option> = ns_window_ref.contentView(); + + match content_view { + Some(view) => Retained::as_ptr(&view) as *mut std::ffi::c_void, + None => { + return Err(error::ProcessingError::InvalidWindowHandle); } - }; - - let window = AppKitWindowHandle::new(NonNull::new(ns_view_ptr).unwrap()); - let display = AppKitDisplayHandle::new(); - - spawn_surface( - &mut commands, - RawWindowHandle::AppKit(window), - RawDisplayHandle::AppKit(display), - width, - height, - scale_factor, - ) - } + } + }; - world - .run_system_cached_with(create_inner, (window_handle, width, height, scale_factor)) - .unwrap() + let window = AppKitWindowHandle::new(NonNull::new(ns_view_ptr).unwrap()); + let display = AppKitDisplayHandle::new(); + + spawn_surface( + &mut commands, + RawWindowHandle::AppKit(window), + RawDisplayHandle::AppKit(display), + width, + height, + scale_factor, + ) } /// Create a WebGPU surface from a Windows HWND handle. @@ -178,58 +166,46 @@ pub fn create_surface_macos( /// * `window_handle` - The HWND value (from GLFW's `get_win32_window()`) #[cfg(target_os = "windows")] pub fn create_surface_windows( - world: &mut World, - window_handle: u64, - width: u32, - height: u32, - scale_factor: f32, + In((window_handle, width, height, scale_factor)): In<(u64, u32, u32, f32)>, + mut commands: Commands, ) -> Result { - fn create_inner( - In((window_handle, width, height, scale_factor)): In<(u64, u32, u32, f32)>, - mut commands: Commands, - ) -> Result { - use std::num::NonZeroIsize; + use std::num::NonZeroIsize; - use raw_window_handle::{Win32WindowHandle, WindowsDisplayHandle}; - use windows::Win32::System::LibraryLoader::GetModuleHandleW; + use raw_window_handle::{Win32WindowHandle, WindowsDisplayHandle}; + use windows::Win32::System::LibraryLoader::GetModuleHandleW; - if window_handle == 0 { - return Err(error::ProcessingError::InvalidWindowHandle); - } - - // HWND is isize, so cast it - let hwnd_isize = window_handle as isize; - let hwnd_nonzero = match NonZeroIsize::new(hwnd_isize) { - Some(nz) => nz, - None => return Err(error::ProcessingError::InvalidWindowHandle), - }; - - let mut window = Win32WindowHandle::new(hwnd_nonzero); - - // VK_KHR_win32_surface requires hinstance *and* hwnd - // SAFETY: GetModuleHandleW(NULL) is safe - let hinstance = unsafe { GetModuleHandleW(None) } - .map_err(|_| error::ProcessingError::InvalidWindowHandle)?; - - let hinstance_nonzero = NonZeroIsize::new(hinstance.0 as isize) - .ok_or(error::ProcessingError::InvalidWindowHandle)?; - window.hinstance = Some(hinstance_nonzero); - - let display = WindowsDisplayHandle::new(); - - spawn_surface( - &mut commands, - RawWindowHandle::Win32(window), - RawDisplayHandle::Windows(display), - width, - height, - scale_factor, - ) + if window_handle == 0 { + return Err(error::ProcessingError::InvalidWindowHandle); } - world - .run_system_cached_with(create_inner, (window_handle, width, height, scale_factor)) - .unwrap() + // HWND is isize, so cast it + let hwnd_isize = window_handle as isize; + let hwnd_nonzero = match NonZeroIsize::new(hwnd_isize) { + Some(nz) => nz, + None => return Err(error::ProcessingError::InvalidWindowHandle), + }; + + let mut window = Win32WindowHandle::new(hwnd_nonzero); + + // VK_KHR_win32_surface requires hinstance *and* hwnd + // SAFETY: GetModuleHandleW(NULL) is safe + let hinstance = unsafe { GetModuleHandleW(None) } + .map_err(|_| error::ProcessingError::InvalidWindowHandle)?; + + let hinstance_nonzero = NonZeroIsize::new(hinstance.0 as isize) + .ok_or(error::ProcessingError::InvalidWindowHandle)?; + window.hinstance = Some(hinstance_nonzero); + + let display = WindowsDisplayHandle::new(); + + spawn_surface( + &mut commands, + RawWindowHandle::Win32(window), + RawDisplayHandle::Windows(display), + width, + height, + scale_factor, + ) } /// Create a WebGPU surface from a Wayland window and display handle. @@ -239,57 +215,35 @@ pub fn create_surface_windows( /// * `display_handle` - The wl_display pointer (from GLFW's `get_wayland_display()`) #[cfg(all(target_os = "linux", feature = "wayland"))] pub fn create_surface_wayland( - world: &mut World, - window_handle: u64, - display_handle: u64, - width: u32, - height: u32, - scale_factor: f32, + In((window_handle, display_handle, width, height, scale_factor)): In<(u64, u64, u32, u32, f32)>, + mut commands: Commands, ) -> Result { - fn create_inner( - In((window_handle, display_handle, width, height, scale_factor)): In<( - u64, - u64, - u32, - u32, - f32, - )>, - mut commands: Commands, - ) -> Result { - use raw_window_handle::{WaylandDisplayHandle, WaylandWindowHandle}; - - if window_handle == 0 { - return Err(error::ProcessingError::HandleError( - HandleError::Unavailable, - )); - } - let window_handle_ptr = NonNull::new(window_handle as *mut c_void).unwrap(); - let window = WaylandWindowHandle::new(window_handle_ptr); + use raw_window_handle::{WaylandDisplayHandle, WaylandWindowHandle}; - if display_handle == 0 { - return Err(error::ProcessingError::HandleError( - HandleError::Unavailable, - )); - } - let display_handle_ptr = NonNull::new(display_handle as *mut c_void).unwrap(); - let display = WaylandDisplayHandle::new(display_handle_ptr); - - spawn_surface( - &mut commands, - RawWindowHandle::Wayland(window), - RawDisplayHandle::Wayland(display), - width, - height, - scale_factor, - ) + if window_handle == 0 { + return Err(error::ProcessingError::HandleError( + HandleError::Unavailable, + )); } + let window_handle_ptr = NonNull::new(window_handle as *mut c_void).unwrap(); + let window = WaylandWindowHandle::new(window_handle_ptr); - world - .run_system_cached_with( - create_inner, - (window_handle, display_handle, width, height, scale_factor), - ) - .unwrap() + if display_handle == 0 { + return Err(error::ProcessingError::HandleError( + HandleError::Unavailable, + )); + } + let display_handle_ptr = NonNull::new(display_handle as *mut c_void).unwrap(); + let display = WaylandDisplayHandle::new(display_handle_ptr); + + spawn_surface( + &mut commands, + RawWindowHandle::Wayland(window), + RawDisplayHandle::Wayland(display), + width, + height, + scale_factor, + ) } /// Create a WebGPU surface from an X11 window and display handle. @@ -299,57 +253,35 @@ pub fn create_surface_wayland( /// * `display_handle` - The X11 Display pointer (from GLFW's `get_x11_display()`) #[cfg(all(target_os = "linux", feature = "x11"))] pub fn create_surface_x11( - world: &mut World, - window_handle: u64, - display_handle: u64, - width: u32, - height: u32, - scale_factor: f32, + In((window_handle, display_handle, width, height, scale_factor)): In<(u64, u64, u32, u32, f32)>, + mut commands: Commands, ) -> Result { - fn create_inner( - In((window_handle, display_handle, width, height, scale_factor)): In<( - u64, - u64, - u32, - u32, - f32, - )>, - mut commands: Commands, - ) -> Result { - use raw_window_handle::{XlibDisplayHandle, XlibWindowHandle}; - - if window_handle == 0 { - return Err(error::ProcessingError::HandleError( - HandleError::Unavailable, - )); - } - // X11 Window is a u32/u64 ID, not a pointer - let window = XlibWindowHandle::new(window_handle as std::ffi::c_ulong); + use raw_window_handle::{XlibDisplayHandle, XlibWindowHandle}; - if display_handle == 0 { - return Err(error::ProcessingError::HandleError( - HandleError::Unavailable, - )); - } - let display_ptr = NonNull::new(display_handle as *mut c_void).unwrap(); - let display = XlibDisplayHandle::new(Some(display_ptr), 0); // screen 0 - - spawn_surface( - &mut commands, - RawWindowHandle::Xlib(window), - RawDisplayHandle::Xlib(display), - width, - height, - scale_factor, - ) + if window_handle == 0 { + return Err(error::ProcessingError::HandleError( + HandleError::Unavailable, + )); } + // X11 Window is a u32/u64 ID, not a pointer + let window = XlibWindowHandle::new(window_handle as std::ffi::c_ulong); - world - .run_system_cached_with( - create_inner, - (window_handle, display_handle, width, height, scale_factor), - ) - .unwrap() + if display_handle == 0 { + return Err(error::ProcessingError::HandleError( + HandleError::Unavailable, + )); + } + let display_ptr = NonNull::new(display_handle as *mut c_void).unwrap(); + let display = XlibDisplayHandle::new(Some(display_ptr), 0); // screen 0 + + spawn_surface( + &mut commands, + RawWindowHandle::Xlib(window), + RawDisplayHandle::Xlib(display), + width, + height, + scale_factor, + ) } /// Create a WebGPU surface from a web canvas element. @@ -358,49 +290,35 @@ pub fn create_surface_x11( /// * `window_handle` - A pointer to the HtmlCanvasElement #[cfg(target_arch = "wasm32")] pub fn create_surface_web( - world: &mut World, - window_handle: u64, - width: u32, - height: u32, - scale_factor: f32, + In((window_handle, width, height, scale_factor)): In<(u64, u32, u32, f32)>, + mut commands: Commands, ) -> Result { - fn create_inner( - In((window_handle, width, height, scale_factor)): In<(u64, u32, u32, f32)>, - mut commands: Commands, - ) -> Result { - use raw_window_handle::{WebCanvasWindowHandle, WebDisplayHandle}; - - // For WASM, window_handle is a pointer to an HtmlCanvasElement - if window_handle == 0 { - return Err(error::ProcessingError::InvalidWindowHandle); - } - let canvas_ptr = NonNull::new(window_handle as *mut c_void).unwrap(); - let window = WebCanvasWindowHandle::new(canvas_ptr.cast()); - let display = WebDisplayHandle::new(); - - spawn_surface( - &mut commands, - RawWindowHandle::WebCanvas(window), - RawDisplayHandle::Web(display), - width, - height, - scale_factor, - ) - } + use raw_window_handle::{WebCanvasWindowHandle, WebDisplayHandle}; - world - .run_system_cached_with(create_inner, (window_handle, width, height, scale_factor)) - .unwrap() + // For WASM, window_handle is a pointer to an HtmlCanvasElement + if window_handle == 0 { + return Err(error::ProcessingError::InvalidWindowHandle); + } + let canvas_ptr = NonNull::new(window_handle as *mut c_void).unwrap(); + let window = WebCanvasWindowHandle::new(canvas_ptr.cast()); + let display = WebDisplayHandle::new(); + + spawn_surface( + &mut commands, + RawWindowHandle::WebCanvas(window), + RawDisplayHandle::Web(display), + width, + height, + scale_factor, + ) } -pub fn create_offscreen( - world: &mut World, +pub fn prepare_offscreen( width: u32, height: u32, scale_factor: f32, texture_format: TextureFormat, -) -> Result { - // we just wrap image create here +) -> Result<(Extent3d, Vec, TextureFormat)> { let size = Extent3d { width: (width as f32 * scale_factor) as u32, height: (height as f32 * scale_factor) as u32, @@ -417,55 +335,41 @@ pub fn create_offscreen( }; let data = vec![0u8; (size.width * size.height * pixel_size) as usize]; - let image = crate::image::create(world, size, data, texture_format); - world.entity_mut(image).insert(Surface); - Ok(image) + Ok((size, data, texture_format)) } -pub fn destroy(world: &mut World, window_entity: Entity) -> Result<()> { - fn destroy_inner( - In(surface_entity): In, - mut commands: Commands, - p_images: Query<&Image, With>, - mut images: ResMut>, - mut p_image_textures: ResMut, - ) -> Result<()> { - match p_images.get(surface_entity) { - Ok(p_image) => { - images.remove(&p_image.handle); - p_image_textures.remove(&surface_entity); - commands.entity(surface_entity).despawn(); - Ok(()) - } - Err(QueryEntityError::QueryDoesNotMatch(..)) => { - commands.entity(surface_entity).despawn(); - Ok(()) - } - Err(_) => Err(ProcessingError::SurfaceNotFound), +pub fn destroy( + In(surface_entity): In, + mut commands: Commands, + p_images: Query<&Image, With>, + mut images: ResMut>, + mut p_image_textures: ResMut, +) -> Result<()> { + match p_images.get(surface_entity) { + Ok(p_image) => { + images.remove(&p_image.handle); + p_image_textures.remove(&surface_entity); + commands.entity(surface_entity).despawn(); + Ok(()) } + Err(QueryEntityError::QueryDoesNotMatch(..)) => { + commands.entity(surface_entity).despawn(); + Ok(()) + } + Err(_) => Err(ProcessingError::SurfaceNotFound), } - - world - .run_system_cached_with(destroy_inner, window_entity) - .unwrap() } /// Update window size when resized. -pub fn resize(world: &mut World, window_entity: Entity, width: u32, height: u32) -> Result<()> { - fn resize_inner( - In((window_entity, width, height)): In<(Entity, u32, u32)>, - mut windows: Query<&mut Window>, - ) -> Result<()> { - if let Ok(mut window) = windows.get_mut(window_entity) { - window.resolution.set_physical_resolution(width, height); - - Ok(()) - } else { - Err(error::ProcessingError::SurfaceNotFound) - } +pub fn resize( + In((window_entity, width, height)): In<(Entity, u32, u32)>, + mut windows: Query<&mut Window>, +) -> Result<()> { + if let Ok(mut window) = windows.get_mut(window_entity) { + window.resolution.set_physical_resolution(width, height); + + Ok(()) + } else { + Err(error::ProcessingError::SurfaceNotFound) } - - world - .run_system_cached_with(resize_inner, (window_entity, width, height)) - .unwrap() } diff --git a/docs/principles.md b/docs/principles.md index 654bb96..8f41b4a 100644 --- a/docs/principles.md +++ b/docs/principles.md @@ -118,18 +118,32 @@ more frequently are imperatively modifying the world rather than adding "normal" There are several strategies that can help work around this: -1. You can call systems on `World` that accept and return data! This looks like the following: +1. You can call systems on `World` that accept and return data using `In` parameters: ```rust -fn my_system(In(arg_a, arg_b): In<(u32, u32)>, mut my_res: ResMut, a_cool_query: Query<&Foo, &Bar>) -> u32 { - return 1234; +// In a object module define plain systems with `In` params: +pub fn create( + In((width, height, surface_entity)): In<(u32, u32, Entity)>, + mut commands: Commands, + render_device: Res, +) -> Result { + // implementation that uses Commands, queries, resources, etc. + Ok(entity) } -// ... later -world -.run_system_cached_with(1, 2) ?; +// In lib, call the system via run_system_cached_with: +pub fn graphics_create(surface_entity: Entity, width: u32, height: u32) -> error::Result { + app_mut(|app| { + app.world_mut() + .run_system_cached_with(graphics::create, (width, height, surface_entity)) + .unwrap() + }) +} ``` +The `In` parameter receives input data passed via `run_system_cached_with()`. For multiple parameters, use tuples: +`In<(T, U, V)>`. The `In` parameter must always be the first system parameter. + 2. Collect results from queries into intermediate collections. This can resolve the borrow for a query at the cost of a bit of inefficiency. 3. Use `world.resource_scope` or otherwise temporarily remove certain resources from the world (making sure to add them From efa010149c9ac64bde1c1f97614f378205f0d5a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sun, 21 Dec 2025 19:46:24 -0800 Subject: [PATCH 2/3] Add more docs. --- docs/principles.md | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/principles.md b/docs/principles.md index 8f41b4a..b067c46 100644 --- a/docs/principles.md +++ b/docs/principles.md @@ -93,9 +93,39 @@ destroyed. ### Bridging Bevy and immediate-mode APIs -By default, Bevy is highly optimized for throughput and includes automatic parallelization, pipelined rendering, and -sophisticated rendering batching algorithms. This poses a problem for Processing which wants to present an -immediate-mode API where the user can flush their current draw state to a given surface at any time. +The central architectural challenge of libprocessing is reconciling two fundamentally different models of how graphics +programming works. + +Immediate mode treats drawing as a sequence of imperative commands: when you call `rect(10, 10, 50, 50)` a rectangle +appears *now*. State is global and mutable where the user thinks in terms of a linear script that paints pixels onto a +canvas. This is the traditional "sketch" model of Processing. + +Retained mode in the case of Bevy's ECS treats the scene as a database of entities with components. Systems query and +transform this data, often in parallel. Rendering is a separate phase that happens later, potentially pipelined across +frames. The renderer batches draw calls for efficiency and has a number of optimizations that could be considered a form +of eventual consistency (think of a game where objects take flicker in and out on screen as assets load). The user +thinks in terms of a scene graph that is updated over time, where multiple asynchronous systems are modifying data. + +Neither model is wrong! But they very much optimize for different things. Immediate mode is intuitive and exploratory +which is why it's so well suited to learning, prototyping, and creative coding. Retained mode is efficient and scalable, +perfect for games with thousands or hundreds of thousands of objects or for more complex artworks that require +sophisticated rendering techniques. + +Our job is to present the former while implementing it atop the latter. + +This requires us to invert several of Bevy's defaults: + +- Recording instead of executing: When user code calls a draw function, we don't spawn entities immediately. + Instead, we record the intent as a `DrawCommand` in a per-graphics `CommandBuffer`. This preserves call order and + allows us to process commands in a controlled batch. +- Synchronous frame control: Bevy wants to manage its own main loop with pipelined rendering. We instead hold the + `App` in a thread-local and call `app.update()` only when the user explicitly flushes, i.e. makes a change that + requires rendering to occur in occur because of some data dependency. +- Selective rendering: By default, Bevy will render all active cameras every update. We disable cameras unless the + user has requested a flush, using marker components to signal which surfaces should actually render. +- Transient geometry: In immediate mode, shapes exist only for the frame they're drawn. We spawn mesh entities when + flushing commands and despawn them before the next frame. The ECS becomes a staging area rather than a persistent + scene graph. We work around this in the following manner: From 26018bd08bea52c7d44801e82f77a012d72d4bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Sun, 21 Dec 2025 22:08:06 -0800 Subject: [PATCH 3/3] Fmt. --- crates/processing_render/src/graphics.rs | 24 ++++++++++++++---------- crates/processing_render/src/image.rs | 17 +++++++++++++---- crates/processing_render/src/lib.rs | 19 ++++++++++++++++--- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index 1727e47..5afea47 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -358,8 +358,7 @@ pub fn readback( let texture = view_target.main_texture(); - let mut encoder = - render_device.create_command_encoder(&CommandEncoderDescriptor::default()); + let mut encoder = render_device.create_command_encoder(&CommandEncoderDescriptor::default()); let px_size = pixel_size(graphics.texture_format)?; let padded_bytes_per_row = @@ -413,7 +412,15 @@ pub fn readback( } pub fn update_region_write( - In((entity, x, y, width, height, data, px_size)): In<(Entity, u32, u32, u32, u32, Vec, u32)>, + In((entity, x, y, width, height, data, px_size)): In<( + Entity, + u32, + u32, + u32, + u32, + Vec, + u32, + )>, graphics_query: Query<&Graphics>, graphics_targets: Res, render_queue: Res, @@ -437,11 +444,7 @@ pub fn update_region_write( let texture = view_target.main_texture(); eprintln!( "update_region: writing to texture {:p} at ({}, {}) size {}x{}", - texture as *const _, - x, - y, - width, - height + texture as *const _, x, y, width, height ); let bytes_per_row = width * px_size; @@ -479,7 +482,9 @@ pub fn prepare_update_region( if pixels.len() != expected_count { return Err(ProcessingError::InvalidArgument(format!( "Expected {} pixels for {}x{} region, got {}", - expected_count, width, height, + expected_count, + width, + height, pixels.len() ))); } @@ -493,7 +498,6 @@ pub fn prepare_update_region( Ok((data, px_size)) } - #[derive(Resource, Debug, Clone, Reflect)] pub struct RenderLayersManager { used: RenderLayers, diff --git a/crates/processing_render/src/image.rs b/crates/processing_render/src/image.rs index 4d492a6..b038087 100644 --- a/crates/processing_render/src/image.rs +++ b/crates/processing_render/src/image.rs @@ -209,8 +209,7 @@ pub fn readback( .get(&entity) .ok_or(ProcessingError::ImageNotFound)?; - let mut encoder = - render_device.create_command_encoder(&CommandEncoderDescriptor::default()); + let mut encoder = render_device.create_command_encoder(&CommandEncoderDescriptor::default()); let px_size = pixel_size(p_image.texture_format)?; let padded_bytes_per_row = @@ -269,7 +268,15 @@ pub fn readback( } pub fn update_region_write( - In((entity, x, y, width, height, data, px_size)): In<(Entity, u32, u32, u32, u32, Vec, u32)>, + In((entity, x, y, width, height, data, px_size)): In<( + Entity, + u32, + u32, + u32, + u32, + Vec, + u32, + )>, p_images: Query<&Image>, p_image_textures: Res, render_queue: Res, @@ -325,7 +332,9 @@ pub fn prepare_update_region( if pixels.len() != expected_count { return Err(ProcessingError::InvalidArgument(format!( "Expected {} pixels for {}x{} region, got {}", - expected_count, width, height, + expected_count, + width, + height, pixels.len() ))); } diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 00d3efb..8db84a3 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -358,12 +358,25 @@ pub fn graphics_update(graphics_entity: Entity, pixels: &[LinearRgba]) -> error: .get::(graphics_entity) .ok_or(error::ProcessingError::GraphicsNotFound)? .size; - let (data, px_size) = - graphics::prepare_update_region(world, graphics_entity, size.width, size.height, pixels)?; + let (data, px_size) = graphics::prepare_update_region( + world, + graphics_entity, + size.width, + size.height, + pixels, + )?; world .run_system_cached_with( graphics::update_region_write, - (graphics_entity, 0, 0, size.width, size.height, data, px_size), + ( + graphics_entity, + 0, + 0, + size.width, + size.height, + data, + px_size, + ), ) .unwrap() })