diff --git a/Cargo.lock b/Cargo.lock index 713cbb4..d82fc89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3992,6 +3992,8 @@ name = "processing_render" version = "0.1.0" dependencies = [ "bevy", + "crossbeam-channel", + "half", "lyon", "objc2 0.6.3", "objc2-app-kit 0.3.2", diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000..cf0b2e0 Binary files /dev/null and b/assets/images/logo.png differ diff --git a/crates/processing_ffi/src/color.rs b/crates/processing_ffi/src/color.rs index fc063be..fea6259 100644 --- a/crates/processing_ffi/src/color.rs +++ b/crates/processing_ffi/src/color.rs @@ -1,3 +1,5 @@ +use bevy::color::LinearRgba; + /// A sRGB (?) color #[repr(C)] #[derive(Debug, Clone, Copy)] @@ -13,3 +15,10 @@ impl From for bevy::color::Color { bevy::color::Color::srgba(color.r, color.g, color.b, color.a) } } + +impl From for Color { + fn from(lin: LinearRgba) -> Self { + let srgb = bevy::color::Color::srgba(lin.red, lin.green, lin.blue, lin.alpha); + srgb.into() + } +} diff --git a/crates/processing_ffi/src/error.rs b/crates/processing_ffi/src/error.rs index f02621c..fe8d5ed 100644 --- a/crates/processing_ffi/src/error.rs +++ b/crates/processing_ffi/src/error.rs @@ -4,7 +4,7 @@ use std::{ panic, }; -use processing::prelude::error::ProcessingError; +pub(crate) use processing::prelude::error::ProcessingError; thread_local! { static LAST_ERROR: RefCell> = const { RefCell::new(None) }; diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index 302b96d..6dd1cdf 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -1,4 +1,7 @@ -use bevy::prelude::Entity; +use bevy::{ + prelude::Entity, + render::render_resource::{Extent3d, TextureFormat}, +}; use processing::prelude::*; use crate::color::Color; @@ -26,7 +29,7 @@ pub extern "C" fn processing_init() { /// - window_handle is a valid GLFW window pointer. /// - This is called from the same thread as init. #[unsafe(no_mangle)] -pub extern "C" fn processing_create_surface( +pub extern "C" fn processing_surface_create( window_handle: u64, display_handle: u64, width: u32, @@ -34,7 +37,7 @@ pub extern "C" fn processing_create_surface( scale_factor: f32, ) -> u64 { error::clear_error(); - error::check(|| create_surface(window_handle, display_handle, width, height, scale_factor)) + error::check(|| surface_create(window_handle, display_handle, width, height, scale_factor)) .map(|e| e.to_bits()) .unwrap_or(0) } @@ -42,27 +45,27 @@ pub extern "C" fn processing_create_surface( /// Destroy the surface associated with the given window ID. /// /// SAFETY: -/// - Init and create_surface have been called. -/// - window_id is a valid ID returned from create_surface. +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. /// - This is called from the same thread as init. #[unsafe(no_mangle)] -pub extern "C" fn processing_destroy_surface(window_id: u64) { +pub extern "C" fn processing_surface_destroy(window_id: u64) { error::clear_error(); let window_entity = Entity::from_bits(window_id); - error::check(|| destroy_surface(window_entity)); + error::check(|| surface_destroy(window_entity)); } /// Update window size when resized. /// /// SAFETY: -/// - Init and create_surface have been called. -/// - window_id is a valid ID returned from create_surface. +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. /// - This is called from the same thread as init. #[unsafe(no_mangle)] -pub extern "C" fn processing_resize_surface(window_id: u64, width: u32, height: u32) { +pub extern "C" fn processing_surface_resize(window_id: u64, width: u32, height: u32) { error::clear_error(); let window_entity = Entity::from_bits(window_id); - error::check(|| resize_surface(window_entity, width, height)); + error::check(|| surface_resize(window_entity, width, height)); } /// Set the background color for the given window. @@ -73,7 +76,21 @@ pub extern "C" fn processing_resize_surface(window_id: u64, width: u32, height: pub extern "C" fn processing_background_color(window_id: u64, color: Color) { error::clear_error(); let window_entity = Entity::from_bits(window_id); - error::check(|| background_color(window_entity, color.into())); + error::check(|| record_command(window_entity, DrawCommand::BackgroundColor(color.into()))); +} + +/// Set the background image for the given window. +/// +/// SAFETY: +/// - This is called from the same thread as init. +/// - image_id is a valid ID returned from processing_image_create. +/// - The image has been fully uploaded. +#[unsafe(no_mangle)] +pub extern "C" fn processing_background_image(window_id: u64, image_id: u64) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + let image_entity = Entity::from_bits(image_id); + error::check(|| record_command(window_entity, DrawCommand::BackgroundImage(image_entity))); } /// Begins the draw for the given window. @@ -126,8 +143,8 @@ pub extern "C" fn processing_exit(exit_code: u8) { /// Set the fill color. /// /// SAFETY: -/// - Init and create_surface have been called. -/// - window_id is a valid ID returned from create_surface. +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. /// - This is called from the same thread as init. #[unsafe(no_mangle)] pub extern "C" fn processing_set_fill(window_id: u64, r: f32, g: f32, b: f32, a: f32) { @@ -140,8 +157,8 @@ pub extern "C" fn processing_set_fill(window_id: u64, r: f32, g: f32, b: f32, a: /// Set the stroke color. /// /// SAFETY: -/// - Init and create_surface have been called. -/// - window_id is a valid ID returned from create_surface. +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. /// - This is called from the same thread as init. #[unsafe(no_mangle)] pub extern "C" fn processing_set_stroke_color(window_id: u64, r: f32, g: f32, b: f32, a: f32) { @@ -154,8 +171,8 @@ pub extern "C" fn processing_set_stroke_color(window_id: u64, r: f32, g: f32, b: /// Set the stroke weight. /// /// SAFETY: -/// - Init and create_surface have been called. -/// - window_id is a valid ID returned from create_surface. +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. /// - This is called from the same thread as init. #[unsafe(no_mangle)] pub extern "C" fn processing_set_stroke_weight(window_id: u64, weight: f32) { @@ -167,8 +184,8 @@ pub extern "C" fn processing_set_stroke_weight(window_id: u64, weight: f32) { /// Disable fill for subsequent shapes. /// /// SAFETY: -/// - Init and create_surface have been called. -/// - window_id is a valid ID returned from create_surface. +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. /// - This is called from the same thread as init. #[unsafe(no_mangle)] pub extern "C" fn processing_no_fill(window_id: u64) { @@ -180,8 +197,8 @@ pub extern "C" fn processing_no_fill(window_id: u64) { /// Disable stroke for subsequent shapes. /// /// SAFETY: -/// - Init and create_surface have been called. -/// - window_id is a valid ID returned from create_surface. +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. /// - This is called from the same thread as init. #[unsafe(no_mangle)] pub extern "C" fn processing_no_stroke(window_id: u64) { @@ -193,8 +210,8 @@ pub extern "C" fn processing_no_stroke(window_id: u64) { /// Draw a rectangle. /// /// SAFETY: -/// - Init and create_surface have been called. -/// - window_id is a valid ID returned from create_surface. +/// - Init and surface_create have been called. +/// - window_id is a valid ID returned from surface_create. /// - This is called from the same thread as init. #[unsafe(no_mangle)] pub extern "C" fn processing_rect( @@ -223,3 +240,113 @@ pub extern "C" fn processing_rect( ) }); } + +/// Create an image from raw pixel data. +/// +/// SAFETY: +/// - Init has been called. +/// - data is a valid pointer to data_len bytes of RGBA pixel data. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_image_create( + width: u32, + height: u32, + data: *const u8, + data_len: usize, +) -> u64 { + error::clear_error(); + // SAFETY: Caller must ensure that `data` is valid for `data_len` bytes. + let data = unsafe { std::slice::from_raw_parts(data, data_len) }; + error::check(|| { + let size = Extent3d { + width, + height, + depth_or_array_layers: 1, + }; + image_create(size, data.to_vec(), TextureFormat::Rgba8UnormSrgb) + }) + .map(|entity| entity.to_bits()) + .unwrap_or(0) +} + +/// Load an image from a file path. +/// +/// SAFETY: +/// - Init has been called. +/// - path is a valid null-terminated C string. +/// - This is called from the same thread as init. +/// +/// Note: This function is currently synchronous but Bevy's asset loading is async. +/// The image may not be immediately available. This needs to be improved. +#[unsafe(no_mangle)] +pub extern "C" fn processing_image_load(path: *const std::ffi::c_char) -> u64 { + error::clear_error(); + + // SAFETY: Caller guarantees path is a valid C string + let c_str = unsafe { std::ffi::CStr::from_ptr(path) }; + let path_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + error::set_error("Invalid UTF-8 in image path"); + return 0; + } + }; + + error::check(|| image_load(path_str)) + .map(|entity| entity.to_bits()) + .unwrap_or(0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn processing_image_resize(image_id: u64, new_width: u32, new_height: u32) { + error::clear_error(); + let image_entity = Entity::from_bits(image_id); + let new_size = Extent3d { + width: new_width, + height: new_height, + depth_or_array_layers: 1, + }; + error::check(|| image_resize(image_entity, new_size)); +} + +/// Load pixels from an image into a caller-provided buffer. +/// +/// SAFETY: +/// - Init and image_create have been called. +/// - image_id is a valid ID returned from image_create. +/// - buffer is a valid pointer to at least buffer_len Color elements. +/// - buffer_len must equal width * height of the image. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_image_load_pixels( + image_id: u64, + buffer: *mut Color, + buffer_len: usize, +) { + error::clear_error(); + let image_entity = Entity::from_bits(image_id); + error::check(|| { + let colors = image_load_pixels(image_entity)?; + + // Validate buffer size + if colors.len() != buffer_len { + let error_msg = format!( + "Buffer size mismatch: expected {}, got {}", + colors.len(), + buffer_len + ); + error::set_error(&error_msg); + return Err(error::ProcessingError::InvalidArgument(error_msg)); + } + + // SAFETY: Caller guarantees buffer is valid for buffer_len elements + unsafe { + let buffer_slice = std::slice::from_raw_parts_mut(buffer, buffer_len); + for (i, color) in colors.iter().enumerate() { + buffer_slice[i] = Color::from(*color); + } + } + + Ok(()) + }); +} diff --git a/crates/processing_render/Cargo.toml b/crates/processing_render/Cargo.toml index 581e114..14727ee 100644 --- a/crates/processing_render/Cargo.toml +++ b/crates/processing_render/Cargo.toml @@ -13,6 +13,8 @@ raw-window-handle = "0.6" thiserror = "2" tracing = "0.1" tracing-subscriber = "0.3" +half = "2.7" +crossbeam-channel = "0.5" [target.'cfg(target_os = "macos")'.dependencies] objc2 = { version = "0.6", default-features = false } diff --git a/crates/processing_render/src/error.rs b/crates/processing_render/src/error.rs index 9eedd9d..fa39266 100644 --- a/crates/processing_render/src/error.rs +++ b/crates/processing_render/src/error.rs @@ -14,4 +14,10 @@ pub enum ProcessingError { HandleError(#[from] raw_window_handle::HandleError), #[error("Invalid window handle provided")] InvalidWindowHandle, + #[error("Image not found")] + ImageNotFound, + #[error("Unsupported texture format")] + UnsupportedTextureFormat, + #[error("Invalid argument: {0}")] + InvalidArgument(String), } diff --git a/crates/processing_render/src/image.rs b/crates/processing_render/src/image.rs new file mode 100644 index 0000000..1d6b25b --- /dev/null +++ b/crates/processing_render/src/image.rs @@ -0,0 +1,311 @@ +use std::path::PathBuf; + +use bevy::{ + asset::{ + LoadState, RenderAssetUsages, handle_internal_asset_events, io::embedded::GetAssetServer, + }, + ecs::{entity::EntityHashMap, system::RunSystemOnce}, + prelude::*, + render::{ + ExtractSchedule, MainWorld, + render_asset::{AssetExtractionSystems, RenderAssets}, + render_resource::{ + Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, MapMode, + PollType, TexelCopyBufferInfo, TexelCopyBufferLayout, Texture, TextureDimension, + TextureFormat, + }, + renderer::{RenderDevice, RenderQueue}, + texture::GpuImage, + }, +}; +use half::f16; + +use crate::error::{ProcessingError, Result}; + +pub struct PImagePlugin; + +impl Plugin for PImagePlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + + let render_app = app.sub_app_mut(bevy::render::RenderApp); + render_app.add_systems(ExtractSchedule, sync_textures.after(AssetExtractionSystems)); + } +} + +#[derive(Resource, Deref, DerefMut, Default)] +struct PImageTextures(EntityHashMap); + +#[derive(Component)] +pub struct PImage { + pub handle: Handle, + readback_buffer: Buffer, + pixel_size: usize, + texture_format: TextureFormat, + size: Extent3d, +} + +fn sync_textures(mut main_world: ResMut, gpu_images: Res>) { + main_world.resource_scope(|world, mut p_image_textures: Mut| { + let mut p_images = world.query_filtered::<(Entity, &PImage), Changed>(); + for (entity, p_image) in p_images.iter(world) { + if let Some(gpu_image) = gpu_images.get(&p_image.handle) { + p_image_textures.insert(entity, gpu_image.texture.clone()); + } + } + }); +} + +pub fn create( + world: &mut World, + size: Extent3d, + data: Vec, + texture_format: TextureFormat, +) -> Entity { + fn new_inner( + In((size, data, texture_format)): In<(Extent3d, Vec, TextureFormat)>, + mut commands: Commands, + mut images: ResMut>, + render_device: Res, + ) -> Entity { + let image = Image::new( + size, + TextureDimension::D2, + data, + texture_format, + RenderAssetUsages::all(), + ); + + let handle = images.add(image); + + let pixel_size = match texture_format { + TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb => 4usize, + TextureFormat::Rgba16Float => 8, + TextureFormat::Rgba32Float => 16, + _ => panic!("Unsupported texture format for readback"), + }; + let readback_buffer_size = size.width * size.height * pixel_size as u32; + let readback_buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("PImage Readback Buffer"), + size: readback_buffer_size as u64, + usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + commands + .spawn(PImage { + handle: handle.clone(), + readback_buffer, + pixel_size, + texture_format, + size, + }) + .id() + } + + world + .run_system_cached_with(new_inner, (size, data, texture_format)) + .expect("Failed to run new PImage system") +} + +pub fn load(world: &mut World, path: PathBuf) -> Result { + fn load_inner(In(path): In, world: &mut World) -> Result { + let 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) + .expect("Failed to run internal asset events 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 pixel_size = match texture_format { + TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb => 4usize, + TextureFormat::Rgba16Float => 8, + TextureFormat::Rgba32Float => 16, + _ => panic!("Unsupported texture format for readback"), + }; + let readback_buffer_size = size.width * size.height * pixel_size as u32; + + let render_device = world.resource::(); + let readback_buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("PImage Readback Buffer"), + size: readback_buffer_size as u64, + usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + Ok(world + .spawn(PImage { + handle: handle.clone(), + readback_buffer, + pixel_size, + texture_format, + size, + }) + .id()) + } + + world + .run_system_cached_with(load_inner, path.to_path_buf()) + .expect("Failed to run load system") +} + +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 PImage>, + mut images: ResMut>, + render_device: Res, + ) -> Result<()> { + let mut image = p_images + .get_mut(entity) + .map_err(|_| ProcessingError::ImageNotFound)?; + + images + .get_mut(&image.handle) + .ok_or(ProcessingError::ImageNotFound)? + .resize_in_place(new_size); + + let size = new_size.width as u64 * new_size.height as u64 * image.pixel_size as u64; + image.readback_buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("PImage Readback Buffer"), + size, + usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + + Ok(()) + } + + world + .run_system_cached_with(resize_inner, (entity, new_size)) + .expect("Failed to run resize system") +} + +pub fn load_pixels(world: &mut World, entity: Entity) -> Result> { + fn readback_inner( + In(entity): In, + p_images: Query<&PImage>, + 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 block_dimensions = p_image.texture_format.block_dimensions(); + let block_size = p_image.texture_format.block_copy_size(None).unwrap(); + + let padded_bytes_per_row = RenderDevice::align_copy_bytes_per_row( + (p_image.size.width as usize / block_dimensions.0 as usize) * block_size as usize, + ); + + 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, + ); + + render_queue.submit(std::iter::once(encoder.finish())); + + let buffer_slice = p_image.readback_buffer.slice(..); + + 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}"), + }); + + render_device + .poll(PollType::Wait) + .expect("Failed to poll device for map async"); + + r.recv().expect("Failed to receive the map_async message"); + + 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()); + + p_image.readback_buffer.unmap(); + + let data = match p_image.texture_format { + TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb => data + .chunks_exact(p_image.pixel_size) + .map(|chunk| LinearRgba::from_u8_array([chunk[0], chunk[1], chunk[2], chunk[3]])) + .collect(), + TextureFormat::Rgba16Float => data + .chunks_exact(p_image.pixel_size) + .map(|chunk| { + let r = f16::from_bits(u16::from_le_bytes([chunk[0], chunk[1]])).to_f32(); + let g = f16::from_bits(u16::from_le_bytes([chunk[2], chunk[3]])).to_f32(); + let b = f16::from_bits(u16::from_le_bytes([chunk[4], chunk[5]])).to_f32(); + let a = f16::from_bits(u16::from_le_bytes([chunk[6], chunk[7]])).to_f32(); + LinearRgba::from_f32_array([r, g, b, a]) + }) + .collect(), + TextureFormat::Rgba32Float => data + .chunks_exact(p_image.pixel_size) + .map(|chunk| { + let r = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + let g = f32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]); + let b = f32::from_le_bytes([chunk[8], chunk[9], chunk[10], chunk[11]]); + let a = f32::from_le_bytes([chunk[12], chunk[13], chunk[14], chunk[15]]); + LinearRgba::from_f32_array([r, g, b, a]) + }) + .collect(), + _ => return Err(ProcessingError::UnsupportedTextureFormat), + }; + + Ok(data) + } + + world + .run_system_cached_with(readback_inner, entity) + .expect("Failed to run readback system") +} + +pub fn destroy(world: &mut World, entity: Entity) -> Result<()> { + fn destroy_inner( + In(entity): In, + mut p_images: Query<&mut PImage>, + 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); + + Ok(()) + } + + world + .run_system_cached_with(destroy_inner, entity) + .expect("Failed to run destroy system") +} diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index a516b4c..b28c1ea 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -1,7 +1,8 @@ pub mod error; +pub mod image; pub mod render; -use std::{cell::RefCell, ffi::c_void, num::NonZero, ptr::NonNull, sync::OnceLock}; +use std::{cell::RefCell, num::NonZero, path::PathBuf, ptr::NonNull, sync::OnceLock}; use bevy::{ app::{App, AppExit}, @@ -10,6 +11,7 @@ use bevy::{ log::tracing_subscriber, math::Vec3A, prelude::*, + render::render_resource::{Extent3d, TextureFormat}, window::{RawHandleWrapper, Window, WindowRef, WindowResolution, WindowWrapper}, }; use raw_window_handle::{ @@ -36,6 +38,9 @@ struct WindowCount(u32); #[derive(Component)] pub struct Flush; +#[derive(Component)] +pub struct SurfaceSize(u32, u32); + /// Custom orthographic projection for Processing's coordinate system. /// Origin at top-left, Y-axis down, in pixel units (aka screen space). #[derive(Debug, Clone, Reflect)] @@ -154,7 +159,7 @@ impl HasDisplayHandle for GlfwWindow { /// Currently, this just creates a bevy window with the given parameters and /// stores the raw window handle for later use by the renderer, which will /// actually create the surface. -pub fn create_surface( +pub fn surface_create( window_handle: u64, display_handle: u64, width: u32, @@ -287,6 +292,7 @@ pub fn create_surface( // this doesn't do anything but makes it easier to fetch the render layer for // meshes to be drawn to this window render_layer.clone(), + SurfaceSize(width, height), )); let window_entity = window.id(); @@ -318,7 +324,7 @@ pub fn create_surface( Ok(entity_id) } -pub fn destroy_surface(window_entity: Entity) -> Result<()> { +pub fn surface_destroy(window_entity: Entity) -> Result<()> { app_mut(|app| { if app.world_mut().get::(window_entity).is_some() { app.world_mut().despawn(window_entity); @@ -330,14 +336,17 @@ pub fn destroy_surface(window_entity: Entity) -> Result<()> { } /// Update window size when resized. -pub fn resize_surface(window_entity: Entity, width: u32, height: u32) -> Result<()> { +pub fn surface_resize(window_entity: Entity, width: u32, height: u32) -> Result<()> { app_mut(|app| { if let Some(mut window) = app.world_mut().get_mut::(window_entity) { window.resolution.set_physical_resolution(width, height); - Ok(()) } else { - Err(error::ProcessingError::WindowNotFound) - } + return Err(error::ProcessingError::WindowNotFound); + }; + app.world_mut() + .entity_mut(window_entity) + .insert(SurfaceSize(width, height)); + Ok(()) }) } @@ -503,3 +512,33 @@ pub fn record_command(window_entity: Entity, cmd: DrawCommand) -> Result<()> { Ok(()) }) } + +/// Create a new image with given size and data. +pub fn image_create( + size: Extent3d, + data: Vec, + texture_format: TextureFormat, +) -> Result { + app_mut(|app| Ok(image::create(app.world_mut(), size, data, texture_format))) +} + +/// Load an image from disk. +pub fn image_load(path: &str) -> Result { + let path = PathBuf::from(path); + app_mut(|app| image::load(app.world_mut(), path)) +} + +/// Resize an existing image to new size. +pub fn image_resize(entity: Entity, new_size: Extent3d) -> Result<()> { + app_mut(|app| image::resize(app.world_mut(), entity, new_size)) +} + +/// Read back image data from GPU to CPU. +pub fn image_load_pixels(entity: Entity) -> Result> { + app_mut(|app| image::load_pixels(app.world_mut(), entity)) +} + +/// Destroy an existing image and free its resources. +pub fn image_destroy(entity: Entity) -> Result<()> { + app_mut(|app| image::destroy(app.world_mut(), entity)) +} diff --git a/crates/processing_render/src/render/command.rs b/crates/processing_render/src/render/command.rs index 8965aae..c7ee2b2 100644 --- a/crates/processing_render/src/render/command.rs +++ b/crates/processing_render/src/render/command.rs @@ -2,6 +2,8 @@ use bevy::prelude::*; #[derive(Debug, Clone)] pub enum DrawCommand { + BackgroundColor(Color), + BackgroundImage(Entity), Fill(Color), NoFill, StrokeColor(Color), diff --git a/crates/processing_render/src/render/material.rs b/crates/processing_render/src/render/material.rs index 9529e8e..fc2030c 100644 --- a/crates/processing_render/src/render/material.rs +++ b/crates/processing_render/src/render/material.rs @@ -3,6 +3,7 @@ use bevy::{prelude::*, render::alpha::AlphaMode}; #[derive(Clone, PartialEq, Eq, Hash, Debug)] pub struct MaterialKey { pub transparent: bool, + pub background_image: Option>, } impl MaterialKey { @@ -11,6 +12,7 @@ impl MaterialKey { base_color: Color::WHITE, unlit: true, cull_mode: None, + base_color_texture: self.background_image.clone(), alpha_mode: if self.transparent { AlphaMode::Blend } else { diff --git a/crates/processing_render/src/render/mesh_builder.rs b/crates/processing_render/src/render/mesh_builder.rs index ce3a5ae..84665e3 100644 --- a/crates/processing_render/src/render/mesh_builder.rs +++ b/crates/processing_render/src/render/mesh_builder.rs @@ -45,6 +45,12 @@ impl<'a> MeshBuilder<'a> { normals.push([0.0, 0.0, 1.0]); // flat normal for 2d } + if let Some(VertexAttributeValues::Float32x2(uvs)) = + self.mesh.attribute_mut(Mesh::ATTRIBUTE_UV_0) + { + uvs.push([0.0, 0.0]); + } + id } diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index 397843c..a5689d6 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -1,17 +1,22 @@ pub mod command; pub mod material; pub mod mesh_builder; -mod primitive; +pub mod primitive; use bevy::{camera::visibility::RenderLayers, ecs::system::SystemParam, prelude::*}; use command::{CommandBuffer, DrawCommand}; use material::MaterialKey; use primitive::{TessellationMode, empty_mesh}; -use crate::{Flush, render::primitive::rect}; +use crate::{Flush, SurfaceSize, image::PImage, render::primitive::rect}; #[derive(Component)] -pub struct TransientMesh; +#[relationship(relationship_target = TransientMeshes)] +pub struct BelongsToSurface(pub Entity); + +#[derive(Component, Default)] +#[relationship_target(relationship = BelongsToSurface)] +pub struct TransientMeshes(Vec); #[derive(SystemParam)] pub struct RenderContext<'w, 's> { @@ -73,9 +78,12 @@ impl RenderState { pub fn flush_draw_commands( mut ctx: RenderContext, - mut query: Query<(Entity, &mut CommandBuffer, &RenderLayers), With>, + mut surfaces: Query<(Entity, &mut CommandBuffer, &RenderLayers, &SurfaceSize), With>, + p_images: Query<&PImage>, ) { - for (surface_entity, mut cmd_buffer, render_layers) in query.iter_mut() { + for (surface_entity, mut cmd_buffer, render_layers, SurfaceSize(width, height)) in + surfaces.iter_mut() + { let draw_commands = std::mem::take(&mut cmd_buffer.commands); ctx.batch.render_layers = render_layers.clone(); ctx.batch.surface_entity = Some(surface_entity); @@ -116,6 +124,54 @@ pub fn flush_draw_commands( ) }); } + DrawCommand::BackgroundColor(color) => { + add_fill(&mut ctx, |mesh, _| { + rect( + mesh, + 0.0, + 0.0, + *width as f32, + *height as f32, + [0.0; 4], + color, + TessellationMode::Fill, + ) + }); + } + DrawCommand::BackgroundImage(entity) => { + let Some(p_image) = p_images.get(entity).ok() else { + warn!("Could not find PImage for entity {:?}", entity); + continue; + }; + + // force flush current batch before changing material + flush_batch(&mut ctx); + + let material_key = MaterialKey { + transparent: false, + background_image: Some(p_image.handle.clone()), + }; + + ctx.batch.material_key = Some(material_key); + ctx.batch.current_mesh = Some(empty_mesh()); + + // we're reusing rect to draw the fullscreen quad but don't need to track + // a fill here and can just pass white manually + if let Some(ref mut mesh) = ctx.batch.current_mesh { + rect( + mesh, + 0.0, + 0.0, + *width as f32, + *height as f32, + [0.0; 4], + Color::WHITE, + TessellationMode::Fill, + ) + } + + flush_batch(&mut ctx); + } } } @@ -142,20 +198,16 @@ pub fn activate_cameras( pub fn clear_transient_meshes( mut commands: Commands, - surfaces: Query<&Children, With>, - transient_meshes: Query<(), With>, + surfaces: Query<&TransientMeshes, With>, ) { - // for all flushing surfaces, despawn all transient meshes that rendered in a previous frame - for children in surfaces.iter() { - for child in children.iter() { - if transient_meshes.contains(child) { - commands.entity(child).despawn(); - } + for transient_meshes in surfaces.iter() { + for &mesh_entity in transient_meshes.0.iter() { + commands.entity(mesh_entity).despawn(); } } } -fn spawn_mesh(ctx: &mut RenderContext, mesh: Mesh, z_offset: Option) { +fn spawn_mesh(ctx: &mut RenderContext, mesh: Mesh, z_offset: f32) { let Some(material_key) = &ctx.batch.material_key else { return; }; @@ -166,22 +218,13 @@ fn spawn_mesh(ctx: &mut RenderContext, mesh: Mesh, z_offset: Option) { let mesh_handle = ctx.meshes.add(mesh); let material_handle = ctx.materials.add(material_key.to_material()); - let components = ( + ctx.commands.spawn(( Mesh3d(mesh_handle), MeshMaterial3d(material_handle), - TransientMesh, + BelongsToSurface(surface_entity), + Transform::from_xyz(0.0, 0.0, z_offset), ctx.batch.render_layers.clone(), - ); - - let mesh_id = if let Some(z) = z_offset { - ctx.commands - .spawn((components, Transform::from_xyz(0.0, 0.0, z))) - .id() - } else { - ctx.commands.spawn(components).id() - }; - - ctx.commands.entity(surface_entity).add_child(mesh_id); + )); } fn add_fill(ctx: &mut RenderContext, tessellate: impl FnOnce(&mut Mesh, Color)) { @@ -190,6 +233,7 @@ fn add_fill(ctx: &mut RenderContext, tessellate: impl FnOnce(&mut Mesh, Color)) }; let material_key = MaterialKey { transparent: ctx.state.fill_is_transparent(), + background_image: None, }; // when the material changes, flush the current batch @@ -212,6 +256,7 @@ fn add_stroke(ctx: &mut RenderContext, tessellate: impl FnOnce(&mut Mesh, Color, let stroke_weight = ctx.state.stroke_weight; let material_key = MaterialKey { transparent: ctx.state.stroke_is_transparent(), + background_image: None, }; // when the material changes, flush the current batch @@ -231,7 +276,7 @@ fn flush_batch(ctx: &mut RenderContext) { if let Some(mesh) = ctx.batch.current_mesh.take() { // we defensively apply a small z-offset based on draw_index to preserve painter's algorithm let z_offset = ctx.batch.draw_index as f32 * 0.001; - spawn_mesh(ctx, mesh, Some(z_offset)); + spawn_mesh(ctx, mesh, z_offset); ctx.batch.draw_index += 1; } ctx.batch.material_key = None; diff --git a/crates/processing_render/src/render/primitive/mod.rs b/crates/processing_render/src/render/primitive/mod.rs index 3d47dcd..0b24e40 100644 --- a/crates/processing_render/src/render/primitive/mod.rs +++ b/crates/processing_render/src/render/primitive/mod.rs @@ -52,6 +52,7 @@ pub fn empty_mesh() -> Mesh { mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, Vec::<[f32; 3]>::new()); mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, Vec::<[f32; 4]>::new()); mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, Vec::<[f32; 3]>::new()); + mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, Vec::<[f32; 2]>::new()); mesh.insert_indices(Indices::U32(Vec::new())); mesh diff --git a/crates/processing_render/src/render/primitive/rect.rs b/crates/processing_render/src/render/primitive/rect.rs index 39f49e9..20c5827 100644 --- a/crates/processing_render/src/render/primitive/rect.rs +++ b/crates/processing_render/src/render/primitive/rect.rs @@ -100,6 +100,13 @@ fn simple_rect(mesh: &mut Mesh, x: f32, y: f32, w: f32, h: f32, color: Color) { } } + if let Some(VertexAttributeValues::Float32x2(uvs)) = mesh.attribute_mut(Mesh::ATTRIBUTE_UV_0) { + uvs.push([0.0, 0.0]); // tl + uvs.push([1.0, 0.0]); // tr + uvs.push([1.0, 1.0]); // br + uvs.push([0.0, 1.0]); // bl + } + if let Some(Indices::U32(indices)) = mesh.indices_mut() { indices.push(base_idx); indices.push(base_idx + 1); diff --git a/examples/background_image.rs b/examples/background_image.rs new file mode 100644 index 0000000..f1ba852 --- /dev/null +++ b/examples/background_image.rs @@ -0,0 +1,38 @@ +mod glfw; + +use bevy::prelude::Color; +use glfw::GlfwContext; +use processing::prelude::*; +use processing_render::render::command::DrawCommand; + +fn main() { + match sketch() { + Ok(_) => { + eprintln!("Sketch completed successfully"); + exit(0).unwrap(); + } + Err(e) => { + eprintln!("Sketch error: {:?}", e); + exit(1).unwrap(); + } + }; +} + +fn sketch() -> error::Result<()> { + let mut glfw_ctx = GlfwContext::new(400, 400)?; + init()?; + + let window_handle = glfw_ctx.get_window(); + let display_handle = glfw_ctx.get_display(); + let surface = surface_create(window_handle, display_handle, 400, 400, 1.0)?; + let image = image_load("images/logo.png")?; + + while glfw_ctx.poll_events() { + begin_draw(surface)?; + + record_command(surface, DrawCommand::BackgroundImage(image))?; + + end_draw(surface)?; + } + Ok(()) +} diff --git a/examples/rectangle.rs b/examples/rectangle.rs index b5f8bc2..b8584df 100644 --- a/examples/rectangle.rs +++ b/examples/rectangle.rs @@ -23,7 +23,7 @@ fn sketch() -> error::Result<()> { let window_handle = glfw_ctx.get_window(); let display_handle = glfw_ctx.get_display(); - let surface = create_surface(window_handle, display_handle, 400, 400, 1.0)?; + let surface = surface_create(window_handle, display_handle, 400, 400, 1.0)?; while glfw_ctx.poll_events() { begin_draw(surface)?; diff --git a/ffi/include/processing.h b/ffi/include/processing.h deleted file mode 100644 index e67fefe..0000000 --- a/ffi/include/processing.h +++ /dev/null @@ -1,153 +0,0 @@ -#ifndef PROCESSING_H -#define PROCESSING_H - -#pragma once - -/* Generated with cbindgen:0.29.0 */ - -/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ - -#include -#include -#include -#include - -// A sRGB (?) color -typedef struct Color { - float r; - float g; - float b; - float a; -} Color; - -// Initialize libProcessing. -// -// SAFETY: -// - This is called from the main thread if the platform requires it. -// - This can only be called once. -void processing_init(void); - -// Create a WebGPU surface from a native window handle. -// Returns a window ID (entity ID) that should be used for subsequent operations. -// Returns 0 on failure. -// -// SAFETY: -// - Init has been called. -// - window_handle is a valid GLFW window pointer. -// - This is called from the same thread as init. -uint64_t processing_create_surface(uint64_t window_handle, - uint32_t width, - uint32_t height, - float scale_factor); - -// Destroy the surface associated with the given window ID. -// -// SAFETY: -// - Init and create_surface have been called. -// - window_id is a valid ID returned from create_surface. -// - This is called from the same thread as init. -void processing_destroy_surface(uint64_t window_id); - -// Update window size when resized. -// -// SAFETY: -// - Init and create_surface have been called. -// - window_id is a valid ID returned from create_surface. -// - This is called from the same thread as init. -void processing_resize_surface(uint64_t window_id, uint32_t width, uint32_t height); - -// Set the background color for the given window. -// -// SAFETY: -// - This is called from the same thread as init. -void processing_background_color(uint64_t window_id, struct Color color); - -// Begins the draw for the given window. -// -// SAFETY: -// - Init has been called and exit has not been called. -// - This is called from the same thread as init. -void processing_begin_draw(uint64_t window_id); - -// Flushes recorded draw commands for the given window. -// -// SAFETY: -// - Init has been called and exit has not been called. -// - This is called from the same thread as init. -void processing_flush(uint64_t window_id); - -// Ends the draw for the given window and presents the frame. -// -// SAFETY: -// - Init has been called and exit has not been called. -// - This is called from the same thread as init. -void processing_end_draw(uint64_t window_id); - -// Shuts down internal resources with given exit code, but does *not* terminate the process. -// -// SAFETY: -// - This is called from the same thread as init. -// - Caller ensures that update is never called again after exit. -void processing_exit(uint8_t exit_code); - -// Set the fill color. -// -// SAFETY: -// - Init and create_surface have been called. -// - window_id is a valid ID returned from create_surface. -// - This is called from the same thread as init. -void processing_set_fill(uint64_t window_id, float r, float g, float b, float a); - -// Set the stroke color. -// -// SAFETY: -// - Init and create_surface have been called. -// - window_id is a valid ID returned from create_surface. -// - This is called from the same thread as init. -void processing_set_stroke_color(uint64_t window_id, float r, float g, float b, float a); - -// Set the stroke weight. -// -// SAFETY: -// - Init and create_surface have been called. -// - window_id is a valid ID returned from create_surface. -// - This is called from the same thread as init. -void processing_set_stroke_weight(uint64_t window_id, float weight); - -// Disable fill for subsequent shapes. -// -// SAFETY: -// - Init and create_surface have been called. -// - window_id is a valid ID returned from create_surface. -// - This is called from the same thread as init. -void processing_no_fill(uint64_t window_id); - -// Disable stroke for subsequent shapes. -// -// SAFETY: -// - Init and create_surface have been called. -// - window_id is a valid ID returned from create_surface. -// - This is called from the same thread as init. -void processing_no_stroke(uint64_t window_id); - -// Draw a rectangle. -// -// SAFETY: -// - Init and create_surface have been called. -// - window_id is a valid ID returned from create_surface. -// - This is called from the same thread as init. -void processing_rect(uint64_t window_id, - float x, - float y, - float w, - float h, - float tl, - float tr, - float br, - float bl); - -// Check if the last operation resulted in an error. Returns a pointer to an error message, or null -// if there was no error. -const char *processing_check_error(void); - -#endif /* PROCESSING_H */