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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ bevy = { git = "https://github.com/processing/bevy", branch = "main", default-fe
"shader_format_wesl",
"free_camera",
"pan_camera",


# image formats Processing loads
"jpeg",
] }
bevy_naga_reflect = { git = "https://github.com/tychedelia/bevy_naga_reflect" }
bevy_cuda = { git = "https://github.com/tychedelia/bevy_cuda" }
Expand Down
31 changes: 21 additions & 10 deletions crates/processing_cuda/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use bevy::render::renderer::RenderDevice;
use bevy_cuda::{CudaBuffer, CudaContext};
use processing_core::app_mut;
use processing_core::error::{ProcessingError, Result};
use processing_render::graphics::view_target;
use processing_render::image::{Image, gpu_image, pixel_size};
use processing_render::graphics::{invalidate_pixels, main_texture, view_target};
use processing_render::image::{Image, pixel_size, texture_for_read, texture_for_write};

#[derive(Component)]
pub struct CudaImageBuffer {
Expand Down Expand Up @@ -44,9 +44,18 @@ fn cuda_ctx(world: &World) -> Result<&CudaContext> {
.ok_or(ProcessingError::CudaError("CUDA not available".into()))
}

fn resolve_texture(app: &mut App, entity: Entity) -> Result<(Texture, TextureFormat, u32, u32)> {
/// The texture to copy from, or with `write` to copy into, which leaves an image's CPU copy stale
fn resolve_texture(
app: &mut App,
entity: Entity,
write: bool,
) -> Result<(Texture, TextureFormat, u32, u32)> {
if app.world().get::<Image>(entity).is_some() {
let texture = gpu_image(app, entity)?.texture.clone();
let texture = if write {
texture_for_write(app, entity)?
} else {
texture_for_read(app, entity)?
};
let p_image = app.world().get::<Image>(entity).unwrap();
return Ok((
texture,
Expand All @@ -55,9 +64,11 @@ fn resolve_texture(app: &mut App, entity: Entity) -> Result<(Texture, TextureFor
p_image.size.height,
));
}
if let Ok(vt) = view_target(app, entity) {
let texture = vt.main_texture().clone();
let fmt = vt.main_texture_format();
if let Ok(texture) = main_texture(app, entity) {
if write {
invalidate_pixels(app, entity);
}
let fmt = view_target(app, entity)?.main_texture_format();
let size = texture.size();
return Ok((texture, fmt, size.width, size.height));
}
Expand All @@ -66,7 +77,7 @@ fn resolve_texture(app: &mut App, entity: Entity) -> Result<(Texture, TextureFor

pub fn cuda_export(entity: Entity) -> Result<()> {
app_mut(|app| {
let (texture, texture_format, width, height) = resolve_texture(app, entity)?;
let (texture, texture_format, width, height) = resolve_texture(app, entity, false)?;

let px_size = pixel_size(texture_format)?;
let buffer_size = (width as u64) * (height as u64) * (px_size as u64);
Expand Down Expand Up @@ -103,7 +114,7 @@ pub fn cuda_export(entity: Entity) -> Result<()> {

pub fn cuda_import(entity: Entity, src_device_ptr: u64, byte_size: u64) -> Result<()> {
app_mut(|app| {
let (texture, texture_format, width, height) = resolve_texture(app, entity)?;
let (texture, texture_format, width, height) = resolve_texture(app, entity, true)?;

let existing = app.world().get::<CudaImageBuffer>(entity);
let needs_alloc = existing.is_none_or(|buf| buf.buffer.size() != byte_size);
Expand Down Expand Up @@ -147,7 +158,7 @@ pub fn cuda_import(entity: Entity, src_device_ptr: u64, byte_size: u64) -> Resul

pub fn cuda_write_back(entity: Entity) -> Result<()> {
app_mut(|app| {
let (texture, _, _, _) = resolve_texture(app, entity)?;
let (texture, _, _, _) = resolve_texture(app, entity, true)?;

let cuda_buf = app
.world()
Expand Down
13 changes: 11 additions & 2 deletions crates/processing_ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1763,7 +1763,7 @@ pub unsafe extern "C" fn processing_graphics_update_region(
.iter()
.map(|color| color.to_linear())
.collect();
graphics_update_region(graphics_entity, x, y, width, height, &pixels)
graphics_update_region(graphics_entity, x.into(), y.into(), width, height, &pixels)
});
}

Expand All @@ -1776,7 +1776,16 @@ pub unsafe extern "C" fn processing_graphics_update_region(
pub extern "C" fn processing_graphics_set(graphics_id: u64, x: u32, y: u32, color: Color) {
error::clear_error();
let graphics_entity = Entity::from_bits(graphics_id);
error::check(|| graphics_update_region(graphics_entity, x, y, 1, 1, &[color.to_linear()]));
error::check(|| {
graphics_update_region(
graphics_entity,
x.into(),
y.into(),
1,
1,
&[color.to_linear()],
)
});
}

/// Set the tint color applied to images.
Expand Down
8 changes: 4 additions & 4 deletions crates/processing_node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1202,8 +1202,8 @@ pub fn js_graphics_update_region(
) -> Result<()> {
check(graphics_update_region(
ent(graphics_id),
x,
y,
x.into(),
y.into(),
width,
height,
&floats_to_linear(&pixels, space),
Expand All @@ -1224,8 +1224,8 @@ pub fn js_graphics_set(
let color = Color::new(c1, c2, c3, a, space).to_linear();
check(graphics_update_region(
ent(graphics_id),
x,
y,
x.into(),
y.into(),
1,
1,
&[color],
Expand Down
93 changes: 51 additions & 42 deletions crates/processing_pyo3/src/graphics.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::color::{ColorMode, extract_color_with_mode};
use crate::color::{ColorMode, ColorSpace, extract_color_with_mode, parse_numeric};
use crate::glfw::GlfwContext;
use crate::input;
use crate::math::{extract_vec2, extract_vec3, extract_vec4};
Expand Down Expand Up @@ -37,10 +37,14 @@ fn flatten_floats(args: &Bound<'_, PyTuple>) -> PyResult<Vec<f32>> {
fn pixels_to_pylist(py: Python<'_>, pixels: &[LinearRgba]) -> PyResult<Py<PyList>> {
let colors = pixels
.iter()
.map(|p| crate::color::PyColor(bevy::prelude::Color::LinearRgba(*p)));
.map(|p| crate::color::PyColor(bevy::prelude::Color::Srgba(Srgba::from(*p))));
Ok(PyList::new(py, colors)?.unbind())
}

fn pixel_item_error() -> PyErr {
PyValueError::new_err("pixels must contain Color values or [r, g, b, (a)] sequences")
}

/// Convert a Python sequence of colors (as returned by `load_pixels()`, or
/// individual `Color`/`[r,g,b,a]` values) into linear-RGBA pixels.
fn pyseq_to_pixels(seq: &Bound<'_, PyAny>) -> PyResult<Vec<LinearRgba>> {
Expand All @@ -49,14 +53,21 @@ fn pyseq_to_pixels(seq: &Bound<'_, PyAny>) -> PyResult<Vec<LinearRgba>> {
let item = item?;
let color = if let Ok(c) = item.extract::<crate::color::PyColor>() {
c.0
} else if let Ok(rgba) = item.extract::<[f32; 4]>() {
bevy::prelude::Color::LinearRgba(LinearRgba::new(rgba[0], rgba[1], rgba[2], rgba[3]))
} else if let Ok(rgb) = item.extract::<[f32; 3]>() {
bevy::prelude::Color::LinearRgba(LinearRgba::new(rgb[0], rgb[1], rgb[2], 1.0))
} else {
return Err(PyValueError::new_err(
"pixels must contain Color values or [r, g, b, (a)] sequences",
));
// bare components follow the default color mode: ints 0-255, floats 0-1
let space = ColorSpace::Srgb;
let parts: Vec<Bound<'_, PyAny>> = item
.try_iter()
.and_then(|it| it.collect())
.map_err(|_| pixel_item_error())?;
if parts.len() != 3 && parts.len() != 4 {
return Err(pixel_item_error());
}
let mut c = [1.0f32; 4];
for (ch, part) in parts.iter().enumerate() {
c[ch] = parse_numeric(&space, part, ch).map_err(|_| pixel_item_error())?;
}
space.color(c[0], c[1], c[2], c[3])
};
out.push(LinearRgba::from(color));
}
Expand Down Expand Up @@ -547,14 +558,25 @@ impl<'a, 'py> FromPyObject<'a, 'py> for ImageRef {
if let Ok(img) = ob.extract::<PyRef<Image>>() {
return Ok(ImageRef { entity: img.entity });
}
// an offscreen Graphics renders into an image; a window has no texture to sample
if let Ok(g) = ob.extract::<PyRef<Graphics>>() {
if image_size(g.surface.entity).is_err() {
return Err(pyo3::exceptions::PyTypeError::new_err(
"a window Graphics cannot be drawn as an image",
));
}
return Ok(ImageRef {
entity: g.surface.entity,
});
}
#[cfg(feature = "webcam")]
if let Ok(cam) = ob.extract::<PyRef<crate::webcam::Webcam>>() {
return Ok(ImageRef {
entity: cam.image_entity()?,
});
}
Err(pyo3::exceptions::PyTypeError::new_err(
"expected an Image or Webcam",
"expected an Image, Graphics or Webcam",
))
}
}
Expand Down Expand Up @@ -605,25 +627,19 @@ impl Image {
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

/// Read a single pixel as a `Color` (Processing `get`).
fn get(&self, x: u32, y: u32) -> PyResult<crate::color::PyColor> {
let (w, h) =
image_size(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
if x >= w || y >= h {
return Err(PyValueError::new_err(format!(
"pixel ({x}, {y}) out of bounds for {w}x{h} image"
)));
}
let pixels =
image_readback(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
let p = pixels[(y * w + x) as usize];
Ok(crate::color::PyColor(bevy::prelude::Color::LinearRgba(p)))
/// Read a single pixel as a `Color` (Processing `get`); outside the image it is transparent black.
fn get(&self, x: i64, y: i64) -> PyResult<crate::color::PyColor> {
let p = image_get_pixel(self.entity, x, y)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
Ok(crate::color::PyColor(bevy::prelude::Color::Srgba(
p.map_or(Srgba::NONE, Srgba::from),
)))
}

/// Write a single pixel (Processing `set`). Accepts a `Color`, hex string,
/// or color components in the default (sRGB) color mode.
/// Write a single pixel (Processing `set`); outside the image it is dropped. Accepts a
/// `Color`, hex string, or color components in the default (sRGB) color mode.
#[pyo3(signature = (x, y, *args))]
fn set(&self, x: u32, y: u32, args: &Bound<'_, PyTuple>) -> PyResult<()> {
fn set(&self, x: i64, y: i64, args: &Bound<'_, PyTuple>) -> PyResult<()> {
let color = extract_color_with_mode(args, &ColorMode::default())?;
image_update_region(self.entity, x, y, 1, 1, &[LinearRgba::from(color)])
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
Expand Down Expand Up @@ -1030,30 +1046,23 @@ impl Graphics {
}

/// Read a single pixel as a `Color` (Processing `get`).
pub fn get(&self, x: u32, y: u32) -> PyResult<crate::color::PyColor> {
if x >= self.width || y >= self.height {
return Err(PyValueError::new_err(format!(
"pixel ({x}, {y}) out of bounds for {}x{} canvas",
self.width, self.height
)));
}
let pixels =
graphics_readback(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
let p = pixels
.get((y * self.width + x) as usize)
.ok_or_else(|| PyValueError::new_err("pixel out of bounds"))?;
Ok(crate::color::PyColor(bevy::prelude::Color::LinearRgba(*p)))
pub fn get(&self, x: i64, y: i64) -> PyResult<crate::color::PyColor> {
let p = graphics_get_pixel(self.entity, x, y)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?;
Ok(crate::color::PyColor(bevy::prelude::Color::Srgba(
p.map_or(Srgba::NONE, Srgba::from),
)))
}

/// Write a single pixel (Processing `set`).
/// Write a single pixel (Processing `set`); outside the canvas it is dropped.
#[pyo3(signature = (x, y, *args))]
pub fn set(&self, x: u32, y: u32, args: &Bound<'_, PyTuple>) -> PyResult<()> {
pub fn set(&self, x: i64, y: i64, args: &Bound<'_, PyTuple>) -> PyResult<()> {
let color = extract_color_with_mode(
args,
&graphics_get_color_mode(self.entity)
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))?,
)?;
graphics_update_region(self.entity, x, y, 1, 1, &[LinearRgba::from(color)])
graphics_set_pixel(self.entity, x, y, LinearRgba::from(color))
.map_err(|e| PyRuntimeError::new_err(format!("{e}")))
}

Expand Down
6 changes: 3 additions & 3 deletions crates/processing_pyo3/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2320,16 +2320,16 @@ pub mod processing_py {

#[pyfunction]
#[pyo3(pass_module)]
fn get(module: &Bound<'_, PyModule>, x: u32, y: u32) -> PyResult<PyColor> {
fn get(module: &Bound<'_, PyModule>, x: i64, y: i64) -> PyResult<PyColor> {
graphics!(module).get(x, y)
}

#[pyfunction]
#[pyo3(pass_module, signature = (x, y, *args))]
fn set(
module: &Bound<'_, PyModule>,
x: u32,
y: u32,
x: i64,
y: i64,
args: &Bound<'_, PyTuple>,
) -> PyResult<()> {
graphics!(module).set(x, y, args)
Expand Down
8 changes: 1 addition & 7 deletions crates/processing_render/src/ci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,7 @@ pub fn done() -> bool {
}

pub(crate) fn readback_srgba8(app: &mut App, entity: Entity) -> Result<(u32, u32, Vec<u8>)> {
crate::graphics::flush(app, entity)?;
let vt = crate::graphics::view_target(app, entity)?;
let texture = vt.main_texture().clone();
let raw = app
.world_mut()
.run_system_cached_with(crate::graphics::readback_raw, (entity, texture))
.unwrap()?;
let raw = crate::graphics::read_pixels(app, entity)?;
let rgba = match raw.format {
TextureFormat::Rgba8UnormSrgb => raw.bytes,
format => {
Expand Down
Loading
Loading