From 1b34a507e1b7e3d238587eb442eef660c797a062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 8 Dec 2025 18:59:35 -0800 Subject: [PATCH 1/8] Refactor rendering crate to clarify primary Processing API. --- Cargo.lock | 187 +++--- Cargo.toml | 4 + crates/processing_ffi/src/lib.rs | 30 +- crates/processing_pyo3/src/lib.rs | 3 +- crates/processing_render/src/error.rs | 8 +- crates/processing_render/src/graphics.rs | 629 +++++++++++++++++++++ crates/processing_render/src/image.rs | 440 +++++++++----- crates/processing_render/src/lib.rs | 511 ++++------------- crates/processing_render/src/render/mod.rs | 39 +- crates/processing_render/src/surface.rs | 298 ++++++++++ docs/api.md | 77 +++ examples/background_image.rs | 13 +- examples/rectangle.rs | 15 +- examples/update_pixels.rs | 87 +++ 14 files changed, 1687 insertions(+), 654 deletions(-) create mode 100644 crates/processing_render/src/graphics.rs create mode 100644 crates/processing_render/src/surface.rs create mode 100644 docs/api.md create mode 100644 examples/update_pixels.rs diff --git a/Cargo.lock b/Cargo.lock index 174d436..3ba0dd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -460,7 +460,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bevy" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_internal", ] @@ -468,7 +468,7 @@ dependencies = [ [[package]] name = "bevy_a11y" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "accesskit", "bevy_app", @@ -480,7 +480,7 @@ dependencies = [ [[package]] name = "bevy_android" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "android-activity", ] @@ -488,7 +488,7 @@ dependencies = [ [[package]] name = "bevy_animation" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_animation_macros", "bevy_app", @@ -520,7 +520,7 @@ dependencies = [ [[package]] name = "bevy_animation_macros" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_macro_utils", "quote", @@ -530,7 +530,7 @@ dependencies = [ [[package]] name = "bevy_anti_alias" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -551,7 +551,7 @@ dependencies = [ [[package]] name = "bevy_app" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_derive", "bevy_ecs", @@ -573,7 +573,7 @@ dependencies = [ [[package]] name = "bevy_asset" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "async-broadcast", "async-channel", @@ -614,7 +614,7 @@ dependencies = [ [[package]] name = "bevy_asset_macros" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -625,7 +625,7 @@ dependencies = [ [[package]] name = "bevy_audio" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -642,7 +642,7 @@ dependencies = [ [[package]] name = "bevy_camera" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "bevy_color" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_math", "bevy_reflect", @@ -682,7 +682,7 @@ dependencies = [ [[package]] name = "bevy_core_pipeline" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -710,17 +710,43 @@ dependencies = [ [[package]] name = "bevy_derive" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_macro_utils", "quote", "syn", ] +[[package]] +name = "bevy_dev_tools" +version = "0.18.0-dev" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_diagnostic", + "bevy_ecs", + "bevy_input", + "bevy_math", + "bevy_picking", + "bevy_reflect", + "bevy_render", + "bevy_shader", + "bevy_state", + "bevy_text", + "bevy_time", + "bevy_ui", + "bevy_ui_render", + "bevy_window", + "tracing", +] + [[package]] name = "bevy_diagnostic" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "atomic-waker", "bevy_app", @@ -737,7 +763,7 @@ dependencies = [ [[package]] name = "bevy_ecs" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "arrayvec", "bevy_ecs_macros", @@ -764,7 +790,7 @@ dependencies = [ [[package]] name = "bevy_ecs_macros" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -775,7 +801,7 @@ dependencies = [ [[package]] name = "bevy_encase_derive" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_macro_utils", "encase_derive_impl", @@ -784,7 +810,7 @@ dependencies = [ [[package]] name = "bevy_gilrs" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_ecs", @@ -799,7 +825,7 @@ dependencies = [ [[package]] name = "bevy_gizmos" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -818,7 +844,7 @@ dependencies = [ [[package]] name = "bevy_gizmos_macros" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_macro_utils", "quote", @@ -828,7 +854,7 @@ dependencies = [ [[package]] name = "bevy_gizmos_render" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -852,7 +878,7 @@ dependencies = [ [[package]] name = "bevy_gltf" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "base64", "bevy_animation", @@ -886,7 +912,7 @@ dependencies = [ [[package]] name = "bevy_image" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -914,7 +940,7 @@ dependencies = [ [[package]] name = "bevy_input" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_ecs", @@ -930,7 +956,7 @@ dependencies = [ [[package]] name = "bevy_input_focus" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_camera", @@ -948,7 +974,7 @@ dependencies = [ [[package]] name = "bevy_internal" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_a11y", "bevy_android", @@ -961,6 +987,7 @@ dependencies = [ "bevy_color", "bevy_core_pipeline", "bevy_derive", + "bevy_dev_tools", "bevy_diagnostic", "bevy_ecs", "bevy_gilrs", @@ -1000,7 +1027,7 @@ dependencies = [ [[package]] name = "bevy_light" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -1020,7 +1047,7 @@ dependencies = [ [[package]] name = "bevy_log" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "android_log-sys", "bevy_app", @@ -1037,7 +1064,7 @@ dependencies = [ [[package]] name = "bevy_macro_utils" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "proc-macro2", "quote", @@ -1048,7 +1075,7 @@ dependencies = [ [[package]] name = "bevy_math" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "approx", "arrayvec", @@ -1067,7 +1094,7 @@ dependencies = [ [[package]] name = "bevy_mesh" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -1097,7 +1124,7 @@ checksum = "7ef8e4b7e61dfe7719bb03c884dc270cd46a82efb40f93e9933b990c5c190c59" [[package]] name = "bevy_pbr" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -1132,7 +1159,7 @@ dependencies = [ [[package]] name = "bevy_picking" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -1155,12 +1182,11 @@ dependencies = [ [[package]] name = "bevy_platform" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "critical-section", "foldhash 0.2.0", "futures-channel", - "getrandom", "hashbrown 0.16.1", "js-sys", "portable-atomic", @@ -1175,7 +1201,7 @@ dependencies = [ [[package]] name = "bevy_post_process" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -1204,12 +1230,12 @@ dependencies = [ [[package]] name = "bevy_ptr" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" [[package]] name = "bevy_reflect" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "assert_type_match", "bevy_platform", @@ -1236,7 +1262,7 @@ dependencies = [ [[package]] name = "bevy_reflect_derive" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_macro_utils", "indexmap", @@ -1249,7 +1275,7 @@ dependencies = [ [[package]] name = "bevy_render" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "async-channel", "bevy_app", @@ -1298,7 +1324,7 @@ dependencies = [ [[package]] name = "bevy_render_macros" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_macro_utils", "proc-macro2", @@ -1309,7 +1335,7 @@ dependencies = [ [[package]] name = "bevy_scene" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -1330,7 +1356,7 @@ dependencies = [ [[package]] name = "bevy_shader" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_asset", "bevy_platform", @@ -1346,7 +1372,7 @@ dependencies = [ [[package]] name = "bevy_sprite" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -1370,7 +1396,7 @@ dependencies = [ [[package]] name = "bevy_sprite_render" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -1401,7 +1427,7 @@ dependencies = [ [[package]] name = "bevy_state" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_ecs", @@ -1416,7 +1442,7 @@ dependencies = [ [[package]] name = "bevy_state_macros" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_macro_utils", "quote", @@ -1426,7 +1452,7 @@ dependencies = [ [[package]] name = "bevy_tasks" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "async-channel", "async-executor", @@ -1444,7 +1470,7 @@ dependencies = [ [[package]] name = "bevy_text" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -1469,7 +1495,7 @@ dependencies = [ [[package]] name = "bevy_time" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_ecs", @@ -1483,7 +1509,7 @@ dependencies = [ [[package]] name = "bevy_transform" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_ecs", @@ -1500,7 +1526,7 @@ dependencies = [ [[package]] name = "bevy_ui" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "accesskit", "bevy_a11y", @@ -1532,7 +1558,7 @@ dependencies = [ [[package]] name = "bevy_ui_render" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -1562,7 +1588,7 @@ dependencies = [ [[package]] name = "bevy_utils" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_platform", "disqualified", @@ -1572,7 +1598,7 @@ dependencies = [ [[package]] name = "bevy_window" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "bevy_app", "bevy_asset", @@ -1590,7 +1616,7 @@ dependencies = [ [[package]] name = "bevy_winit" version = "0.18.0-dev" -source = "git+https://github.com/bevyengine/bevy?branch=main#41f59f1891067799eac93796b3e1ffb9775002b5" +source = "git+https://github.com/bevyengine/bevy?branch=main#8ca07c4727ee2cde23e7129014208cf96a08a2b6" dependencies = [ "accesskit", "accesskit_winit", @@ -1831,9 +1857,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.48" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "jobserver", @@ -2676,18 +2702,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", "wasip2", - "wasm-bindgen", ] [[package]] name = "gif" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f954a9e9159ec994f73a30a12b96a702dde78f5547bcb561174597924f7d4162" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" dependencies = [ "color_quant", "weezl", @@ -3020,7 +3044,7 @@ dependencies = [ "rgb", "tiff", "zune-core 0.5.0", - "zune-jpeg 0.5.5", + "zune-jpeg 0.5.6", ] [[package]] @@ -3325,9 +3349,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loop9" @@ -4451,9 +4475,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3502d6155304a4173a5f2c34b52b7ed0dd085890326cb50fd625fdf39e86b3b" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" dependencies = [ "num-traits", ] @@ -4779,14 +4803,15 @@ dependencies = [ [[package]] name = "ron" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468" +checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" dependencies = [ - "base64", "bitflags 2.10.0", + "once_cell", "serde", "serde_derive", + "typeid", "unicode-ident", ] @@ -4985,9 +5010,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "simd_helpers" @@ -5016,9 +5041,9 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "slotmap" -version = "1.0.7" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" dependencies = [ "version_check", ] @@ -5334,9 +5359,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.23.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" dependencies = [ "indexmap", "toml_datetime", @@ -5490,9 +5515,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-script" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" [[package]] name = "unicode-segmentation" @@ -6616,9 +6641,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6fb7703e32e9a07fb3f757360338b3a567a5054f21b5f52a666752e333d58e" +checksum = "f520eebad972262a1dde0ec455bce4f8b298b1e5154513de58c114c4c54303e8" dependencies = [ "zune-core 0.5.0", ] diff --git a/Cargo.toml b/Cargo.toml index 13c10a3..0331c5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,10 @@ path = "examples/rectangle.rs" name = "background_image" path = "examples/background_image.rs" +[[example]] +name = "update_pixels" +path = "examples/update_pixels.rs" + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index 872d40d..977cdc5 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -76,7 +76,9 @@ 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(|| record_command(window_entity, DrawCommand::BackgroundColor(color.into()))); + error::check(|| { + graphics_record_command(window_entity, DrawCommand::BackgroundColor(color.into())) + }); } /// Set the background image for the given window. @@ -90,7 +92,9 @@ 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))); + error::check(|| { + graphics_record_command(window_entity, DrawCommand::BackgroundImage(image_entity)) + }); } /// Begins the draw for the given window. @@ -102,7 +106,7 @@ pub extern "C" fn processing_background_image(window_id: u64, image_id: u64) { pub extern "C" fn processing_begin_draw(window_id: u64) { error::clear_error(); let window_entity = Entity::from_bits(window_id); - error::check(|| begin_draw(window_entity)); + error::check(|| graphics_begin_draw(window_entity)); } /// Flushes recorded draw commands for the given window. @@ -114,7 +118,7 @@ pub extern "C" fn processing_begin_draw(window_id: u64) { pub extern "C" fn processing_flush(window_id: u64) { error::clear_error(); let window_entity = Entity::from_bits(window_id); - error::check(|| flush(window_entity)); + error::check(|| graphics_flush(window_entity)); } /// Ends the draw for the given window and presents the frame. @@ -126,7 +130,7 @@ pub extern "C" fn processing_flush(window_id: u64) { pub extern "C" fn processing_end_draw(window_id: u64) { error::clear_error(); let window_entity = Entity::from_bits(window_id); - error::check(|| end_draw(window_entity)); + error::check(|| graphics_end_draw(window_entity)); } /// Shuts down internal resources with given exit code, but does *not* terminate the process. @@ -151,7 +155,7 @@ pub extern "C" fn processing_set_fill(window_id: u64, r: f32, g: f32, b: f32, a: error::clear_error(); let window_entity = Entity::from_bits(window_id); let color = bevy::color::Color::srgba(r, g, b, a); - error::check(|| record_command(window_entity, DrawCommand::Fill(color))); + error::check(|| graphics_record_command(window_entity, DrawCommand::Fill(color))); } /// Set the stroke color. @@ -165,7 +169,7 @@ pub extern "C" fn processing_set_stroke_color(window_id: u64, r: f32, g: f32, b: error::clear_error(); let window_entity = Entity::from_bits(window_id); let color = bevy::color::Color::srgba(r, g, b, a); - error::check(|| record_command(window_entity, DrawCommand::StrokeColor(color))); + error::check(|| graphics_record_command(window_entity, DrawCommand::StrokeColor(color))); } /// Set the stroke weight. @@ -178,7 +182,7 @@ pub extern "C" fn processing_set_stroke_color(window_id: u64, r: f32, g: f32, b: pub extern "C" fn processing_set_stroke_weight(window_id: u64, weight: f32) { error::clear_error(); let window_entity = Entity::from_bits(window_id); - error::check(|| record_command(window_entity, DrawCommand::StrokeWeight(weight))); + error::check(|| graphics_record_command(window_entity, DrawCommand::StrokeWeight(weight))); } /// Disable fill for subsequent shapes. @@ -191,7 +195,7 @@ pub extern "C" fn processing_set_stroke_weight(window_id: u64, weight: f32) { pub extern "C" fn processing_no_fill(window_id: u64) { error::clear_error(); let window_entity = Entity::from_bits(window_id); - error::check(|| record_command(window_entity, DrawCommand::NoFill)); + error::check(|| graphics_record_command(window_entity, DrawCommand::NoFill)); } /// Disable stroke for subsequent shapes. @@ -204,7 +208,7 @@ pub extern "C" fn processing_no_fill(window_id: u64) { pub extern "C" fn processing_no_stroke(window_id: u64) { error::clear_error(); let window_entity = Entity::from_bits(window_id); - error::check(|| record_command(window_entity, DrawCommand::NoStroke)); + error::check(|| graphics_record_command(window_entity, DrawCommand::NoStroke)); } /// Draw a rectangle. @@ -228,7 +232,7 @@ pub extern "C" fn processing_rect( error::clear_error(); let window_entity = Entity::from_bits(window_id); error::check(|| { - record_command( + graphics_record_command( window_entity, DrawCommand::Rect { x, @@ -318,7 +322,7 @@ pub extern "C" fn processing_image_resize(image_id: u64, new_width: u32, new_hei /// - buffer_len must equal width * height of the image. /// - This is called from the same thread as init. #[unsafe(no_mangle)] -pub unsafe extern "C" fn processing_image_load_pixels( +pub unsafe extern "C" fn processing_image_readback( image_id: u64, buffer: *mut Color, buffer_len: usize, @@ -326,7 +330,7 @@ pub unsafe extern "C" fn processing_image_load_pixels( error::clear_error(); let image_entity = Entity::from_bits(image_id); error::check(|| { - let colors = image_load_pixels(image_entity)?; + let colors = image_readback(image_entity)?; // Validate buffer size if colors.len() != buffer_len { diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 9f1abef..5c6db44 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -3,10 +3,11 @@ use pyo3::prelude::*; #[pymodule] mod processing { - use crate::glfw::GlfwContext; use processing::prelude::*; use pyo3::prelude::*; + use crate::glfw::GlfwContext; + /// create surface #[pyfunction] fn size(width: u32, height: u32) -> PyResult { diff --git a/crates/processing_render/src/error.rs b/crates/processing_render/src/error.rs index fa39266..db1af3b 100644 --- a/crates/processing_render/src/error.rs +++ b/crates/processing_render/src/error.rs @@ -8,8 +8,8 @@ pub enum ProcessingError { AppAccess, #[error("Error initializing tracing: {0}")] Tracing(#[from] tracing::subscriber::SetGlobalDefaultError), - #[error("Window not found")] - WindowNotFound, + #[error("Surface not found")] + SurfaceNotFound, #[error("Handle error: {0}")] HandleError(#[from] raw_window_handle::HandleError), #[error("Invalid window handle provided")] @@ -20,4 +20,8 @@ pub enum ProcessingError { UnsupportedTextureFormat, #[error("Invalid argument: {0}")] InvalidArgument(String), + #[error("Graphics not found")] + GraphicsNotFound, + #[error("Invalid entity")] + InvalidEntity, } diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs new file mode 100644 index 0000000..70055cf --- /dev/null +++ b/crates/processing_render/src/graphics.rs @@ -0,0 +1,629 @@ +//! A graphics object is the core rendering context in Processing, responsible for managing the +//! draw state and recording draw commands to be executed each frame. +//! +//! In Bevy terms, a graphics object is represented as an entity with a camera component +//! configured to render to a specific surface (either a window or an offscreen image). +use bevy::{ + camera::{ + CameraMainTextureUsages, CameraOutputMode, CameraProjection, ClearColorConfig, + ImageRenderTarget, MsaaWriteback, Projection, RenderTarget, visibility::RenderLayers, + }, + core_pipeline::tonemapping::Tonemapping, + ecs::{entity::EntityHashMap, query::QueryEntityError}, + math::{Mat4, Vec3A}, + prelude::*, + render::{ + Render, RenderSystems, + render_resource::{ + CommandEncoderDescriptor, Extent3d, MapMode, Origin3d, PollType, TexelCopyBufferInfo, + TexelCopyBufferLayout, TexelCopyTextureInfo, TextureFormat, TextureUsages, + }, + renderer::{RenderDevice, RenderQueue}, + sync_world::MainEntity, + view::{Hdr, ViewTarget, prepare_view_targets}, + }, + window::WindowRef, +}; + +use crate::{ + Flush, + error::{ProcessingError, Result}, + image::{Image, bytes_to_pixels, create_readback_buffer, pixel_size, pixels_to_bytes}, + render::command::{CommandBuffer, DrawCommand}, + surface::Surface, +}; + +pub struct GraphicsPlugin; + +impl Plugin for GraphicsPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + + let (tx, rx) = crossbeam_channel::unbounded::<(Entity, ViewTarget)>(); + app.init_resource::() + .insert_resource(GraphicsTargetReceiver(rx)) + .add_systems(First, update_view_targets); + + let render_app = app.sub_app_mut(bevy::render::RenderApp); + render_app + .add_systems( + Render, + send_view_targets + .in_set(RenderSystems::ManageViews) + .after(prepare_view_targets), + ) + .insert_resource(GraphicsTargetSender(tx)); + } +} + +#[derive(Component)] +pub struct Graphics { + readback_buffer: bevy::render::render_resource::Buffer, + pub texture_format: TextureFormat, + pub size: Extent3d, +} + +// We store a mapping of graphics target entities to their GPU `ViewTarget`s. In the +// Processing API, graphics *are* images, so we need to be able to look up the `ViewTarget` for a +// given graphics entity when referencing it as an image. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct GraphicsTargets(EntityHashMap); + +#[derive(Resource, Deref, DerefMut)] +pub struct GraphicsTargetReceiver(crossbeam_channel::Receiver<(Entity, ViewTarget)>); + +#[derive(Resource, Deref, DerefMut)] +pub struct GraphicsTargetSender(crossbeam_channel::Sender<(Entity, ViewTarget)>); + +fn send_view_targets( + view_targets: Query<(MainEntity, &ViewTarget), Changed>, + sender: Res, +) { + for (main_entity, view_target) in view_targets.iter() { + sender + .send((main_entity, view_target.clone())) + .expect("Failed to send updated view target"); + } +} + +pub fn update_view_targets( + mut graphics_targets: ResMut, + receiver: Res, +) { + while let Ok((entity, view_target)) = receiver.0.try_recv() { + graphics_targets.insert(entity, view_target); + } +} + +macro_rules! graphics_mut { + ($app:expr, $entity:expr) => { + $app.world_mut() + .get_entity_mut($entity) + .map_err(|_| ProcessingError::GraphicsNotFound)? + }; +} + +#[derive(Component)] +pub struct SurfaceSize(pub u32, pub 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)] +#[reflect(Default)] +pub struct ProcessingProjection { + pub width: f32, + pub height: f32, + pub near: f32, + pub far: f32, +} + +impl Default for ProcessingProjection { + fn default() -> Self { + Self { + width: 1.0, + height: 1.0, + near: 0.0, + far: 1000.0, + } + } +} + +impl CameraProjection for ProcessingProjection { + fn get_clip_from_view(&self) -> Mat4 { + Mat4::orthographic_rh( + 0.0, + self.width, + self.height, // bottom = height + 0.0, // top = 0 + self.near, + self.far, + ) + } + + fn get_clip_from_view_for_sub(&self, _sub_view: &bevy::camera::SubCameraView) -> Mat4 { + // TODO: implement sub-view support if needed (probably not) + self.get_clip_from_view() + } + + fn update(&mut self, width: f32, height: f32) { + self.width = width; + self.height = height; + } + + fn far(&self) -> f32 { + self.far + } + + fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] { + // order: bottom-right, top-right, top-left, bottom-left for near, then far + let near_center = Vec3A::new(self.width / 2.0, self.height / 2.0, z_near); + let far_center = Vec3A::new(self.width / 2.0, self.height / 2.0, z_far); + + let half_width = self.width / 2.0; + let half_height = self.height / 2.0; + + [ + // near plane + near_center + Vec3A::new(half_width, half_height, 0.0), // bottom-right + near_center + Vec3A::new(half_width, -half_height, 0.0), // top-right + near_center + Vec3A::new(-half_width, -half_height, 0.0), // top-left + near_center + Vec3A::new(-half_width, half_height, 0.0), // bottom-left + // far plane + far_center + Vec3A::new(half_width, half_height, 0.0), // bottom-right + far_center + Vec3A::new(half_width, -half_height, 0.0), // top-right + far_center + Vec3A::new(-half_width, -half_height, 0.0), // top-left + far_center + Vec3A::new(-half_width, half_height, 0.0), // bottom-left + ] + } +} + +pub fn create( + world: &mut World, + surface_entity: Entity, + width: u32, + height: u32, +) -> Result { + fn create_inner( + In((width, height, surface_entity)): In<(u32, u32, Entity)>, + mut commands: Commands, + mut layer_manager: ResMut, + p_images: Query<&Image, With>, + render_device: Res, + ) -> Result { + // find the surface entity, if it is an image, we will render to that image + // otherwise we will render to the window + let target = match p_images.get(surface_entity) { + Ok(p_image) => RenderTarget::Image(ImageRenderTarget::from(p_image.handle.clone())), + Err(QueryEntityError::QueryDoesNotMatch(..)) => { + RenderTarget::Window(WindowRef::Entity(surface_entity)) + } + Err(_) => return Err(ProcessingError::SurfaceNotFound), + }; + // allocate a new render layer for this graphics entity, which ensures that anything + // drawn to this camera will only be visible to this camera + let render_layer = layer_manager.allocate(); + + // TODO: make this configurable, right now we are hard-coding hdr camera + let texture_format = TextureFormat::Rgba16Float; + let size = Extent3d { + width, + height, + depth_or_array_layers: 1, + }; + let readback_buffer = create_readback_buffer( + &render_device, + width, + height, + texture_format, + "Graphics Readback Buffer", + ) + .expect("Failed to create readback buffer"); + + let entity = commands + .spawn(( + Camera3d::default(), + Camera { + target, + // always load the previous frame (provides sketch like behavior) + clear_color: ClearColorConfig::None, + // TODO: toggle this conditionally based on whether we need to write back MSAA + // when doing manual pixel udpates + msaa_writeback: MsaaWriteback::Always, + ..default() + }, + // default to floating point texture format + Hdr, + // tonemapping prevents color accurate readback, so we disable it + Tonemapping::None, + // we need to be able to write to the texture + CameraMainTextureUsages::default().with(TextureUsages::COPY_DST), + Projection::custom(ProcessingProjection { + width: width as f32, + height: height as f32, + near: 0.0, + far: 1000.0, + }), + Transform::from_xyz(0.0, 0.0, 999.9), + render_layer, + CommandBuffer::new(), + SurfaceSize(width, height), + Graphics { + readback_buffer, + texture_format, + size, + }, + )) + .id(); + + Ok(entity) + } + + world + .run_system_cached_with(create_inner, (width, height, surface_entity)) + .unwrap() +} + +#[allow(dead_code)] +pub fn resize(world: &mut World, entity: Entity, width: u32, height: u32) -> Result<()> { + fn resize_inner( + In((entity, width, height)): In<(Entity, u32, u32)>, + mut graphics_query: Query<&mut Projection>, + ) -> Result<()> { + let mut projection = graphics_query + .get_mut(entity) + .map_err(|_| ProcessingError::GraphicsNotFound)?; + + if let Projection::Custom(ref mut custom_proj) = *projection { + custom_proj.update(width as f32, height as f32); + Ok(()) + } else { + panic!( + "Expected custom projection for Processing graphics entity, this should not happen. If you are seeing this message, please report a bug." + ); + } + } + + world + .run_system_cached_with(resize_inner, (entity, width, height)) + .unwrap() +} + +pub fn destroy(world: &mut World, entity: Entity) -> Result<()> { + fn destroy_inner( + In(entity): In, + mut commands: Commands, + mut layer_manager: ResMut, + graphics_query: Query<&RenderLayers>, + ) -> Result<()> { + let Ok(render_layers) = graphics_query.get(entity) else { + return Err(ProcessingError::GraphicsNotFound); + }; + + layer_manager.free(render_layers.clone()); + commands.entity(entity).despawn(); + Ok(()) + } + + world.run_system_cached_with(destroy_inner, entity).unwrap() +} + +pub fn begin_draw(_app: &mut App, _entity: Entity) -> Result<()> { + // nothing to do here for now + Ok(()) +} + +pub fn flush(app: &mut App, entity: Entity) -> Result<()> { + graphics_mut!(app, entity).insert(Flush); + app.update(); + graphics_mut!(app, entity).remove::(); + // ensure graphics targets are available immediately after flush + app.world_mut() + .run_system_cached(update_view_targets) + .expect("Failed to run update_view_targets"); + + Ok(()) +} + +pub fn end_draw(app: &mut App, entity: Entity) -> Result<()> { + // Enable output to present the frame, but don't clear (preserve pixel writes) + graphics_mut!(app, entity) + .get_mut::() + .ok_or(ProcessingError::GraphicsNotFound)? + .output_mode = CameraOutputMode::Write { + blend_state: None, + clear_color: ClearColorConfig::None, + }; + // flush any remaining draw commands, this ensures that the frame is presented even if there + // is no remaining draw commands + flush(app, entity)?; + graphics_mut!(app, entity) + .get_mut::() + .ok_or(ProcessingError::GraphicsNotFound)? + .output_mode = CameraOutputMode::Skip; + Ok(()) +} + +pub fn record_command(world: &mut World, window_entity: Entity, cmd: DrawCommand) -> Result<()> { + fn record_command_inner( + In((graphics_entity, cmd)): In<(Entity, DrawCommand)>, + mut graphics_query: Query<&mut CommandBuffer>, + ) -> Result<()> { + let mut command_buffer = graphics_query + .get_mut(graphics_entity) + .map_err(|_| ProcessingError::GraphicsNotFound)?; + + command_buffer.push(cmd); + Ok(()) + } + + world + .run_system_cached_with(record_command_inner, (window_entity, cmd)) + .unwrap() +} + +pub fn readback(world: &mut World, entity: Entity) -> Result> { + fn readback_inner( + In(entity): In, + graphics_query: Query<&Graphics>, + graphics_targets: Res, + render_device: Res, + render_queue: Res, + ) -> Result> { + let graphics = graphics_query + .get(entity) + .map_err(|_| ProcessingError::GraphicsNotFound)?; + + let view_target = graphics_targets + .get(&entity) + .ok_or(ProcessingError::GraphicsNotFound)?; + + let texture = view_target.main_texture(); + eprintln!("readback: reading from texture {:p}", texture as *const _); + + let mut encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor::default()); + + let px_size = pixel_size(graphics.texture_format)?; + let padded_bytes_per_row = + RenderDevice::align_copy_bytes_per_row(graphics.size.width as usize * px_size); + + encoder.copy_texture_to_buffer( + texture.as_image_copy(), + TexelCopyBufferInfo { + buffer: &graphics.readback_buffer, + layout: TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some( + std::num::NonZero::::new(padded_bytes_per_row as u32) + .unwrap() + .into(), + ), + rows_per_image: None, + }, + }, + graphics.size, + ); + + render_queue.submit(std::iter::once(encoder.finish())); + + let buffer_slice = graphics.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(); + + graphics.readback_buffer.unmap(); + + bytes_to_pixels( + &data, + graphics.texture_format, + graphics.size.width, + graphics.size.height, + padded_bytes_per_row, + ) + } + + world + .run_system_cached_with(readback_inner, entity) + .expect("Failed to run readback system") +} + +pub fn update_region( + world: &mut World, + entity: Entity, + x: u32, + y: u32, + width: u32, + height: u32, + pixels: &[LinearRgba], +) -> Result<()> { + let expected_count = (width * height) as usize; + if pixels.len() != expected_count { + return Err(ProcessingError::InvalidArgument(format!( + "Expected {} pixels for {}x{} region, got {}", + expected_count, + width, + height, + pixels.len() + ))); + } + + fn update_region_inner( + In((entity, x, y, width, height, data, px_size)): In<( + Entity, + u32, + u32, + u32, + u32, + Vec, + u32, + )>, + graphics_query: Query<&Graphics>, + graphics_targets: Res, + render_queue: Res, + ) -> Result<()> { + let graphics = graphics_query + .get(entity) + .map_err(|_| ProcessingError::GraphicsNotFound)?; + + // bounds check + if x + width > graphics.size.width || y + height > graphics.size.height { + return Err(ProcessingError::InvalidArgument(format!( + "Region ({}, {}, {}, {}) exceeds graphics bounds ({}, {})", + x, y, width, height, graphics.size.width, graphics.size.height + ))); + } + + let view_target = graphics_targets + .get(&entity) + .ok_or(ProcessingError::GraphicsNotFound)?; + + let texture = view_target.main_texture(); + eprintln!("update_region: writing to texture {:p} at ({}, {}) size {}x{}", texture as *const _, x, y, width, height); + let bytes_per_row = width * px_size; + + render_queue.write_texture( + TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: Origin3d { x, y, z: 0 }, + aspect: Default::default(), + }, + &data, + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(bytes_per_row.try_into().unwrap()), + rows_per_image: None, + }, + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + + Ok(()) + } + + let graphics = world + .get::(entity) + .ok_or(ProcessingError::GraphicsNotFound)?; + let px_size = pixel_size(graphics.texture_format)? as u32; + let data = pixels_to_bytes(pixels, graphics.texture_format)?; + + world + .run_system_cached_with( + update_region_inner, + (entity, x, y, width, height, data, px_size), + ) + .expect("Failed to run update_region system") +} + +pub fn update(world: &mut World, entity: Entity, pixels: &[LinearRgba]) -> Result<()> { + let size = world + .get::(entity) + .ok_or(ProcessingError::GraphicsNotFound)? + .size; + update_region(world, entity, 0, 0, size.width, size.height, pixels) +} + +#[derive(Resource, Debug, Clone, Reflect)] +pub struct RenderLayersManager { + used: RenderLayers, + next_free: usize, +} + +impl Default for RenderLayersManager { + fn default() -> Self { + RenderLayersManager { + used: RenderLayers::none(), + next_free: 1, + } + } +} + +impl RenderLayersManager { + pub fn allocate(&mut self) -> RenderLayers { + let layer = self.next_free; + if layer >= Self::max_layer() { + // if the user is hitting this limit, they are probably doing something wrong + // as this is a very large number of layers that would likely cause serious + // performance issues long before reaching this point + panic!( + "Exceeded maximum number of render layers, this should not happen. If you are seeing this message, please report a bug." + ); + } + + self.used = self.used.clone().with(layer); + + self.next_free = (layer + 1..Self::max_layer()) + .find(|&l| !self.is_used(l)) + .unwrap_or(Self::max_layer()); + + RenderLayers::none().with(layer) + } + + pub fn free(&mut self, layers: RenderLayers) { + for layer in layers.iter() { + if layer == 0 { + continue; + } + self.used = self.used.clone().without(layer); + if layer < self.next_free { + self.next_free = layer; + } + } + } + + pub fn is_used(&self, layer: usize) -> bool { + let single = RenderLayers::none().with(layer); + self.used.intersects(&single) + } + + const fn max_layer() -> usize { + // an arbitrary limit, in theory we could keep going forever but + // if we reach this point something is probably wrong + 4096 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_processing_projection() { + let proj = ProcessingProjection { + width: 800.0, + height: 600.0, + near: 0.1, + far: 1000.0, + }; + let clip_matrix = proj.get_clip_from_view(); + // Check some values in the matrix to ensure it's correct + assert_eq!(clip_matrix.w_axis.z, -2.0 / (1000.0 - 0.1)); + } + + #[test] + fn test_layer_reservation() { + let mut manager = RenderLayersManager::default(); + let layer1 = manager.allocate(); + let layer1_clone = layer1.clone(); + let layer2 = manager.allocate(); + assert_ne!(layer1, layer2); + manager.free(layer1); + let layer3 = manager.allocate(); + assert_eq!(layer1_clone, layer3); + } +} diff --git a/crates/processing_render/src/image.rs b/crates/processing_render/src/image.rs index e0c2335..b9a3b67 100644 --- a/crates/processing_render/src/image.rs +++ b/crates/processing_render/src/image.rs @@ -1,3 +1,6 @@ +//! An image in Processing is a 2D texture that can be used for rendering. +//! +//! It can be created from raw pixel data, loaded from disk, resized, and read back to CPU memory. use std::path::PathBuf; use bevy::{ @@ -11,8 +14,8 @@ use bevy::{ render_asset::{AssetExtractionSystems, RenderAssets}, render_resource::{ Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, MapMode, - PollType, TexelCopyBufferInfo, TexelCopyBufferLayout, Texture, TextureDimension, - TextureFormat, + Origin3d, PollType, TexelCopyBufferInfo, TexelCopyBufferLayout, TexelCopyTextureInfo, + Texture, TextureDimension, TextureFormat, }, renderer::{RenderDevice, RenderQueue}, texture::GpuImage, @@ -22,32 +25,35 @@ use half::f16; use crate::error::{ProcessingError, Result}; -pub struct PImagePlugin; +pub struct ImagePlugin; -impl Plugin for PImagePlugin { +impl Plugin for ImagePlugin { fn build(&self, app: &mut App) { - app.init_resource::(); + app.init_resource::(); let render_app = app.sub_app_mut(bevy::render::RenderApp); render_app.add_systems(ExtractSchedule, sync_textures.after(AssetExtractionSystems)); } } +// In Bevy, `Image` is a `RenderResource`, which means its descriptor is stored in the main world +// but its GPU texture is stored in the render world. To avoid tedious lookups or the need to +// explicitly reference the render world, we store a mapping of `PImage` entities to their +// corresponding GPU `Texture` in the main world. This is as bit hacky, but it simplifies the API. #[derive(Resource, Deref, DerefMut, Default)] -struct PImageTextures(EntityHashMap); +pub struct ImageTextures(EntityHashMap); #[derive(Component)] -pub struct PImage { - pub handle: Handle, +pub struct Image { + pub handle: Handle, readback_buffer: Buffer, - pixel_size: usize, - texture_format: TextureFormat, - size: Extent3d, + pub texture_format: TextureFormat, + pub 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>(); + main_world.resource_scope(|world, mut p_image_textures: Mut| { + let mut p_images = world.query_filtered::<(Entity, &Image), 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()); @@ -62,13 +68,13 @@ pub fn create( data: Vec, texture_format: TextureFormat, ) -> Entity { - fn new_inner( + fn create_inner( In((size, data, texture_format)): In<(Extent3d, Vec, TextureFormat)>, mut commands: Commands, - mut images: ResMut>, + mut images: ResMut>, render_device: Res, ) -> Entity { - let image = Image::new( + let image = bevy::image::Image::new( size, TextureDimension::D2, data, @@ -77,41 +83,35 @@ pub fn create( ); let handle = images.add(image); + let readback_buffer = create_readback_buffer( + &render_device, + size.width, + size.height, + texture_format, + "Image Readback Buffer", + ) + .expect("Failed to create readback buffer"); - 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 { + .spawn((Image { 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") + .run_system_cached_with(create_inner, (size, data, texture_format)) + .unwrap() } -pub fn load_start(world: &mut World, path: PathBuf) -> Handle { +pub fn load_start(world: &mut World, path: PathBuf) -> Handle { world.get_asset_server().load(path) } -pub fn is_loaded(world: &World, handle: &Handle) -> bool { +pub fn is_loaded(world: &World, handle: &Handle) -> bool { matches!( world.get_asset_server().load_state(handle), LoadState::Loaded @@ -119,34 +119,30 @@ pub fn is_loaded(world: &World, handle: &Handle) -> bool { } #[cfg(target_arch = "wasm32")] -pub fn from_handle(world: &mut World, handle: Handle) -> Result { - fn from_handle_inner(In(handle): In>, world: &mut World) -> Result { - let images = world.resource::>(); +pub fn from_handle(world: &mut World, handle: Handle) -> Result { + fn from_handle_inner( + In(handle): In>, + world: &mut World, + ) -> Result { + let images = world.resource::>(); let image = images.get(&handle).ok_or(ProcessingError::ImageNotFound)?; let size = image.texture_descriptor.size; let texture_format = image.texture_descriptor.format; - let 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, - }); + let readback_buffer = create_readback_buffer( + render_device, + size.width, + size.height, + texture_format, + "Image Readback Buffer", + )?; Ok(world - .spawn(PImage { + .spawn(Image { handle: handle.clone(), readback_buffer, - pixel_size, texture_format, size, }) @@ -160,37 +156,29 @@ pub fn from_handle(world: &mut World, handle: Handle) -> Result { 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); + let handle: Handle = world.get_asset_server().load(path); while let LoadState::Loading = world.get_asset_server().load_state(&handle) { - world - .run_system_once(handle_internal_asset_events) - .expect("Failed to run internal asset events system"); + world.run_system_once(handle_internal_asset_events).unwrap(); } - let images = world.resource::>(); + 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, - }); + let readback_buffer = create_readback_buffer( + render_device, + size.width, + size.height, + texture_format, + "Image Readback Buffer", + )?; + Ok(world - .spawn(PImage { + .spawn(Image { handle: handle.clone(), readback_buffer, - pixel_size, texture_format, size, }) @@ -205,26 +193,27 @@ pub fn load(world: &mut World, path: PathBuf) -> Result { 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>, + mut p_images: Query<&mut Image>, + mut images: ResMut>, render_device: Res, ) -> Result<()> { - let mut image = p_images + let mut p_image = p_images .get_mut(entity) .map_err(|_| ProcessingError::ImageNotFound)?; images - .get_mut(&image.handle) + .get_mut(&p_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, - }); + p_image.readback_buffer = create_readback_buffer( + &render_device, + new_size.width, + new_size.height, + p_image.texture_format, + "Image Readback Buffer", + )?; + p_image.size = new_size; Ok(()) } @@ -234,12 +223,12 @@ pub fn resize(world: &mut World, entity: Entity, new_size: Extent3d) -> Result<( .expect("Failed to run resize system") } -pub fn load_pixels(world: &mut World, entity: Entity) -> Result> { +pub fn readback(world: &mut World, entity: Entity) -> Result> { fn readback_inner( In(entity): In, - p_images: Query<&PImage>, - p_image_textures: Res, - mut images: ResMut>, + p_images: Query<&Image>, + p_image_textures: Res, + mut images: ResMut>, render_device: Res, render_queue: ResMut, ) -> Result> { @@ -253,12 +242,9 @@ pub fn load_pixels(world: &mut World, entity: Entity) -> Result> 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, - ); + let px_size = pixel_size(p_image.texture_format)?; + let padded_bytes_per_row = + RenderDevice::align_copy_bytes_per_row(p_image.size.width as usize * px_size); encoder.copy_texture_to_buffer( texture.as_image_copy(), @@ -303,35 +289,13 @@ pub fn load_pixels(world: &mut World, entity: Entity) -> Result> 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) + bytes_to_pixels( + &data, + p_image.texture_format, + p_image.size.width, + p_image.size.height, + padded_bytes_per_row, + ) } world @@ -339,12 +303,109 @@ pub fn load_pixels(world: &mut World, entity: Entity) -> Result> .expect("Failed to run readback system") } +pub fn update_region( + world: &mut World, + entity: Entity, + x: u32, + y: u32, + width: u32, + height: u32, + pixels: &[LinearRgba], +) -> Result<()> { + let expected_count = (width * height) as usize; + if pixels.len() != expected_count { + return Err(ProcessingError::InvalidArgument(format!( + "Expected {} pixels for {}x{} region, got {}", + expected_count, + width, + height, + pixels.len() + ))); + } + + fn update_region_inner( + In((entity, x, y, width, height, data, px_size)): In<( + Entity, + u32, + u32, + u32, + u32, + Vec, + u32, + )>, + p_images: Query<&Image>, + p_image_textures: Res, + render_queue: Res, + ) -> Result<()> { + let p_image = p_images + .get(entity) + .map_err(|_| ProcessingError::ImageNotFound)?; + + if x + width > p_image.size.width || y + height > p_image.size.height { + return Err(ProcessingError::InvalidArgument(format!( + "Region ({}, {}, {}, {}) exceeds image bounds ({}, {})", + x, y, width, height, p_image.size.width, p_image.size.height + ))); + } + + let texture = p_image_textures + .get(&entity) + .ok_or(ProcessingError::ImageNotFound)?; + + let bytes_per_row = width * px_size; + + render_queue.write_texture( + TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: Origin3d { x, y, z: 0 }, + aspect: Default::default(), + }, + &data, + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(bytes_per_row.try_into().unwrap()), + rows_per_image: None, + }, + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + + Ok(()) + } + + let p_image = world + .get::(entity) + .ok_or(ProcessingError::ImageNotFound)?; + let px_size = pixel_size(p_image.texture_format)? as u32; + let data = pixels_to_bytes(pixels, p_image.texture_format)?; + + world + .run_system_cached_with( + update_region_inner, + (entity, x, y, width, height, data, px_size), + ) + .expect("Failed to run update_region system") +} + +pub fn update(world: &mut World, entity: Entity, pixels: &[LinearRgba]) -> Result<()> { + let size = world + .get::(entity) + .ok_or(ProcessingError::ImageNotFound)? + .size; + update_region(world, entity, 0, 0, size.width, size.height, pixels) +} + pub fn destroy(world: &mut World, entity: Entity) -> Result<()> { fn destroy_inner( In(entity): In, - mut p_images: Query<&mut PImage>, - mut images: ResMut>, - mut p_image_textures: ResMut, + mut commands: Commands, + mut p_images: Query<&mut Image>, + mut images: ResMut>, + mut p_image_textures: ResMut, ) -> Result<()> { let p_image = p_images .get_mut(entity) @@ -352,7 +413,7 @@ pub fn destroy(world: &mut World, entity: Entity) -> Result<()> { images.remove(&p_image.handle); p_image_textures.remove(&entity); - + commands.entity(entity).despawn(); Ok(()) } @@ -360,3 +421,126 @@ pub fn destroy(world: &mut World, entity: Entity) -> Result<()> { .run_system_cached_with(destroy_inner, entity) .expect("Failed to run destroy system") } + +/// Get the size in bytes of a single pixel for the given texture format. +pub fn pixel_size(format: TextureFormat) -> Result { + match format { + TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb => Ok(4), + TextureFormat::Rgba16Float => Ok(8), + TextureFormat::Rgba32Float => Ok(16), + _ => Err(ProcessingError::UnsupportedTextureFormat), + } +} + +/// Convert LinearRgba pixels to raw bytes in the specified texture format. +pub fn pixels_to_bytes(pixels: &[LinearRgba], format: TextureFormat) -> Result> { + match format { + TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb => { + Ok(pixels.iter().flat_map(|p| p.to_u8_array()).collect()) + } + TextureFormat::Rgba16Float => Ok(pixels + .iter() + .flat_map(|p| { + let [r, g, b, a] = p.to_f32_array(); + [ + f16::from_f32(r).to_bits().to_le_bytes(), + f16::from_f32(g).to_bits().to_le_bytes(), + f16::from_f32(b).to_bits().to_le_bytes(), + f16::from_f32(a).to_bits().to_le_bytes(), + ] + .into_iter() + .flatten() + }) + .collect()), + TextureFormat::Rgba32Float => Ok(pixels + .iter() + .flat_map(|p| { + let [r, g, b, a] = p.to_f32_array(); + [ + r.to_le_bytes(), + g.to_le_bytes(), + b.to_le_bytes(), + a.to_le_bytes(), + ] + .into_iter() + .flatten() + }) + .collect()), + _ => Err(ProcessingError::UnsupportedTextureFormat), + } +} + +/// Convert raw bytes to LinearRgba pixels based on the texture format. +/// Handles row padding, data should come from a GPU texture readback with proper alignment. +pub fn bytes_to_pixels( + data: &[u8], + format: TextureFormat, + width: u32, + height: u32, + padded_bytes_per_row: usize, +) -> Result> { + let px_size = pixel_size(format)?; + let bytes_per_row = width as usize * px_size; + + let pixels = match format { + TextureFormat::Rgba8Unorm | TextureFormat::Rgba8UnormSrgb => data + .chunks_exact(padded_bytes_per_row) + .take(height as usize) + .flat_map(|row| { + row[..bytes_per_row].chunks_exact(px_size).map(|chunk| { + LinearRgba::from_u8_array([chunk[0], chunk[1], chunk[2], chunk[3]]) + }) + }) + .collect(), + TextureFormat::Rgba16Float => data + .chunks_exact(padded_bytes_per_row) + .take(height as usize) + .flat_map(|row| { + row[..bytes_per_row].chunks_exact(px_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(padded_bytes_per_row) + .take(height as usize) + .flat_map(|row| { + row[..bytes_per_row].chunks_exact(px_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(), + // TODO: Handle more formats as needed + _ => return Err(ProcessingError::UnsupportedTextureFormat), + }; + + Ok(pixels) +} + +/// Create a readback buffer for the given texture dimensions and format. +pub fn create_readback_buffer( + render_device: &RenderDevice, + width: u32, + height: u32, + format: TextureFormat, + label: &str, +) -> Result { + let px_size = pixel_size(format)?; + let padded_bytes_per_row = RenderDevice::align_copy_bytes_per_row(width as usize * px_size); + let buffer_size = padded_bytes_per_row as u64 * height as u64; + + Ok(render_device.create_buffer(&BufferDescriptor { + label: Some(label), + size: buffer_size, + usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ, + mapped_at_creation: false, + })) +} diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 16ad051..8ca2202 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -1,32 +1,26 @@ pub mod error; +mod graphics; pub mod image; pub mod render; - -use std::{cell::RefCell, num::NonZero, path::PathBuf, ptr::NonNull, sync::OnceLock}; +mod surface; #[cfg(any(target_os = "linux", target_arch = "wasm32"))] use std::ffi::c_void; +use std::{cell::RefCell, num::NonZero, path::PathBuf, sync::OnceLock}; use bevy::{ app::{App, AppExit}, asset::AssetEventSystems, - camera::{CameraOutputMode, CameraProjection, RenderTarget, visibility::RenderLayers}, log::tracing_subscriber, - math::Vec3A, prelude::*, render::render_resource::{Extent3d, TextureFormat}, - window::{RawHandleWrapper, Window, WindowRef, WindowResolution, WindowWrapper}, -}; -use raw_window_handle::{ - DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, - RawWindowHandle, WindowHandle, }; use render::{activate_cameras, clear_transient_meshes, flush_draw_commands}; use tracing::debug; use crate::{ - error::Result, - render::command::{CommandBuffer, DrawCommand}, + graphics::GraphicsPlugin, image::ImagePlugin, render::command::DrawCommand, + surface::SurfacePlugin, }; static IS_INIT: OnceLock<()> = OnceLock::new(); @@ -35,87 +29,10 @@ thread_local! { static APP: RefCell> = const { RefCell::new(None) }; } -#[derive(Resource, Default)] -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)] -#[reflect(Default)] -pub struct ProcessingProjection { - pub width: f32, - pub height: f32, - pub near: f32, - pub far: f32, -} - -impl Default for ProcessingProjection { - fn default() -> Self { - Self { - width: 1.0, - height: 1.0, - near: 0.0, - far: 1000.0, - } - } -} - -impl CameraProjection for ProcessingProjection { - fn get_clip_from_view(&self) -> Mat4 { - Mat4::orthographic_rh( - 0.0, - self.width, - self.height, // bottom = height - 0.0, // top = 0 - self.near, - self.far, - ) - } - - fn get_clip_from_view_for_sub(&self, _sub_view: &bevy::camera::SubCameraView) -> Mat4 { - // TODO: implement sub-view support if needed (probably not) - self.get_clip_from_view() - } - - fn update(&mut self, width: f32, height: f32) { - self.width = width; - self.height = height; - } - - fn far(&self) -> f32 { - self.far - } - - fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] { - // order: bottom-right, top-right, top-left, bottom-left for near, then far - let near_center = Vec3A::new(self.width / 2.0, self.height / 2.0, z_near); - let far_center = Vec3A::new(self.width / 2.0, self.height / 2.0, z_far); - - let half_width = self.width / 2.0; - let half_height = self.height / 2.0; - - [ - // near plane - near_center + Vec3A::new(half_width, half_height, 0.0), // bottom-right - near_center + Vec3A::new(half_width, -half_height, 0.0), // top-right - near_center + Vec3A::new(-half_width, -half_height, 0.0), // top-left - near_center + Vec3A::new(-half_width, half_height, 0.0), // bottom-left - // far plane - far_center + Vec3A::new(half_width, half_height, 0.0), // bottom-right - far_center + Vec3A::new(half_width, -half_height, 0.0), // top-right - far_center + Vec3A::new(-half_width, -half_height, 0.0), // top-left - far_center + Vec3A::new(-half_width, half_height, 0.0), // bottom-left - ] - } -} - -fn app_mut(cb: impl FnOnce(&mut App) -> Result) -> Result { +fn app_mut(cb: impl FnOnce(&mut App) -> error::Result) -> error::Result { let res = APP.with(|app_cell| { let mut app_borrow = app_cell.borrow_mut(); let app = app_borrow @@ -126,37 +43,6 @@ fn app_mut(cb: impl FnOnce(&mut App) -> Result) -> Result { Ok(res) } -struct GlfwWindow { - window_handle: RawWindowHandle, - display_handle: RawDisplayHandle, -} - -// SAFETY: -// - RawWindowHandle and RawDisplayHandle are just pointers -// - The actual window is managed by Java and outlives this struct -// - GLFW is thread-safe-ish, see https://www.glfw.org/faq#29---is-glfw-thread-safe -// -// Note: we enforce that all calls to init/update/exit happen on the main thread, so -// there should be no concurrent access to the window from multiple threads anyway. -unsafe impl Send for GlfwWindow {} -unsafe impl Sync for GlfwWindow {} - -impl HasWindowHandle for GlfwWindow { - fn window_handle(&self) -> core::result::Result, HandleError> { - // SAFETY: - // - Handles passed from Java are valid - Ok(unsafe { WindowHandle::borrow_raw(self.window_handle) }) - } -} - -impl HasDisplayHandle for GlfwWindow { - fn display_handle(&self) -> core::result::Result, HandleError> { - // SAFETY: - // - Handles passed from Java are valid - Ok(unsafe { DisplayHandle::borrow_raw(self.display_handle) }) - } -} - /// Create a WebGPU surface from a native window handle. /// /// Currently, this just creates a bevy window with the given parameters and @@ -168,188 +54,41 @@ pub fn surface_create( width: u32, height: u32, scale_factor: f32, -) -> Result { - #[cfg(target_os = "macos")] - let (raw_window_handle, raw_display_handle) = { - use raw_window_handle::{AppKitDisplayHandle, AppKitWindowHandle}; - - // GLFW gives us NSWindow*, but AppKitWindowHandle needs NSView* - // so we have to do some objc magic to grab the right pointer - let ns_view_ptr = { - use objc2::rc::Retained; - use objc2_app_kit::{NSView, NSWindow}; - - // SAFETY: - // - window_handle is a valid NSWindow pointer from the GLFW window - let ns_window = window_handle as *mut NSWindow; - if ns_window.is_null() { - return Err(error::ProcessingError::InvalidWindowHandle); - } - - // SAFETY: - // - The contentView is owned by NSWindow and remains valid as long as the window exists - let ns_window_ref = unsafe { &*ns_window }; - let content_view: Option> = ns_window_ref.contentView(); - - match content_view { - Some(view) => Retained::as_ptr(&view) as *mut std::ffi::c_void, - None => { - return Err(error::ProcessingError::InvalidWindowHandle); - } - } - }; - - let window = AppKitWindowHandle::new(NonNull::new(ns_view_ptr).unwrap()); - let display = AppKitDisplayHandle::new(); - ( - RawWindowHandle::AppKit(window), - RawDisplayHandle::AppKit(display), - ) - }; - - #[cfg(target_os = "windows")] - let (raw_window_handle, raw_display_handle) = { - use std::num::NonZeroIsize; - - use raw_window_handle::{Win32WindowHandle, WindowsDisplayHandle}; - use windows::Win32::{Foundation::HINSTANCE, System::LibraryLoader::GetModuleHandleW}; - - if window_handle == 0 { - return Err(error::ProcessingError::InvalidWindowHandle); - } - - // HWND is isize, so cast it - let hwnd_isize = window_handle as isize; - let hwnd_nonzero = match NonZeroIsize::new(hwnd_isize) { - Some(nz) => nz, - None => return Err(error::ProcessingError::InvalidWindowHandle), - }; - - let mut window = Win32WindowHandle::new(hwnd_nonzero); - - // VK_KHR_win32_surface requires hinstance *and* hwnd - // SAFETY: GetModuleHandleW(NULL) is safe - let hinstance = unsafe { GetModuleHandleW(None) } - .map_err(|_| error::ProcessingError::InvalidWindowHandle)?; - - let hinstance_nonzero = NonZeroIsize::new(hinstance.0 as isize) - .ok_or(error::ProcessingError::InvalidWindowHandle)?; - window.hinstance = Some(hinstance_nonzero); - - let display = WindowsDisplayHandle::new(); - - ( - RawWindowHandle::Win32(window), - RawDisplayHandle::Windows(display), - ) - }; - - #[cfg(target_os = "linux")] - let (raw_window_handle, raw_display_handle) = { - use raw_window_handle::{WaylandDisplayHandle, WaylandWindowHandle}; - - if window_handle == 0 { - return Err(error::ProcessingError::HandleError( - HandleError::Unavailable, - )); - } - let window_handle_ptr = NonNull::new(window_handle as *mut c_void).unwrap(); - let window = WaylandWindowHandle::new(window_handle_ptr); - - if display_handle == 0 { - return Err(error::ProcessingError::HandleError( - HandleError::Unavailable, - )); - } - let display_handle_ptr = NonNull::new(display_handle as *mut c_void).unwrap(); - let display = WaylandDisplayHandle::new(display_handle_ptr); - - ( - RawWindowHandle::Wayland(window), - RawDisplayHandle::Wayland(display), - ) - }; - - #[cfg(target_arch = "wasm32")] - let (raw_window_handle, raw_display_handle) = { - use raw_window_handle::{WebCanvasWindowHandle, WebDisplayHandle}; - - // window_handle is a pointer to HtmlCanvasElement DOM obj - let canvas_ptr = window_handle as *mut c_void; - let canvas = NonNull::new(canvas_ptr).ok_or(error::ProcessingError::InvalidWindowHandle)?; - - let window = WebCanvasWindowHandle::new(canvas); - let display = WebDisplayHandle::new(); - ( - RawWindowHandle::WebCanvas(window), - RawDisplayHandle::Web(display), +) -> error::Result { + app_mut(|app| { + surface::create( + app.world_mut(), + window_handle, + display_handle, + width, + height, + scale_factor, ) - }; - - let glfw_window = GlfwWindow { - window_handle: raw_window_handle, - display_handle: raw_display_handle, - }; - - let window_wrapper = WindowWrapper::new(glfw_window); - let handle_wrapper = RawHandleWrapper::new(&window_wrapper)?; - - let entity_id = app_mut(|app| { - let mut window_count = app.world_mut().resource_mut::(); - let count = window_count.0; - window_count.0 += 1; - let render_layer = RenderLayers::none().with(count as usize); - - let mut window = app.world_mut().spawn(( - Window { - resolution: WindowResolution::new(width, height) - .with_scale_factor_override(scale_factor), - ..default() - }, - handle_wrapper, - CommandBuffer::default(), - // 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(); - window.with_children(|parent| { - // processing has a different coordinate system for 2d rendering: - // - origin at top-left - // - x increases to the right, y increases downward - // - coordinate units are in screen pixels - parent.spawn(( - Camera3d::default(), - Camera { - target: RenderTarget::Window(WindowRef::Entity(window_entity)), - ..default() - }, - Projection::custom(ProcessingProjection { - width: width as f32, - height: height as f32, - near: 0.0, - far: 1000.0, - }), - Transform::from_xyz(0.0, 0.0, 999.9), - render_layer, - )); - }); - - Ok(window_entity) - })?; + }) +} - Ok(entity_id) +pub fn surface_create_offscreen( + width: u32, + height: u32, + scale_factor: f32, + texture_format: TextureFormat, +) -> error::Result { + app_mut(|app| { + surface::create_offscreen(app.world_mut(), width, height, scale_factor, texture_format) + }) } /// Create a WebGPU surface from a canvas element ID #[cfg(target_arch = "wasm32")] -pub fn surface_create_from_canvas(canvas_id: &str, width: u32, height: u32) -> Result { +pub fn surface_create_from_canvas( + canvas_id: &str, + width: u32, + height: u32, +) -> error::Result { use wasm_bindgen::JsCast; use web_sys::HtmlCanvasElement; - // find the canvas elelment + // find the canvas element let web_window = web_sys::window().ok_or(error::ProcessingError::InvalidWindowHandle)?; let document = web_window .document() @@ -371,30 +110,13 @@ pub fn surface_create_from_canvas(canvas_id: &str, width: u32, height: u32) -> R surface_create(canvas_ptr, 0, width, height, scale_factor) } -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); - let mut window_count = app.world_mut().resource_mut::(); - window_count.0 = window_count.0.saturating_sub(1); - } - Ok(()) - }) +pub fn surface_destroy(graphics_entity: Entity) -> error::Result<()> { + app_mut(|app| surface::destroy(app.world_mut(), graphics_entity)) } /// Update window size when resized. -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); - } else { - return Err(error::ProcessingError::WindowNotFound); - }; - app.world_mut() - .entity_mut(window_entity) - .insert(SurfaceSize(width, height)); - Ok(()) - }) +pub fn surface_resize(graphics_entity: Entity, width: u32, height: u32) -> error::Result<()> { + app_mut(|app| surface::resize(app.world_mut(), graphics_entity, width, height)) } fn create_app() -> App { @@ -425,14 +147,14 @@ fn create_app() -> App { }); app.add_plugins(plugins); - app.init_resource::(); + app.add_plugins((ImagePlugin, GraphicsPlugin, SurfacePlugin)); app.add_systems(First, (clear_transient_meshes, activate_cameras)) .add_systems(Update, flush_draw_commands.before(AssetEventSystems)); app } -fn is_already_init() -> Result { +fn is_already_init() -> error::Result { let is_init = IS_INIT.get().is_some(); let thread_has_app = APP.with(|app_cell| app_cell.borrow().is_some()); if is_init && !thread_has_app { @@ -455,7 +177,7 @@ fn set_app(app: App) { /// Initialize the app, if not already initialized. Must be called from the main thread and cannot /// be called concurrently from multiple threads. #[cfg(not(target_arch = "wasm32"))] -pub fn init() -> Result<()> { +pub fn init() -> error::Result<()> { setup_tracing()?; if is_already_init()? { return Ok(()); @@ -471,7 +193,7 @@ pub fn init() -> Result<()> { /// Initialize the app asynchronously #[cfg(target_arch = "wasm32")] -pub async fn init() -> Result<()> { +pub async fn init() -> error::Result<()> { use bevy::app::PluginsState; setup_tracing()?; @@ -501,67 +223,67 @@ pub async fn init() -> Result<()> { Ok(()) } -macro_rules! camera_mut { - ($app:expr, $window_entity:expr) => { - $app.world_mut() - .query::<(&mut Camera, &ChildOf)>() - .iter_mut(&mut $app.world_mut()) - .filter_map(|(camera, parent)| { - if parent.parent() == $window_entity { - Some(camera) - } else { - None - } - }) - .next() - .ok_or_else(|| error::ProcessingError::WindowNotFound)? - }; +/// Create a new graphics surface for rendering. +pub fn graphics_create(surface_entity: Entity, width: u32, height: u32) -> error::Result { + app_mut(|app| graphics::create(app.world_mut(), surface_entity, width, height)) } -macro_rules! window_mut { - ($app:expr, $window_entity:expr) => { - $app.world_mut() - .get_entity_mut($window_entity) - .map_err(|_| error::ProcessingError::WindowNotFound)? - }; +/// Begin a new draw pass for the graphics surface. +pub fn graphics_begin_draw(_graphics_entity: Entity) -> error::Result<()> { + app_mut(|app| graphics::begin_draw(app, _graphics_entity)) } -pub fn begin_draw(_window_entity: Entity) -> Result<()> { - app_mut(|_app| Ok(())) +/// Flush current pending draw commands to the graphics surface. +pub fn graphics_flush(graphics_entity: Entity) -> error::Result<()> { + app_mut(|app| graphics::flush(app, graphics_entity)) } -pub fn flush(window_entity: Entity) -> Result<()> { - app_mut(|app| { - window_mut!(app, window_entity).insert(Flush); - app.update(); - window_mut!(app, window_entity).remove::(); +/// End the current draw pass for the graphics surface. +pub fn graphics_end_draw(graphics_entity: Entity) -> error::Result<()> { + app_mut(|app| graphics::end_draw(app, graphics_entity)) +} - // ensure that the intermediate texture is not cleared - camera_mut!(app, window_entity).clear_color = ClearColorConfig::None; - Ok(()) - }) +/// Destroy the graphics surface and free its resources. +pub fn graphics_destroy(graphics_entity: Entity) -> error::Result<()> { + app_mut(|app| graphics::destroy(app.world_mut(), graphics_entity)) } -pub fn end_draw(window_entity: Entity) -> Result<()> { - // since we are ending the draw, set the camera to write to the output render target +/// Read back pixel data from the graphics surface. +pub fn graphics_readback(graphics_entity: Entity) -> error::Result> { app_mut(|app| { - camera_mut!(app, window_entity).output_mode = CameraOutputMode::Write { - blend_state: None, - clear_color: ClearColorConfig::Default, - }; - Ok(()) - })?; - // flush any remaining draw commands, this ensures that the frame is presented even if there - // is no remaining draw commands - flush(window_entity)?; - // reset to skipping output for the next frame + graphics::flush(app, graphics_entity)?; + graphics::readback(app.world_mut(), graphics_entity) + }) +} + +/// Update the graphics surface with new pixel data. +pub fn graphics_update(graphics_entity: Entity, pixels: &[LinearRgba]) -> error::Result<()> { + app_mut(|app| graphics::update(app.world_mut(), graphics_entity, pixels)) +} + +/// Update a region of the graphics surface with new pixel data. +pub fn graphics_update_region( + graphics_entity: Entity, + x: u32, + y: u32, + width: u32, + height: u32, + pixels: &[LinearRgba], +) -> error::Result<()> { app_mut(|app| { - camera_mut!(app, window_entity).output_mode = CameraOutputMode::Skip; - Ok(()) + graphics::update_region( + app.world_mut(), + graphics_entity, + x, + y, + width, + height, + pixels, + ) }) } -pub fn exit(exit_code: u8) -> Result<()> { +pub fn exit(exit_code: u8) -> error::Result<()> { app_mut(|app| { app.world_mut().write_message(match exit_code { 0 => AppExit::Success, @@ -573,7 +295,7 @@ pub fn exit(exit_code: u8) -> Result<()> { Ok(()) })?; - // we need to drop the app in a deterministic manner to ensure resourcse are cleaned up + // we need to drop the app in a deterministic manner to ensure resources are cleaned up // otherwise we'll get wgpu graphics backend errors on exit APP.with(|app_cell| { let app = app_cell.borrow_mut().take(); @@ -583,19 +305,7 @@ pub fn exit(exit_code: u8) -> Result<()> { Ok(()) } -pub fn background_color(window_entity: Entity, color: Color) -> Result<()> { - app_mut(|app| { - let mut camera_query = app.world_mut().query::<(&mut Camera, &ChildOf)>(); - for (mut camera, parent) in camera_query.iter_mut(app.world_mut()) { - if parent.parent() == window_entity { - camera.clear_color = ClearColorConfig::Custom(color); - } - } - Ok(()) - }) -} - -fn setup_tracing() -> Result<()> { +fn setup_tracing() -> error::Result<()> { // TODO: figure out wasm compatible tracing subscriber #[cfg(not(target_arch = "wasm32"))] { @@ -606,15 +316,8 @@ fn setup_tracing() -> Result<()> { } /// Record a drawing command for a window -pub fn record_command(window_entity: Entity, cmd: DrawCommand) -> Result<()> { - app_mut(|app| { - let mut entity_mut = app.world_mut().entity_mut(window_entity); - if let Some(mut buffer) = entity_mut.get_mut::() { - buffer.push(cmd); - } - - Ok(()) - }) +pub fn graphics_record_command(graphics_entity: Entity, cmd: DrawCommand) -> error::Result<()> { + app_mut(|app| graphics::record_command(app.world_mut(), graphics_entity, cmd)) } /// Create a new image with given size and data. @@ -622,18 +325,19 @@ pub fn image_create( size: Extent3d, data: Vec, texture_format: TextureFormat, -) -> Result { +) -> error::Result { app_mut(|app| Ok(image::create(app.world_mut(), size, data, texture_format))) } +/// Load an image from disk. #[cfg(not(target_arch = "wasm32"))] -pub fn image_load(path: &str) -> Result { +pub fn image_load(path: &str) -> error::Result { let path = PathBuf::from(path); app_mut(|app| image::load(app.world_mut(), path)) } #[cfg(target_arch = "wasm32")] -pub async fn image_load(path: &str) -> Result { +pub async fn image_load(path: &str) -> error::Result { use bevy::prelude::{Handle, Image}; let path = PathBuf::from(path); @@ -668,16 +372,33 @@ pub async fn image_load(path: &str) -> Result { } /// Resize an existing image to new size. -pub fn image_resize(entity: Entity, new_size: Extent3d) -> Result<()> { +pub fn image_resize(entity: Entity, new_size: Extent3d) -> error::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)) +pub fn image_readback(entity: Entity) -> error::Result> { + app_mut(|app| image::readback(app.world_mut(), entity)) +} + +/// Update an existing image with new pixel data. +pub fn image_update(entity: Entity, pixels: &[LinearRgba]) -> error::Result<()> { + app_mut(|app| image::update(app.world_mut(), entity, pixels)) +} + +/// Update a region of an existing image with new pixel data. +pub fn image_update_region( + entity: Entity, + x: u32, + y: u32, + width: u32, + height: u32, + pixels: &[LinearRgba], +) -> error::Result<()> { + app_mut(|app| image::update_region(app.world_mut(), entity, x, y, width, height, pixels)) } /// Destroy an existing image and free its resources. -pub fn image_destroy(entity: Entity) -> Result<()> { +pub fn image_destroy(entity: Entity) -> error::Result<()> { app_mut(|app| image::destroy(app.world_mut(), entity)) } diff --git a/crates/processing_render/src/render/mod.rs b/crates/processing_render/src/render/mod.rs index 4f95c09..4687b14 100644 --- a/crates/processing_render/src/render/mod.rs +++ b/crates/processing_render/src/render/mod.rs @@ -8,14 +8,14 @@ use command::{CommandBuffer, DrawCommand}; use material::MaterialKey; use primitive::{TessellationMode, empty_mesh}; -use crate::{Flush, SurfaceSize, image::PImage, render::primitive::rect}; +use crate::{Flush, graphics::SurfaceSize, image::Image, render::primitive::rect}; #[derive(Component)] #[relationship(relationship_target = TransientMeshes)] -pub struct BelongsToSurface(pub Entity); +pub struct BelongsToGraphics(pub Entity); #[derive(Component, Default)] -#[relationship_target(relationship = BelongsToSurface)] +#[relationship_target(relationship = BelongsToGraphics)] pub struct TransientMeshes(Vec); #[derive(SystemParam)] @@ -33,7 +33,7 @@ struct BatchState { material_key: Option, draw_index: u32, render_layers: RenderLayers, - surface_entity: Option, + graphics_entity: Option, } #[derive(Debug)] @@ -78,15 +78,15 @@ impl RenderState { pub fn flush_draw_commands( mut ctx: RenderContext, - mut surfaces: Query<(Entity, &mut CommandBuffer, &RenderLayers, &SurfaceSize), With>, - p_images: Query<&PImage>, + mut graphics: Query<(Entity, &mut CommandBuffer, &RenderLayers, &SurfaceSize), With>, + p_images: Query<&Image>, ) { - for (surface_entity, mut cmd_buffer, render_layers, SurfaceSize(width, height)) in - surfaces.iter_mut() + for (graphics_entity, mut cmd_buffer, render_layers, SurfaceSize(width, height)) in + graphics.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); + ctx.batch.graphics_entity = Some(graphics_entity); ctx.batch.draw_index = 0; // Reset draw index for each flush for cmd in draw_commands { @@ -179,20 +179,9 @@ pub fn flush_draw_commands( } } -pub fn activate_cameras( - mut cameras: Query<&mut Camera>, - mut surfaces: Query<&Children, With>, -) { - for mut camera in cameras.iter_mut() { - camera.is_active = false; - } - - for children in surfaces.iter_mut() { - for child in children.iter() { - if let Ok(mut camera) = cameras.get_mut(child) { - camera.is_active = true; - } - } +pub fn activate_cameras(mut cameras: Query<(&mut Camera, Option<&Flush>)>) { + for (mut camera, flush) in cameras.iter_mut() { + camera.is_active = flush.is_some(); } } @@ -211,7 +200,7 @@ fn spawn_mesh(ctx: &mut RenderContext, mesh: Mesh, z_offset: f32) { let Some(material_key) = &ctx.batch.material_key else { return; }; - let Some(surface_entity) = ctx.batch.surface_entity else { + let Some(surface_entity) = ctx.batch.graphics_entity else { return; }; @@ -221,7 +210,7 @@ fn spawn_mesh(ctx: &mut RenderContext, mesh: Mesh, z_offset: f32) { ctx.commands.spawn(( Mesh3d(mesh_handle), MeshMaterial3d(material_handle), - BelongsToSurface(surface_entity), + BelongsToGraphics(surface_entity), Transform::from_xyz(0.0, 0.0, z_offset), ctx.batch.render_layers.clone(), )); diff --git a/crates/processing_render/src/surface.rs b/crates/processing_render/src/surface.rs new file mode 100644 index 0000000..b7a4116 --- /dev/null +++ b/crates/processing_render/src/surface.rs @@ -0,0 +1,298 @@ +//! A "surface" in Processing is essentially a window or canvas where graphics are rendered. In +//! typical rendering backends, a surface corresponds to a native window, i.e. a swapchain. However, +//! processing allows for "offscreen" rendering via the `PSurfaceNone` type, which does not have a +//! native window associated with it. This module provides functionality to create and manage both +//! types of surfaces. +//! +//! In Bevy, we can consider a surface to be a [`RenderTarget`], which is either a window or a +//! texture. +use std::ptr::NonNull; + +use bevy::{ + app::{App, Plugin}, + asset::Assets, + ecs::query::QueryEntityError, + prelude::{Commands, Component, Entity, In, Query, ResMut, Window, With, World, default}, + render::render_resource::{Extent3d, TextureFormat}, + window::{RawHandleWrapper, WindowResolution, WindowWrapper}, +}; +use raw_window_handle::{ + DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, + RawWindowHandle, WindowHandle, +}; + +use crate::{ + error, + error::{ProcessingError, Result}, + image::{Image, ImageTextures}, +}; + +#[derive(Component, Debug, Clone)] +pub struct Surface; + +pub struct SurfacePlugin; + +impl Plugin for SurfacePlugin { + fn build(&self, _app: &mut App) {} +} + +struct GlfwWindow { + window_handle: RawWindowHandle, + display_handle: RawDisplayHandle, +} + +// SAFETY: +// - RawWindowHandle and RawDisplayHandle are just pointers +// - The actual window is managed by Java and outlives this struct +// - GLFW is thread-safe-ish, see https://www.glfw.org/faq#29---is-glfw-thread-safe +// +// Note: we enforce that all calls to init/update/exit happen on the main thread, so +// there should be no concurrent access to the window from multiple threads anyway. +unsafe impl Send for GlfwWindow {} +unsafe impl Sync for GlfwWindow {} + +impl HasWindowHandle for GlfwWindow { + fn window_handle(&self) -> core::result::Result, HandleError> { + // SAFETY: + // - Handles passed from Java are valid + Ok(unsafe { WindowHandle::borrow_raw(self.window_handle) }) + } +} + +impl HasDisplayHandle for GlfwWindow { + fn display_handle(&self) -> core::result::Result, HandleError> { + // SAFETY: + // - Handles passed from Java are valid + Ok(unsafe { DisplayHandle::borrow_raw(self.display_handle) }) + } +} + +/// Create a WebGPU surface from a native window handle. +/// +/// 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( + world: &mut World, + window_handle: u64, + display_handle: u64, + width: u32, + height: u32, + scale_factor: f32, +) -> Result { + fn create_inner( + In((window_handle, _display_handle, width, height, scale_factor)): In<( + u64, + u64, + u32, + u32, + f32, + )>, + mut commands: Commands, + ) -> Result { + #[cfg(target_os = "macos")] + let (raw_window_handle, raw_display_handle) = { + use raw_window_handle::{AppKitDisplayHandle, AppKitWindowHandle}; + + // GLFW gives us NSWindow*, but AppKitWindowHandle needs NSView* + // so we have to do some objc magic to grab the right pointer + let ns_view_ptr = { + use objc2::rc::Retained; + use objc2_app_kit::{NSView, NSWindow}; + + // SAFETY: + // - window_handle is a valid NSWindow pointer from the GLFW window + let ns_window = window_handle as *mut NSWindow; + if ns_window.is_null() { + return Err(error::ProcessingError::InvalidWindowHandle); + } + + // SAFETY: + // - The contentView is owned by NSWindow and remains valid as long as the window exists + let ns_window_ref = unsafe { &*ns_window }; + let content_view: Option> = ns_window_ref.contentView(); + + match content_view { + Some(view) => Retained::as_ptr(&view) as *mut std::ffi::c_void, + None => { + return Err(error::ProcessingError::InvalidWindowHandle); + } + } + }; + + let window = AppKitWindowHandle::new(NonNull::new(ns_view_ptr).unwrap()); + let display = AppKitDisplayHandle::new(); + ( + RawWindowHandle::AppKit(window), + RawDisplayHandle::AppKit(display), + ) + }; + + #[cfg(target_os = "windows")] + let (raw_window_handle, raw_display_handle) = { + use std::num::NonZeroIsize; + + use raw_window_handle::{Win32WindowHandle, WindowsDisplayHandle}; + use windows::Win32::{Foundation::HINSTANCE, System::LibraryLoader::GetModuleHandleW}; + + if window_handle == 0 { + return Err(error::ProcessingError::InvalidWindowHandle); + } + + // HWND is isize, so cast it + let hwnd_isize = window_handle as isize; + let hwnd_nonzero = match NonZeroIsize::new(hwnd_isize) { + Some(nz) => nz, + None => return Err(error::ProcessingError::InvalidWindowHandle), + }; + + let mut window = Win32WindowHandle::new(hwnd_nonzero); + + // VK_KHR_win32_surface requires hinstance *and* hwnd + // SAFETY: GetModuleHandleW(NULL) is safe + let hinstance = unsafe { GetModuleHandleW(None) } + .map_err(|_| error::ProcessingError::InvalidWindowHandle)?; + + let hinstance_nonzero = NonZeroIsize::new(hinstance.0 as isize) + .ok_or(error::ProcessingError::InvalidWindowHandle)?; + window.hinstance = Some(hinstance_nonzero); + + let display = WindowsDisplayHandle::new(); + + ( + RawWindowHandle::Win32(window), + RawDisplayHandle::Windows(display), + ) + }; + + #[cfg(target_os = "linux")] + let (raw_window_handle, raw_display_handle) = { + use raw_window_handle::{WaylandDisplayHandle, WaylandWindowHandle}; + + if window_handle == 0 { + return Err(error::ProcessingError::HandleError( + HandleError::Unavailable, + )); + } + let window_handle_ptr = NonNull::new(window_handle as *mut c_void).unwrap(); + let window = WaylandWindowHandle::new(window_handle_ptr); + + if display_handle == 0 { + return Err(error::ProcessingError::HandleError( + HandleError::Unavailable, + )); + } + let display_handle_ptr = NonNull::new(display_handle as *mut c_void).unwrap(); + let display = WaylandDisplayHandle::new(display_handle_ptr); + + ( + RawWindowHandle::Wayland(window), + RawDisplayHandle::Wayland(display), + ) + }; + + let glfw_window = GlfwWindow { + window_handle: raw_window_handle, + display_handle: raw_display_handle, + }; + + let window_wrapper = WindowWrapper::new(glfw_window); + let handle_wrapper = RawHandleWrapper::new(&window_wrapper)?; + + Ok(commands + .spawn(( + Window { + resolution: WindowResolution::new(width, height) + .with_scale_factor_override(scale_factor), + ..default() + }, + handle_wrapper, + Surface, + )) + .id()) + } + + world + .run_system_cached_with( + create_inner, + (window_handle, display_handle, width, height, scale_factor), + ) + .unwrap() +} + +pub fn create_offscreen( + world: &mut World, + width: u32, + height: u32, + scale_factor: f32, + texture_format: TextureFormat, +) -> Result { + // we just wrap image create here + let size = Extent3d { + width: (width as f32 * scale_factor) as u32, + height: (height as f32 * scale_factor) as u32, + depth_or_array_layers: 1, + }; + let pixel_size = match texture_format { + TextureFormat::R8Unorm => 1, + TextureFormat::Rg8Unorm => 2, + TextureFormat::Rgba8Unorm + | TextureFormat::Bgra8Unorm + | TextureFormat::Rgba16Float + | TextureFormat::Rgba32Float => 4, + _ => return Err(ProcessingError::UnsupportedTextureFormat), + }; + + let data = vec![0u8; (size.width * size.height * pixel_size) as usize]; + let image = crate::image::create(world, size, data, texture_format); + world.entity_mut(image).insert(Surface); + Ok(image) +} + +pub fn destroy(world: &mut World, window_entity: Entity) -> Result<()> { + fn destroy_inner( + In(surface_entity): In, + mut commands: Commands, + p_images: Query<&Image, With>, + mut images: ResMut>, + mut p_image_textures: ResMut, + ) -> Result<()> { + match p_images.get(surface_entity) { + Ok(p_image) => { + images.remove(&p_image.handle); + p_image_textures.remove(&surface_entity); + commands.entity(surface_entity).despawn(); + Ok(()) + } + Err(QueryEntityError::QueryDoesNotMatch(..)) => { + commands.entity(surface_entity).despawn(); + Ok(()) + } + Err(_) => Err(ProcessingError::SurfaceNotFound), + } + } + + world + .run_system_cached_with(destroy_inner, window_entity) + .unwrap() +} + +/// Update window size when resized. +pub fn resize(world: &mut World, window_entity: Entity, width: u32, height: u32) -> Result<()> { + fn resize_inner( + In((window_entity, width, height)): In<(Entity, u32, u32)>, + mut windows: Query<&mut Window>, + ) -> Result<()> { + if let Ok(mut window) = windows.get_mut(window_entity) { + window.resolution.set_physical_resolution(width, height); + + Ok(()) + } else { + Err(error::ProcessingError::SurfaceNotFound) + } + } + + world + .run_system_cached_with(resize_inner, (window_entity, width, height)) + .unwrap() +} diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..34e9c3e --- /dev/null +++ b/docs/api.md @@ -0,0 +1,77 @@ +# API + +## API Objects + +All objects are referenced via an entity identifier returned to the caller as an opaque +`u64` value via a `create` function. When an object is no longer needed, it must be +explicitly destroyed via a `destroy` function. + +### Surface + +A surface represents a drawing area. It is primarily used as a target for rendering and typically +is associated with a window or an off-screen buffer. The equivalent of a surface in `Bevy` is +a `RenderTarget`, which can be a window or a texture. + +Note, a "surface" is also a technical term in graphics APIs like Vulkan and WebGPU, where it refers to +the platform-specific representation of a drawing area that the swapchain presents images to. However, +in this API, we use "surface" in a more general sense to refer to any drawing area, not just those tied +to swapchains. + +Processing users are not typically expected to interact directly with surfaces. Rather, a graphics object +is created and associated with a surface internally, which will implicitly be a new window unless the user +specifically requests a headless drawing context, in which case an off-screen surface is created. + +### Graphics + +Graphics objects encapsulate the rendering context and state and provide the core methods for drawing shapes, +images, and text. They manage the current drawing state, including colors, stroke weights, transformations, +and other properties that affect rendering. In `Bevy`, this is equivalent to a `Camera` entity. + +In the Java Processing API, graphics objects are a subclass of `PImage`. In this API, graphics objects are distinct from +images rather than bearing an explicit is-a relationship. Importantly, the "image" for a Bevy `Camera` is the internal +rendering texture that the camera draws to (`ViewTarget`), which is not typically directly exposed to users. + +For consistency, all image functions should accept a graphics object, although its internal image representation is +not guaranteed to be the same as a user-created image. + +### Image + +Images are 2D or 3D arrays of pixels that can be drawn onto surfaces. They can be created from files, generated procedurally, +or created as empty canvases for off-screen rendering. In `Bevy`, images are represented as `Image` assets. Images exist +simultaneously as GPU resources and CPU-side data structures and have a lifecycle that requires the use to load pixels +from the GPU to the CPU before accessing pixel data directly and to flush pixel data from the CPU to the GPU after +modifying pixel data directly. + +### Font + +[//]: # (TODO: Document Font API object) + +### Geometry + +Geometry (also known as `PShape` in the Java Processing API) represents complex shapes defined by vertices, edges, and +faces. Geometry objects can encapsulate 2D shapes, 3D models, or custom vertex data. They can be created +programmatically or loaded from external files. In `Bevy`, geometry is typically represented using `Mesh` assets and +require an associated `Material` to be rendered. + +Like an image, geometry exists as both a GPU resource and a CPU-side data structure. Users must ensure that any changes +to the CPU-side geometry are synchronized with the GPU resource before rendering. + +### Material + +A material defines the appearance of geometry when rendered. In Bevy, materials are represented using `Material` assets +that define how geometry interacts with light and other visual effects, typically a PBR `StandardMaterial`. Processing +has a simpler material model based on Blinn-Phong shading and in the Java Processing API, materials are typically defined +as vertex attributes within a `PShape`. + +We define materials as their own API objects as this is key to enabling instanced rendering in retained mode graphics. +Bevy will batch draws of geometry that share the same material, allowing for efficient rendering of many instances of +the same geometry with the same appearance. + +This API also helps define the high level interface to work with shaders, which are used to implement custom materials. +While materials are typically defined in terms of their interaction with light in a 3D scene, the simplest "sketch" +implementation of a shader is simply a fragment shader that is applied to a full-screen quad. In this way, materials +can also be used to implement 2D image processing effects, etc. + +### Shader + +[//]: # (TODO: Document Shader API object, do we even need this with a sufficiently robust Material API?) \ No newline at end of file diff --git a/examples/background_image.rs b/examples/background_image.rs index f1ba852..da0da83 100644 --- a/examples/background_image.rs +++ b/examples/background_image.rs @@ -22,17 +22,22 @@ fn sketch() -> error::Result<()> { let mut glfw_ctx = GlfwContext::new(400, 400)?; init()?; + let width = 400; + let height = 400; + let scale_factor = 1.0; + 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 surface = surface_create(window_handle, display_handle, width, height, scale_factor)?; + let graphics = graphics_create(surface, width, height)?; let image = image_load("images/logo.png")?; while glfw_ctx.poll_events() { - begin_draw(surface)?; + graphics_begin_draw(graphics)?; - record_command(surface, DrawCommand::BackgroundImage(image))?; + graphics_record_command(graphics, DrawCommand::BackgroundImage(image))?; - end_draw(surface)?; + graphics_end_draw(graphics)?; } Ok(()) } diff --git a/examples/rectangle.rs b/examples/rectangle.rs index b8584df..6e60e16 100644 --- a/examples/rectangle.rs +++ b/examples/rectangle.rs @@ -21,15 +21,20 @@ fn sketch() -> error::Result<()> { let mut glfw_ctx = GlfwContext::new(400, 400)?; init()?; + let width = 400; + let height = 400; + let scale_factor = 1.0; + 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 surface = surface_create(window_handle, display_handle, width, height, scale_factor)?; + let graphics = graphics_create(surface, width, height)?; while glfw_ctx.poll_events() { - begin_draw(surface)?; + graphics_begin_draw(graphics)?; - record_command( - surface, + graphics_record_command( + graphics, DrawCommand::Rect { x: 10.0, y: 10.0, @@ -39,7 +44,7 @@ fn sketch() -> error::Result<()> { }, )?; - end_draw(surface)?; + graphics_end_draw(graphics)?; } Ok(()) } diff --git a/examples/update_pixels.rs b/examples/update_pixels.rs new file mode 100644 index 0000000..287d6b8 --- /dev/null +++ b/examples/update_pixels.rs @@ -0,0 +1,87 @@ +mod glfw; + +use bevy::{color::Color, prelude::LinearRgba}; +use glfw::GlfwContext; +use processing::prelude::*; + +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(100, 100)?; + init()?; + + let width = 100; + let height = 100; + let scale_factor = 1.0; + + let window_handle = glfw_ctx.get_window(); + let display_handle = glfw_ctx.get_display(); + let surface = surface_create(window_handle, display_handle, width, height, scale_factor)?; + let graphics = graphics_create(surface, width, height)?; + + let rect_w = 10; + let rect_h = 10; + let red = LinearRgba::new(1.0, 0.0, 0.0, 1.0); + let red_pixels: Vec = vec![red; rect_w * rect_h]; + + let blue = LinearRgba::new(0.0, 0.0, 1.0, 1.0); + let blue_pixels: Vec = vec![blue; rect_w * rect_h]; + + let mut first_frame = true; + + while glfw_ctx.poll_events() { + graphics_begin_draw(graphics)?; + + graphics_record_command(graphics, DrawCommand::BackgroundColor(Color::BLACK))?; + graphics_flush(graphics)?; + graphics_update_region(graphics, 20, 20, rect_w as u32, rect_h as u32, &red_pixels)?; + graphics_update_region(graphics, 60, 60, rect_w as u32, rect_h as u32, &blue_pixels)?; + + graphics_end_draw(graphics)?; + + if first_frame { + first_frame = false; + + let pixels = graphics_readback(graphics)?; + eprintln!("Total pixels: {}", pixels.len()); + + for y in 0..height { + for x in 0..width { + let idx = (y * width + x) as usize; + if idx < pixels.len() { + let pixel = pixels[idx]; + if pixel.red > 0.5 { + eprint!("R"); + } else if pixel.blue > 0.5 { + eprint!("B"); + } else if pixel.alpha > 0.5 { + eprint!("."); + } else { + eprint!(" "); + } + } + } + eprintln!(); + } + + eprintln!("\nSample pixels:"); + eprintln!("(25, 25): {:?}", pixels[25 * width as usize + 25]); + eprintln!("(65, 65): {:?}", pixels[65 * width as usize + 65]); + eprintln!("(0, 0): {:?}", pixels[0]); + eprintln!("(50, 50): {:?}", pixels[50 * width as usize + 50]); + } + } + + Ok(()) +} From d2fecc05ed594b157367b659c754d6f9c99d780c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 8 Dec 2025 19:22:01 -0800 Subject: [PATCH 2/8] Fixup other crates. --- crates/processing_pyo3/src/lib.rs | 6 ++--- crates/processing_render/src/lib.rs | 5 ++-- crates/processing_render/src/surface.rs | 20 ++++++++++++++++ crates/processing_wasm/src/lib.rs | 32 ++++++++++++------------- 4 files changed, 41 insertions(+), 22 deletions(-) diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 5c6db44..7073634 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -19,9 +19,9 @@ mod processing { let surface = surface_create(window_handle, display_handle, width, height, 1.0).unwrap(); while glfw_ctx.poll_events() { - begin_draw(surface).unwrap(); + graphics_begin_draw(surface).unwrap(); - record_command( + graphics_record_command( surface, DrawCommand::Rect { x: 10.0, @@ -33,7 +33,7 @@ mod processing { ) .unwrap(); - end_draw(surface).unwrap(); + graphics_end_draw(surface).unwrap(); } Ok("OK".to_string()) diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 8ca2202..d51ac9a 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -4,17 +4,16 @@ pub mod image; pub mod render; mod surface; -#[cfg(any(target_os = "linux", target_arch = "wasm32"))] -use std::ffi::c_void; use std::{cell::RefCell, num::NonZero, path::PathBuf, sync::OnceLock}; use bevy::{ app::{App, AppExit}, asset::AssetEventSystems, - log::tracing_subscriber, prelude::*, render::render_resource::{Extent3d, TextureFormat}, }; +#[cfg(not(target_arch = "wasm32"))] +use bevy::log::tracing_subscriber; use render::{activate_cameras, clear_transient_meshes, flush_draw_commands}; use tracing::debug; diff --git a/crates/processing_render/src/surface.rs b/crates/processing_render/src/surface.rs index b7a4116..6970022 100644 --- a/crates/processing_render/src/surface.rs +++ b/crates/processing_render/src/surface.rs @@ -6,6 +6,8 @@ //! //! In Bevy, we can consider a surface to be a [`RenderTarget`], which is either a window or a //! texture. +#[cfg(any(target_os = "linux", target_arch = "wasm32"))] +use std::ffi::c_void; use std::ptr::NonNull; use bevy::{ @@ -191,6 +193,24 @@ pub fn create( ) }; + #[cfg(target_arch = "wasm32")] + let (raw_window_handle, raw_display_handle) = { + use raw_window_handle::{WebCanvasWindowHandle, WebDisplayHandle}; + + // For WASM, window_handle is a pointer to an HtmlCanvasElement + if window_handle == 0 { + return Err(error::ProcessingError::InvalidWindowHandle); + } + let canvas_ptr = NonNull::new(window_handle as *mut c_void).unwrap(); + let window = WebCanvasWindowHandle::new(canvas_ptr.cast()); + let display = WebDisplayHandle::new(); + + ( + RawWindowHandle::WebCanvas(window), + RawDisplayHandle::Web(display), + ) + }; + let glfw_window = GlfwWindow { window_handle: raw_window_handle, display_handle: raw_display_handle, diff --git a/crates/processing_wasm/src/lib.rs b/crates/processing_wasm/src/lib.rs index f5359c7..96da341 100644 --- a/crates/processing_wasm/src/lib.rs +++ b/crates/processing_wasm/src/lib.rs @@ -1,8 +1,8 @@ use bevy::prelude::Entity; use processing_render::{ - begin_draw, end_draw, exit, flush, image_create, image_destroy, image_load, image_load_pixels, - image_resize, init, record_command, render::command::DrawCommand, surface_create_from_canvas, - surface_destroy, surface_resize, + exit, graphics_begin_draw, graphics_end_draw, graphics_flush, graphics_record_command, + image_create, image_destroy, image_load, image_readback, image_resize, init, + render::command::DrawCommand, surface_create_from_canvas, surface_destroy, surface_resize, }; use wasm_bindgen::prelude::*; @@ -38,7 +38,7 @@ pub fn js_surface_resize(surface_id: u64, width: u32, height: u32) -> Result<(), #[wasm_bindgen(js_name = "background")] pub fn js_background_color(surface_id: u64, r: f32, g: f32, b: f32, a: f32) -> Result<(), JsValue> { let color = bevy::color::Color::srgba(r, g, b, a); - check(record_command( + check(graphics_record_command( Entity::from_bits(surface_id), DrawCommand::BackgroundColor(color), )) @@ -46,7 +46,7 @@ pub fn js_background_color(surface_id: u64, r: f32, g: f32, b: f32, a: f32) -> R #[wasm_bindgen(js_name = "backgroundImage")] pub fn js_background_image(surface_id: u64, image_id: u64) -> Result<(), JsValue> { - check(record_command( + check(graphics_record_command( Entity::from_bits(surface_id), DrawCommand::BackgroundImage(Entity::from_bits(image_id)), )) @@ -54,17 +54,17 @@ pub fn js_background_image(surface_id: u64, image_id: u64) -> Result<(), JsValue #[wasm_bindgen(js_name = "beginDraw")] pub fn js_begin_draw(surface_id: u64) -> Result<(), JsValue> { - check(begin_draw(Entity::from_bits(surface_id))) + check(graphics_begin_draw(Entity::from_bits(surface_id))) } #[wasm_bindgen(js_name = "flush")] pub fn js_flush(surface_id: u64) -> Result<(), JsValue> { - check(flush(Entity::from_bits(surface_id))) + check(graphics_flush(Entity::from_bits(surface_id))) } #[wasm_bindgen(js_name = "endDraw")] pub fn js_end_draw(surface_id: u64) -> Result<(), JsValue> { - check(end_draw(Entity::from_bits(surface_id))) + check(graphics_end_draw(Entity::from_bits(surface_id))) } #[wasm_bindgen(js_name = "exit")] @@ -75,7 +75,7 @@ pub fn js_exit(exit_code: u8) -> Result<(), JsValue> { #[wasm_bindgen(js_name = "fill")] pub fn js_fill(surface_id: u64, r: f32, g: f32, b: f32, a: f32) -> Result<(), JsValue> { let color = bevy::color::Color::srgba(r, g, b, a); - check(record_command( + check(graphics_record_command( Entity::from_bits(surface_id), DrawCommand::Fill(color), )) @@ -84,7 +84,7 @@ pub fn js_fill(surface_id: u64, r: f32, g: f32, b: f32, a: f32) -> Result<(), Js #[wasm_bindgen(js_name = "stroke")] pub fn js_stroke(surface_id: u64, r: f32, g: f32, b: f32, a: f32) -> Result<(), JsValue> { let color = bevy::color::Color::srgba(r, g, b, a); - check(record_command( + check(graphics_record_command( Entity::from_bits(surface_id), DrawCommand::StrokeColor(color), )) @@ -92,7 +92,7 @@ pub fn js_stroke(surface_id: u64, r: f32, g: f32, b: f32, a: f32) -> Result<(), #[wasm_bindgen(js_name = "strokeWeight")] pub fn js_stroke_weight(surface_id: u64, weight: f32) -> Result<(), JsValue> { - check(record_command( + check(graphics_record_command( Entity::from_bits(surface_id), DrawCommand::StrokeWeight(weight), )) @@ -100,7 +100,7 @@ pub fn js_stroke_weight(surface_id: u64, weight: f32) -> Result<(), JsValue> { #[wasm_bindgen(js_name = "noFill")] pub fn js_no_fill(surface_id: u64) -> Result<(), JsValue> { - check(record_command( + check(graphics_record_command( Entity::from_bits(surface_id), DrawCommand::NoFill, )) @@ -108,7 +108,7 @@ pub fn js_no_fill(surface_id: u64) -> Result<(), JsValue> { #[wasm_bindgen(js_name = "noStroke")] pub fn js_no_stroke(surface_id: u64) -> Result<(), JsValue> { - check(record_command( + check(graphics_record_command( Entity::from_bits(surface_id), DrawCommand::NoStroke, )) @@ -126,7 +126,7 @@ pub fn js_rect( br: f32, bl: f32, ) -> Result<(), JsValue> { - check(record_command( + check(graphics_record_command( Entity::from_bits(surface_id), DrawCommand::Rect { x, @@ -168,8 +168,8 @@ pub fn js_image_resize(image_id: u64, new_width: u32, new_height: u32) -> Result } #[wasm_bindgen(js_name = "loadPixels")] -pub fn js_image_load_pixels(image_id: u64) -> Result, JsValue> { - let colors = check(image_load_pixels(Entity::from_bits(image_id)))?; +pub fn js_image_readback(image_id: u64) -> Result, JsValue> { + let colors = check(image_readback(Entity::from_bits(image_id)))?; let mut result = Vec::with_capacity(colors.len() * 4); for color in colors { From 1013dfe94acd2c5eafbf9c02d12909f4f5679ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 8 Dec 2025 19:29:21 -0800 Subject: [PATCH 3/8] Clippy. --- crates/processing_render/src/surface.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/processing_render/src/surface.rs b/crates/processing_render/src/surface.rs index 6970022..64cdc66 100644 --- a/crates/processing_render/src/surface.rs +++ b/crates/processing_render/src/surface.rs @@ -83,7 +83,7 @@ pub fn create( scale_factor: f32, ) -> Result { fn create_inner( - In((window_handle, _display_handle, width, height, scale_factor)): In<( + In((window_handle, display_handle, width, height, scale_factor)): In<( u64, u64, u32, From 53b8a662c239324577d664f90b67f9a9c39e753a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 8 Dec 2025 19:34:13 -0800 Subject: [PATCH 4/8] Fmt. --- crates/processing_render/src/graphics.rs | 5 ++++- crates/processing_render/src/lib.rs | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index 70055cf..c46ea5c 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -490,7 +490,10 @@ pub fn update_region( .ok_or(ProcessingError::GraphicsNotFound)?; let texture = view_target.main_texture(); - eprintln!("update_region: writing to texture {:p} at ({}, {}) size {}x{}", texture as *const _, x, y, width, height); + eprintln!( + "update_region: writing to texture {:p} at ({}, {}) size {}x{}", + texture as *const _, x, y, width, height + ); let bytes_per_row = width * px_size; render_queue.write_texture( diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index d51ac9a..330e8be 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -6,14 +6,14 @@ mod surface; use std::{cell::RefCell, num::NonZero, path::PathBuf, sync::OnceLock}; +#[cfg(not(target_arch = "wasm32"))] +use bevy::log::tracing_subscriber; use bevy::{ app::{App, AppExit}, asset::AssetEventSystems, prelude::*, render::render_resource::{Extent3d, TextureFormat}, }; -#[cfg(not(target_arch = "wasm32"))] -use bevy::log::tracing_subscriber; use render::{activate_cameras, clear_transient_meshes, flush_draw_commands}; use tracing::debug; From 234ed2104202adaeb2feebbc261543c60cc39b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 8 Dec 2025 19:38:08 -0800 Subject: [PATCH 5/8] Clippy. --- crates/processing_render/src/graphics.rs | 2 +- crates/processing_render/src/image.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index c46ea5c..ddb0d4c 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -506,7 +506,7 @@ pub fn update_region( &data, TexelCopyBufferLayout { offset: 0, - bytes_per_row: Some(bytes_per_row.try_into().unwrap()), + bytes_per_row: Some(bytes_per_row), rows_per_image: None, }, Extent3d { diff --git a/crates/processing_render/src/image.rs b/crates/processing_render/src/image.rs index b9a3b67..f0b59b6 100644 --- a/crates/processing_render/src/image.rs +++ b/crates/processing_render/src/image.rs @@ -364,7 +364,7 @@ pub fn update_region( &data, TexelCopyBufferLayout { offset: 0, - bytes_per_row: Some(bytes_per_row.try_into().unwrap()), + bytes_per_row: Some(bytes_per_row), rows_per_image: None, }, Extent3d { From 4dc135011d69a9caf7cc64dd3bc6e570b77f0b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 8 Dec 2025 20:15:44 -0800 Subject: [PATCH 6/8] Yet more clippy. --- crates/processing_pyo3/src/glfw.rs | 5 ++--- crates/processing_wasm/src/lib.rs | 2 ++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/processing_pyo3/src/glfw.rs b/crates/processing_pyo3/src/glfw.rs index ba56a55..3c4363b 100644 --- a/crates/processing_pyo3/src/glfw.rs +++ b/crates/processing_pyo3/src/glfw.rs @@ -58,9 +58,8 @@ impl GlfwContext { self.glfw.poll_events(); for (_, event) in glfw::flush_messages(&self.events) { - match event { - WindowEvent::Close => return false, - _ => {} + if event == WindowEvent::Close { + return false; } } diff --git a/crates/processing_wasm/src/lib.rs b/crates/processing_wasm/src/lib.rs index 96da341..3d7d4df 100644 --- a/crates/processing_wasm/src/lib.rs +++ b/crates/processing_wasm/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg(target_arch = "wasm32")] + use bevy::prelude::Entity; use processing_render::{ exit, graphics_begin_draw, graphics_end_draw, graphics_flush, graphics_record_command, From 0d1630cbbcef0da15c7b6376c62fde03cd12478b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Tue, 9 Dec 2025 13:15:18 -0800 Subject: [PATCH 7/8] Fix test. We use vk/webgpu depth not ogl. --- crates/processing_render/src/graphics.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index ddb0d4c..cae9f23 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -615,7 +615,9 @@ mod tests { }; let clip_matrix = proj.get_clip_from_view(); // Check some values in the matrix to ensure it's correct - assert_eq!(clip_matrix.w_axis.z, -2.0 / (1000.0 - 0.1)); + // In [0,1] depth orthographic projection, w_axis.z = -near/(far-near) + let expected = -0.1 / (1000.0 - 0.1); + assert!((clip_matrix.w_axis.z - expected).abs() < 1e-6); } #[test] From 20f4cec184f10e45029f2e82467764c734a1f441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Tue, 9 Dec 2025 14:56:37 -0800 Subject: [PATCH 8/8] fmt. --- crates/processing_pyo3/src/graphics.rs | 41 +++++++++++++++++++------- crates/processing_pyo3/src/lib.rs | 17 ++++++++--- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index 7d41cca..91c1c03 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -1,8 +1,6 @@ use bevy::prelude::Entity; use processing::prelude::*; -use pyo3::exceptions::PyRuntimeError; -use pyo3::prelude::*; -use pyo3::types::PyAny; +use pyo3::{exceptions::PyRuntimeError, prelude::*, types::PyAny}; use crate::glfw::GlfwContext; @@ -16,8 +14,8 @@ pub struct Graphics { impl Graphics { #[new] pub fn new(width: u32, height: u32) -> PyResult { - let glfw_ctx = GlfwContext::new(width, height) - .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + let glfw_ctx = + GlfwContext::new(width, height).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; init().map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; @@ -65,10 +63,26 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } - pub fn rect(&self, x: f32, y: f32, w: f32, h: f32, tl: f32, tr: f32, br: f32, bl: f32) -> PyResult<()> { + pub fn rect( + &self, + x: f32, + y: f32, + w: f32, + h: f32, + tl: f32, + tr: f32, + br: f32, + bl: f32, + ) -> PyResult<()> { graphics_record_command( self.surface, - DrawCommand::Rect { x, y, w, h, radii: [tl, tr, br, bl] }, + DrawCommand::Rect { + x, + y, + w, + h, + radii: [tl, tr, br, bl], + }, ) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -84,12 +98,12 @@ impl Graphics { if let Some(ref draw) = draw_fn { Python::attach(|py| { - draw.call0(py).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + draw.call0(py) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) })?; } - graphics_end_draw(self.surface) - .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; + graphics_end_draw(self.surface).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?; } Ok(()) } @@ -108,7 +122,12 @@ fn parse_color(args: &[f32]) -> PyResult<(f32, f32, f32, f32)> { Ok((v, v, v, args[1] / 255.0)) } 3 => Ok((args[0] / 255.0, args[1] / 255.0, args[2] / 255.0, 1.0)), - 4 => Ok((args[0] / 255.0, args[1] / 255.0, args[2] / 255.0, args[3] / 255.0)), + 4 => Ok(( + args[0] / 255.0, + args[1] / 255.0, + args[2] / 255.0, + args[3] / 255.0, + )), _ => Err(PyRuntimeError::new_err("color requires 1-4 arguments")), } } diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 6526e6a..ca4a90d 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -11,9 +11,8 @@ mod glfw; mod graphics; -use graphics::{get_graphics, get_graphics_mut, Graphics}; -use pyo3::prelude::*; -use pyo3::types::PyAny; +use graphics::{Graphics, get_graphics, get_graphics_mut}; +use pyo3::{prelude::*, types::PyAny}; #[pymodule] fn processing(m: &Bound<'_, PyModule>) -> PyResult<()> { @@ -82,6 +81,16 @@ fn stroke_weight(module: &Bound<'_, PyModule>, weight: f32) -> PyResult<()> { #[pyfunction] #[pyo3(pass_module, signature = (x, y, w, h, tl=0.0, tr=0.0, br=0.0, bl=0.0))] -fn rect(module: &Bound<'_, PyModule>, x: f32, y: f32, w: f32, h: f32, tl: f32, tr: f32, br: f32, bl: f32) -> PyResult<()> { +fn rect( + module: &Bound<'_, PyModule>, + x: f32, + y: f32, + w: f32, + h: f32, + tl: f32, + tr: f32, + br: f32, + bl: f32, +) -> PyResult<()> { get_graphics(module)?.rect(x, y, w, h, tl, tr, br, bl) }