From f81c48f26332862e459887123a94132327b71dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Tue, 25 Nov 2025 17:32:08 -0800 Subject: [PATCH 1/5] Initial image implementation. --- Cargo.lock | 2 + assets/images/logo.png | Bin 0 -> 68387 bytes crates/processing_ffi/src/lib.rs | 44 +-- crates/processing_render/Cargo.toml | 2 + crates/processing_render/src/error.rs | 4 + crates/processing_render/src/image.rs | 311 ++++++++++++++++++ crates/processing_render/src/lib.rs | 55 +++- .../processing_render/src/render/command.rs | 2 + .../processing_render/src/render/material.rs | 2 + .../src/render/mesh_builder.rs | 6 + crates/processing_render/src/render/mod.rs | 111 +++++-- .../src/render/primitive/mod.rs | 1 + .../src/render/primitive/rect.rs | 13 +- examples/background_image.rs | 38 +++ examples/rectangle.rs | 2 +- ffi/include/processing.h | 153 --------- 16 files changed, 530 insertions(+), 216 deletions(-) create mode 100644 assets/images/logo.png create mode 100644 crates/processing_render/src/image.rs create mode 100644 examples/background_image.rs delete mode 100644 ffi/include/processing.h 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 0000000000000000000000000000000000000000..cf0b2e0bf186a55a621a723ec6a0f485302dd2a3 GIT binary patch literal 68387 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 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 +42,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. @@ -126,8 +126,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 +140,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 +154,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 +167,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 +180,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 +193,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( 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..6a4c828 100644 --- a/crates/processing_render/src/error.rs +++ b/crates/processing_render/src/error.rs @@ -14,4 +14,8 @@ 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, } diff --git a/crates/processing_render/src/image.rs b/crates/processing_render/src/image.rs new file mode 100644 index 0000000..93b5f74 --- /dev/null +++ b/crates/processing_render/src/image.rs @@ -0,0 +1,311 @@ +use crate::error::{ProcessingError, Result}; +use bevy::asset::io::embedded::GetAssetServer; +use bevy::asset::{RenderAssetUsages, handle_internal_asset_events, LoadState}; +use bevy::ecs::entity::EntityHashMap; +use bevy::ecs::system::{RunSystemOnce, SystemState}; +use bevy::prelude::*; +use bevy::render::render_asset::{AssetExtractionSystems, RenderAssets}; +use bevy::render::render_resource::{ + Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, MapMode, PollType, + TexelCopyBufferInfo, TexelCopyBufferLayout, Texture, TextureDimension, TextureFormat, +}; +use bevy::render::renderer::{RenderDevice, RenderQueue}; +use bevy::render::texture::GpuImage; +use bevy::render::{Extract, ExtractSchedule, MainWorld}; +use half::f16; +use std::path::PathBuf; + +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_else(|| 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..6ec6415 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -1,8 +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 bevy::render::render_resource::{Extent3d, TextureFormat}; use bevy::{ app::{App, AppExit}, asset::AssetEventSystems, @@ -17,6 +17,8 @@ use raw_window_handle::{ RawWindowHandle, WindowHandle, }; use render::{activate_cameras, clear_transient_meshes, flush_draw_commands}; +use std::path::PathBuf; +use std::{cell::RefCell, ffi::c_void, num::NonZero, ptr::NonNull, sync::OnceLock}; use tracing::debug; use crate::{ @@ -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..eddacfb 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -1,17 +1,28 @@ pub mod command; -pub mod material; pub mod mesh_builder; -mod primitive; +pub mod primitive; +pub mod material; -use bevy::{camera::visibility::RenderLayers, ecs::system::SystemParam, prelude::*}; +use bevy::{ + camera::visibility::RenderLayers, + ecs::system::SystemParam, + mesh::{Indices, VertexAttributeValues}, + prelude::*, +}; use command::{CommandBuffer, DrawCommand}; use material::MaterialKey; use primitive::{TessellationMode, empty_mesh}; -use crate::{Flush, render::primitive::rect}; +use crate::{Flush, SurfaceSize, render::primitive::rect}; +use crate::image::PImage; #[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 +84,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 +130,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 +204,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 +224,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 +239,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 +262,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 +282,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..1271867 100644 --- a/crates/processing_render/src/render/primitive/rect.rs +++ b/crates/processing_render/src/render/primitive/rect.rs @@ -100,12 +100,21 @@ 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 + 0); indices.push(base_idx + 1); indices.push(base_idx + 2); - indices.push(base_idx); + indices.push(base_idx + 0); indices.push(base_idx + 2); indices.push(base_idx + 3); } 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 */ From 3d837f0c1ad747ddc52899b94d6be6369ac36aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Tue, 25 Nov 2025 17:43:46 -0800 Subject: [PATCH 2/5] Ffi image fns. --- crates/processing_ffi/src/color.rs | 9 ++ crates/processing_ffi/src/error.rs | 2 +- crates/processing_ffi/src/lib.rs | 127 +++++++++++++++++++++++++- crates/processing_render/src/error.rs | 2 + 4 files changed, 138 insertions(+), 2 deletions(-) diff --git a/crates/processing_ffi/src/color.rs b/crates/processing_ffi/src/color.rs index fc063be..064da69 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() + } +} \ No newline at end of file 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 8d35370..3d7e6ab 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -1,4 +1,5 @@ use bevy::prelude::Entity; +use bevy::render::render_resource::{Extent3d, TextureFormat}; use processing::prelude::*; use crate::color::Color; @@ -73,7 +74,21 @@ pub extern "C" fn processing_surface_resize(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. @@ -223,3 +238,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/src/error.rs b/crates/processing_render/src/error.rs index 6a4c828..fa39266 100644 --- a/crates/processing_render/src/error.rs +++ b/crates/processing_render/src/error.rs @@ -18,4 +18,6 @@ pub enum ProcessingError { ImageNotFound, #[error("Unsupported texture format")] UnsupportedTextureFormat, + #[error("Invalid argument: {0}")] + InvalidArgument(String), } From 8e0c75bdaa1d05cb29221096a4ab45d2cdb6eae4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Tue, 25 Nov 2025 17:43:58 -0800 Subject: [PATCH 3/5] Fmt. --- crates/processing_ffi/src/color.rs | 2 +- crates/processing_ffi/src/lib.rs | 6 ++- crates/processing_render/src/image.rs | 47 ++++++++++--------- crates/processing_render/src/lib.rs | 6 +-- crates/processing_render/src/render/mod.rs | 5 +- .../src/render/primitive/rect.rs | 4 +- 6 files changed, 37 insertions(+), 33 deletions(-) diff --git a/crates/processing_ffi/src/color.rs b/crates/processing_ffi/src/color.rs index 064da69..fea6259 100644 --- a/crates/processing_ffi/src/color.rs +++ b/crates/processing_ffi/src/color.rs @@ -21,4 +21,4 @@ impl From for Color { let srgb = bevy::color::Color::srgba(lin.red, lin.green, lin.blue, lin.alpha); srgb.into() } -} \ No newline at end of file +} diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index 3d7e6ab..6dd1cdf 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -1,5 +1,7 @@ -use bevy::prelude::Entity; -use bevy::render::render_resource::{Extent3d, TextureFormat}; +use bevy::{ + prelude::Entity, + render::render_resource::{Extent3d, TextureFormat}, +}; use processing::prelude::*; use crate::color::Color; diff --git a/crates/processing_render/src/image.rs b/crates/processing_render/src/image.rs index 93b5f74..44fd259 100644 --- a/crates/processing_render/src/image.rs +++ b/crates/processing_render/src/image.rs @@ -1,19 +1,29 @@ -use crate::error::{ProcessingError, Result}; -use bevy::asset::io::embedded::GetAssetServer; -use bevy::asset::{RenderAssetUsages, handle_internal_asset_events, LoadState}; -use bevy::ecs::entity::EntityHashMap; -use bevy::ecs::system::{RunSystemOnce, SystemState}; -use bevy::prelude::*; -use bevy::render::render_asset::{AssetExtractionSystems, RenderAssets}; -use bevy::render::render_resource::{ - Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, MapMode, PollType, - TexelCopyBufferInfo, TexelCopyBufferLayout, Texture, TextureDimension, TextureFormat, +use std::path::PathBuf; + +use bevy::{ + asset::{ + LoadState, RenderAssetUsages, handle_internal_asset_events, io::embedded::GetAssetServer, + }, + ecs::{ + entity::EntityHashMap, + system::{RunSystemOnce, SystemState}, + }, + prelude::*, + render::{ + Extract, 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 bevy::render::renderer::{RenderDevice, RenderQueue}; -use bevy::render::texture::GpuImage; -use bevy::render::{Extract, ExtractSchedule, MainWorld}; use half::f16; -use std::path::PathBuf; + +use crate::error::{ProcessingError, Result}; pub struct PImagePlugin; @@ -101,18 +111,14 @@ pub fn create( } pub fn load(world: &mut World, path: PathBuf) -> Result { - fn load_inner( - In(path): In, - world: &mut World, - ) -> 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 images = world.resource::>(); let image = images .get(&handle) .ok_or_else(|| ProcessingError::ImageNotFound)?; @@ -308,4 +314,3 @@ pub fn destroy(world: &mut World, entity: Entity) -> Result<()> { .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 6ec6415..e4de6e3 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -2,7 +2,8 @@ pub mod error; pub mod image; pub mod render; -use bevy::render::render_resource::{Extent3d, TextureFormat}; +use std::{cell::RefCell, ffi::c_void, num::NonZero, path::PathBuf, ptr::NonNull, sync::OnceLock}; + use bevy::{ app::{App, AppExit}, asset::AssetEventSystems, @@ -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::{ @@ -17,8 +19,6 @@ use raw_window_handle::{ RawWindowHandle, WindowHandle, }; use render::{activate_cameras, clear_transient_meshes, flush_draw_commands}; -use std::path::PathBuf; -use std::{cell::RefCell, ffi::c_void, num::NonZero, ptr::NonNull, sync::OnceLock}; use tracing::debug; use crate::{ diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index eddacfb..4d05e01 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -1,7 +1,7 @@ pub mod command; +pub mod material; pub mod mesh_builder; pub mod primitive; -pub mod material; use bevy::{ camera::visibility::RenderLayers, @@ -13,8 +13,7 @@ use command::{CommandBuffer, DrawCommand}; use material::MaterialKey; use primitive::{TessellationMode, empty_mesh}; -use crate::{Flush, SurfaceSize, render::primitive::rect}; -use crate::image::PImage; +use crate::{Flush, SurfaceSize, image::PImage, render::primitive::rect}; #[derive(Component)] #[relationship(relationship_target = TransientMeshes)] diff --git a/crates/processing_render/src/render/primitive/rect.rs b/crates/processing_render/src/render/primitive/rect.rs index 1271867..b3bf76b 100644 --- a/crates/processing_render/src/render/primitive/rect.rs +++ b/crates/processing_render/src/render/primitive/rect.rs @@ -100,9 +100,7 @@ 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) - { + 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 From 8630d80841ff450c7c43e4d6da88ee7564ad0fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Tue, 25 Nov 2025 17:44:18 -0800 Subject: [PATCH 4/5] Clippy. --- crates/processing_render/src/image.rs | 6 +++--- crates/processing_render/src/lib.rs | 2 +- crates/processing_render/src/render/mod.rs | 1 - crates/processing_render/src/render/primitive/rect.rs | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/processing_render/src/image.rs b/crates/processing_render/src/image.rs index 44fd259..da051a2 100644 --- a/crates/processing_render/src/image.rs +++ b/crates/processing_render/src/image.rs @@ -6,11 +6,11 @@ use bevy::{ }, ecs::{ entity::EntityHashMap, - system::{RunSystemOnce, SystemState}, + system::RunSystemOnce, }, prelude::*, render::{ - Extract, ExtractSchedule, MainWorld, + ExtractSchedule, MainWorld, render_asset::{AssetExtractionSystems, RenderAssets}, render_resource::{ Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, MapMode, @@ -121,7 +121,7 @@ pub fn load(world: &mut World, path: PathBuf) -> Result { let images = world.resource::>(); let image = images .get(&handle) - .ok_or_else(|| ProcessingError::ImageNotFound)?; + .ok_or(ProcessingError::ImageNotFound)?; let size = image.texture_descriptor.size; let texture_format = image.texture_descriptor.format; diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index e4de6e3..b28c1ea 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -2,7 +2,7 @@ pub mod error; pub mod image; pub mod render; -use std::{cell::RefCell, ffi::c_void, num::NonZero, path::PathBuf, ptr::NonNull, sync::OnceLock}; +use std::{cell::RefCell, num::NonZero, path::PathBuf, ptr::NonNull, sync::OnceLock}; use bevy::{ app::{App, AppExit}, diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index 4d05e01..04bcdb5 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -6,7 +6,6 @@ pub mod primitive; use bevy::{ camera::visibility::RenderLayers, ecs::system::SystemParam, - mesh::{Indices, VertexAttributeValues}, prelude::*, }; use command::{CommandBuffer, DrawCommand}; diff --git a/crates/processing_render/src/render/primitive/rect.rs b/crates/processing_render/src/render/primitive/rect.rs index b3bf76b..20c5827 100644 --- a/crates/processing_render/src/render/primitive/rect.rs +++ b/crates/processing_render/src/render/primitive/rect.rs @@ -108,11 +108,11 @@ fn simple_rect(mesh: &mut Mesh, x: f32, y: f32, w: f32, h: f32, color: Color) { } if let Some(Indices::U32(indices)) = mesh.indices_mut() { - indices.push(base_idx + 0); + indices.push(base_idx); indices.push(base_idx + 1); indices.push(base_idx + 2); - indices.push(base_idx + 0); + indices.push(base_idx); indices.push(base_idx + 2); indices.push(base_idx + 3); } From a768cd3207f07731c3b433950d3443b61041f912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Tue, 25 Nov 2025 18:12:59 -0800 Subject: [PATCH 5/5] Fmt. --- crates/processing_render/src/image.rs | 9 ++------- crates/processing_render/src/render/mod.rs | 6 +----- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/crates/processing_render/src/image.rs b/crates/processing_render/src/image.rs index da051a2..1d6b25b 100644 --- a/crates/processing_render/src/image.rs +++ b/crates/processing_render/src/image.rs @@ -4,10 +4,7 @@ use bevy::{ asset::{ LoadState, RenderAssetUsages, handle_internal_asset_events, io::embedded::GetAssetServer, }, - ecs::{ - entity::EntityHashMap, - system::RunSystemOnce, - }, + ecs::{entity::EntityHashMap, system::RunSystemOnce}, prelude::*, render::{ ExtractSchedule, MainWorld, @@ -119,9 +116,7 @@ pub fn load(world: &mut World, path: PathBuf) -> Result { .expect("Failed to run internal asset events system"); } let images = world.resource::>(); - let image = images - .get(&handle) - .ok_or(ProcessingError::ImageNotFound)?; + let image = images.get(&handle).ok_or(ProcessingError::ImageNotFound)?; let size = image.texture_descriptor.size; let texture_format = image.texture_descriptor.format; diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index 04bcdb5..a5689d6 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -3,11 +3,7 @@ pub mod material; pub mod mesh_builder; pub mod primitive; -use bevy::{ - camera::visibility::RenderLayers, - ecs::system::SystemParam, - prelude::*, -}; +use bevy::{camera::visibility::RenderLayers, ecs::system::SystemParam, prelude::*}; use command::{CommandBuffer, DrawCommand}; use material::MaterialKey; use primitive::{TessellationMode, empty_mesh};