diff --git a/core/examples/src/main/java/WebGPU.java b/core/examples/src/main/java/WebGPU.java new file mode 100644 index 0000000000..71ce807ff8 --- /dev/null +++ b/core/examples/src/main/java/WebGPU.java @@ -0,0 +1,58 @@ +import processing.core.PApplet; + +public class WebGPU extends PApplet { + public void settings() { + size(600, 400, WEBGPU); + } + + public void draw() { + background(200); + + noStroke(); + fill(255, 0, 0); + rect(50, 50, 80, 80); + + fill(0, 255, 0); + rect(150, 50, 80, 80); + + fill(0, 0, 255); + rect(250, 50, 80, 80); + + fill(255, 0, 0, 128); + rect(50, 150, 80, 80); + + fill(0, 255, 0, 128); + rect(150, 150, 80, 80); + + fill(0, 0, 255, 128); + rect(250, 150, 80, 80); + + stroke(0); + strokeWeight(4); + fill(255, 200, 0); + rect(50, 250, 80, 80); + + fill(200, 0, 255); + rect(150, 250, 80, 80); + + noFill(); + stroke(255, 0, 255); + strokeWeight(6); + rect(250, 250, 80, 80); + + noStroke(); + fill(255, 0, 0, 100); + rect(400, 100, 100, 100); + + fill(0, 255, 0, 100); + rect(440, 140, 100, 100); + + fill(0, 0, 255, 100); + rect(420, 180, 100, 100); + } + + public static void main(String[] args) { + PApplet.disableAWT = true; + PApplet.main(WebGPU.class.getName()); + } +} diff --git a/core/src/processing/webgpu/PGraphicsWebGPU.java b/core/src/processing/webgpu/PGraphicsWebGPU.java index 914f736836..3e27967c4a 100644 --- a/core/src/processing/webgpu/PGraphicsWebGPU.java +++ b/core/src/processing/webgpu/PGraphicsWebGPU.java @@ -4,7 +4,7 @@ import processing.core.PSurface; public class PGraphicsWebGPU extends PGraphics { - private long windowId = 0; + private long surfaceId = 0; @Override public PSurface createSurface() { @@ -12,8 +12,8 @@ public PSurface createSurface() { } protected void initWebGPUSurface(long windowHandle, int width, int height, float scaleFactor) { - windowId = PWebGPU.createSurface(windowHandle, width, height, scaleFactor); - if (windowId == 0) { + surfaceId = PWebGPU.createSurface(windowHandle, width, height, scaleFactor); + if (surfaceId == 0) { System.err.println("Failed to create WebGPU surface"); } } @@ -21,8 +21,8 @@ protected void initWebGPUSurface(long windowHandle, int width, int height, float @Override public void setSize(int w, int h) { super.setSize(w, h); - if (windowId != 0) { - PWebGPU.windowResized(windowId, pixelWidth, pixelHeight); + if (surfaceId != 0) { + PWebGPU.windowResized(surfaceId, pixelWidth, pixelHeight); } } @@ -30,29 +30,105 @@ public void setSize(int w, int h) { public void beginDraw() { super.beginDraw(); checkSettings(); + System.out.println("Beginning draw on surfaceId: " + surfaceId); + PWebGPU.beginDraw(surfaceId); + } + + @Override + public void flush() { + super.flush(); + PWebGPU.flush(surfaceId); } @Override public void endDraw() { super.endDraw(); - PWebGPU.update(); + PWebGPU.endDraw(surfaceId); } @Override public void dispose() { super.dispose(); - if (windowId != 0) { - PWebGPU.destroySurface(windowId); - windowId = 0; + if (surfaceId != 0) { + PWebGPU.destroySurface(surfaceId); + surfaceId = 0; } PWebGPU.exit(); } @Override protected void backgroundImpl() { - if (windowId == 0) { + if (surfaceId == 0) { + return; + } + PWebGPU.backgroundColor(surfaceId, backgroundR, backgroundG, backgroundB, backgroundA); + } + + @Override + protected void fillFromCalc() { + super.fillFromCalc(); + if (surfaceId == 0) { + return; + } + if (fill) { + PWebGPU.setFill(surfaceId, fillR, fillG, fillB, fillA); + } else { + PWebGPU.noFill(surfaceId); + } + } + + @Override + protected void strokeFromCalc() { + super.strokeFromCalc(); + if (surfaceId == 0) { + return; + } + if (stroke) { + PWebGPU.setStrokeColor(surfaceId, strokeR, strokeG, strokeB, strokeA); + } else { + PWebGPU.noStroke(surfaceId); + } + } + + @Override + public void strokeWeight(float weight) { + super.strokeWeight(weight); + if (surfaceId == 0) { + return; + } + PWebGPU.setStrokeWeight(surfaceId, weight); + } + + @Override + public void noFill() { + super.noFill(); + if (surfaceId == 0) { + return; + } + PWebGPU.noFill(surfaceId); + } + + @Override + public void noStroke() { + super.noStroke(); + if (surfaceId == 0) { + return; + } + PWebGPU.noStroke(surfaceId); + } + + @Override + protected void rectImpl(float x1, float y1, float x2, float y2) { + rectImpl(x1, y1, x2, y2, 0, 0, 0, 0); + } + + @Override + protected void rectImpl(float x1, float y1, float x2, float y2, + float tl, float tr, float br, float bl) { + if (surfaceId == 0) { return; } - PWebGPU.backgroundColor(windowId, backgroundR, backgroundG, backgroundB, backgroundA); + // rectImpl receives corner coordinates, so let's convert to x,y,w,h + PWebGPU.rect(surfaceId, x1, y1, x2 - x1, y2 - y1, tl, tr, br, bl); } } diff --git a/core/src/processing/webgpu/PWebGPU.java b/core/src/processing/webgpu/PWebGPU.java index 2fde44f71f..fdc1512623 100644 --- a/core/src/processing/webgpu/PWebGPU.java +++ b/core/src/processing/webgpu/PWebGPU.java @@ -44,38 +44,46 @@ public static void init() { * @return Window ID to use for subsequent operations */ public static long createSurface(long windowHandle, int width, int height, float scaleFactor) { - long windowId = processing_create_surface(windowHandle, width, height, scaleFactor); + long surfaceId = processing_create_surface(windowHandle, width, height, scaleFactor); checkError(); - return windowId; + return surfaceId; } /** * Destroys a WebGPU surface. * - * @param windowId The window ID returned from createSurface + * @param surfaceId The window ID returned from createSurface */ - public static void destroySurface(long windowId) { - processing_destroy_surface(windowId); + public static void destroySurface(long surfaceId) { + processing_destroy_surface(surfaceId); checkError(); } /** * Updates a window's size. * - * @param windowId The window ID returned from createSurface + * @param surfaceId The window ID returned from createSurface * @param width New physical window width in pixels * @param height New physical window height in pixels */ - public static void windowResized(long windowId, int width, int height) { - processing_resize_surface(windowId, width, height); + public static void windowResized(long surfaceId, int width, int height) { + processing_resize_surface(surfaceId, width, height); checkError(); } - /** - * Updates the WebGPU subsystem. Should be called once per frame after all drawing is complete. - */ - public static void update() { - processing_update(); + + public static void beginDraw(long surfaceId) { + processing_begin_draw(surfaceId); + checkError(); + } + + public static void flush(long surfaceId) { + processing_flush(surfaceId); + checkError(); + } + + public static void endDraw(long surfaceId) { + processing_end_draw(surfaceId); checkError(); } @@ -87,7 +95,7 @@ public static void exit() { checkError(); } - public static void backgroundColor(long windowId, float r, float g, float b, float a) { + public static void backgroundColor(long surfaceId, float r, float g, float b, float a) { try (Arena arena = Arena.ofConfined()) { MemorySegment color = Color.allocate(arena); @@ -96,11 +104,60 @@ public static void backgroundColor(long windowId, float r, float g, float b, flo Color.b(color, b); Color.a(color, a); - processing_background_color(windowId, color); + processing_background_color(surfaceId, color); checkError(); } } + /** + * Set the fill color. + */ + public static void setFill(long surfaceId, float r, float g, float b, float a) { + processing_set_fill(surfaceId, r, g, b, a); + checkError(); + } + + /** + * Set the stroke color. + */ + public static void setStrokeColor(long surfaceId, float r, float g, float b, float a) { + processing_set_stroke_color(surfaceId, r, g, b, a); + checkError(); + } + + /** + * Set the stroke weight. + */ + public static void setStrokeWeight(long surfaceId, float weight) { + processing_set_stroke_weight(surfaceId, weight); + checkError(); + } + + /** + * Disable fill for subsequent shapes. + */ + public static void noFill(long surfaceId) { + processing_no_fill(surfaceId); + checkError(); + } + + /** + * Disable stroke for subsequent shapes. + */ + public static void noStroke(long surfaceId) { + processing_no_stroke(surfaceId); + checkError(); + } + + /** + * Draw a rectangle. + */ + public static void rect(long surfaceId, float x, float y, float w, float h, + float tl, float tr, float br, float bl) { + processing_rect(surfaceId, x, y, w, h, tl, tr, br, bl); + checkError(); + } + /** * Checks for errors from the native library and throws a PWebGPUException if an error occurred. */ diff --git a/libProcessing/Cargo.lock b/libProcessing/Cargo.lock index aaee0a5a0e..e8af7ea5f5 100644 --- a/libProcessing/Cargo.lock +++ b/libProcessing/Cargo.lock @@ -110,7 +110,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "libc", ] @@ -132,7 +132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.9.4", + "bitflags 2.10.0", "cc", "cesu8", "jni", @@ -502,7 +502,7 @@ dependencies = [ "bevy_reflect", "bevy_tasks", "bevy_utils", - "bitflags 2.9.4", + "bitflags 2.10.0", "blake3", "crossbeam-channel", "derive_more", @@ -617,7 +617,7 @@ dependencies = [ "bevy_transform", "bevy_utils", "bevy_window", - "bitflags 2.9.4", + "bitflags 2.10.0", "nonmax", "radsort", "smallvec", @@ -667,7 +667,7 @@ dependencies = [ "bevy_reflect", "bevy_tasks", "bevy_utils", - "bitflags 2.9.4", + "bitflags 2.10.0", "bumpalo", "concurrent-queue", "derive_more", @@ -809,7 +809,7 @@ dependencies = [ "bevy_platform", "bevy_reflect", "bevy_utils", - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "futures-lite", "guillotiere", @@ -998,7 +998,7 @@ dependencies = [ "bevy_platform", "bevy_reflect", "bevy_transform", - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "derive_more", "hexasphere", @@ -1037,7 +1037,7 @@ dependencies = [ "bevy_shader", "bevy_transform", "bevy_utils", - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "derive_more", "fixedbitset", @@ -1116,7 +1116,7 @@ dependencies = [ "bevy_transform", "bevy_utils", "bevy_window", - "bitflags 2.9.4", + "bitflags 2.10.0", "nonmax", "radsort", "smallvec", @@ -1199,7 +1199,7 @@ dependencies = [ "bevy_transform", "bevy_utils", "bevy_window", - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "derive_more", "downcast-rs 2.0.2", @@ -1320,7 +1320,7 @@ dependencies = [ "bevy_text", "bevy_transform", "bevy_utils", - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "derive_more", "fixedbitset", @@ -1566,7 +1566,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1601,12 +1601,12 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ "bytemuck", - "serde", + "serde_core", ] [[package]] @@ -1709,7 +1709,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "log", "polling", "rustix 0.38.44", @@ -1964,7 +1964,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.1", "libc", ] @@ -1995,7 +1995,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "fontdb", "log", "rangemap", @@ -2142,7 +2142,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", ] @@ -2329,6 +2329,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + [[package]] name = "fnv" version = "1.0.7" @@ -2590,7 +2596,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "gpu-alloc-types", ] @@ -2600,7 +2606,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -2621,7 +2627,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -2632,7 +2638,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -2768,7 +2774,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "inotify-sys", "libc", ] @@ -2896,7 +2902,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff7f53bdf698e7aa7ec916411bbdc8078135da11b66db5182675b2227f6c0d07" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -2944,7 +2950,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "redox_syscall 0.5.18", ] @@ -2992,6 +2998,58 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "lyon" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbcb7d54d54c8937364c9d41902d066656817dce1e03a44e5533afebd1ef4352" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c0829e28c4f336396f250d850c3987e16ce6db057ffe047ce0dd54aab6b647" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e16770d760c7848b0c1c2d209101e408207a65168109509f8483837a36cf2e7" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aeca86bcfd632a15984ba029b539ffb811e0a70bf55e814ef8b0f54f506fdeb" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f586142e1280335b1bc89539f7c97dd80f08fc43e9ab1b74ef0a42b04aa353" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + [[package]] name = "mach2" version = "0.4.3" @@ -3040,7 +3098,7 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block", "core-graphics-types 0.2.0", "foreign-types", @@ -3083,7 +3141,7 @@ checksum = "916cbc7cb27db60be930a4e2da243cf4bc39569195f22fd8ee419cd31d5b662c" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "codespan-reporting", @@ -3125,7 +3183,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "jni-sys", "log", "ndk-sys 0.5.0+25.2.9519653", @@ -3139,7 +3197,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "jni-sys", "log", "ndk-sys 0.6.0+11769913", @@ -3178,7 +3236,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -3301,7 +3359,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -3317,7 +3375,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.6.2", "libc", "objc2 0.6.3", @@ -3338,7 +3396,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -3351,7 +3409,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-foundation 0.3.2", ] @@ -3373,7 +3431,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3385,7 +3443,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-foundation 0.3.2", ] @@ -3396,7 +3454,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "dispatch2", "objc2 0.6.3", ] @@ -3407,7 +3465,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "dispatch2", "objc2 0.6.3", "objc2-core-foundation", @@ -3454,7 +3512,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-core-foundation", "objc2-core-graphics", @@ -3466,7 +3524,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-core-foundation", "objc2-core-graphics", @@ -3485,7 +3543,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "dispatch", "libc", @@ -3498,7 +3556,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-core-foundation", ] @@ -3519,7 +3577,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-core-foundation", ] @@ -3542,7 +3600,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3554,7 +3612,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3567,7 +3625,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2 0.6.3", "objc2-foundation 0.3.2", ] @@ -3588,7 +3646,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit 0.2.2", @@ -3620,7 +3678,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -3811,7 +3869,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "crc32fast", "fdeflate", "flate2", @@ -4022,7 +4080,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -4065,6 +4123,7 @@ name = "renderer" version = "0.1.0" dependencies = [ "bevy", + "lyon", "objc2 0.6.3", "objc2-app-kit 0.3.2", "raw-window-handle", @@ -4091,7 +4150,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f" dependencies = [ "base64", - "bitflags 2.9.4", + "bitflags 2.10.0", "serde", "serde_derive", "unicode-ident", @@ -4121,7 +4180,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4134,7 +4193,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -4153,7 +4212,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "libm", "smallvec", @@ -4335,7 +4394,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -4378,7 +4437,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -4994,7 +5053,7 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "rustix 1.1.2", "wayland-backend", "wayland-scanner", @@ -5006,7 +5065,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cursor-icon", "wayland-backend", ] @@ -5028,7 +5087,7 @@ version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -5040,7 +5099,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5053,7 +5112,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5109,7 +5168,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70b6ff82bbf6e9206828e1a3178e851f8c20f1c9028e74dd3a8090741ccd5798" dependencies = [ "arrayvec", - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "document-features", @@ -5138,7 +5197,7 @@ dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg_aliases", "document-features", "hashbrown 0.15.5", @@ -5197,7 +5256,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.9.4", + "bitflags 2.10.0", "block", "bytemuck", "cfg-if", @@ -5241,7 +5300,7 @@ version = "26.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eca7a8d8af57c18f57d393601a1fb159ace8b2328f1b6b5f80893f7d672c9ae2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "js-sys", "log", @@ -5813,7 +5872,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.9.4", + "bitflags 2.10.0", "block2 0.5.1", "bytemuck", "calloop", @@ -5915,7 +5974,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "dlib", "log", "once_cell", diff --git a/libProcessing/ffi/build.rs b/libProcessing/ffi/build.rs index b9dcb3d616..e94d8186d1 100644 --- a/libProcessing/ffi/build.rs +++ b/libProcessing/ffi/build.rs @@ -1,5 +1,4 @@ -use std::env; -use std::path::PathBuf; +use std::{env, path::PathBuf}; fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); @@ -11,7 +10,9 @@ fn main() { let config_path = PathBuf::from(&crate_dir).join("cbindgen.toml"); cbindgen::Builder::new() - .with_config(cbindgen::Config::from_file(&config_path).expect("Failed to load cbindgen.toml")) + .with_config( + cbindgen::Config::from_file(&config_path).expect("Failed to load cbindgen.toml"), + ) .with_crate(&crate_dir) .generate() .expect("Unable to generate bindings") @@ -19,5 +20,8 @@ fn main() { println!("cargo:rerun-if-changed=src/lib.rs"); println!("cargo:rerun-if-changed=cbindgen.toml"); - println!("cargo:warning=Generated header at: {}", output_file.display()); + println!( + "cargo:warning=Generated header at: {}", + output_file.display() + ); } diff --git a/libProcessing/ffi/src/color.rs b/libProcessing/ffi/src/color.rs index 25735675d7..fc063be63b 100644 --- a/libProcessing/ffi/src/color.rs +++ b/libProcessing/ffi/src/color.rs @@ -1,5 +1,6 @@ /// A sRGB (?) color #[repr(C)] +#[derive(Debug, Clone, Copy)] pub struct Color { pub r: f32, pub g: f32, @@ -11,4 +12,4 @@ impl From for bevy::color::Color { fn from(color: Color) -> Self { bevy::color::Color::srgba(color.r, color.g, color.b, color.a) } -} \ No newline at end of file +} diff --git a/libProcessing/ffi/src/error.rs b/libProcessing/ffi/src/error.rs index 1f235580e3..ee509cd0c1 100644 --- a/libProcessing/ffi/src/error.rs +++ b/libProcessing/ffi/src/error.rs @@ -1,7 +1,10 @@ +use std::{ + cell::RefCell, + ffi::{CString, c_char}, + panic, +}; + use renderer::error::ProcessingError; -use std::cell::RefCell; -use std::ffi::{CString, c_char}; -use std::panic; thread_local! { static LAST_ERROR: RefCell> = RefCell::new(None); diff --git a/libProcessing/ffi/src/lib.rs b/libProcessing/ffi/src/lib.rs index ff62d03613..cbd010785a 100644 --- a/libProcessing/ffi/src/lib.rs +++ b/libProcessing/ffi/src/lib.rs @@ -1,5 +1,7 @@ -use crate::color::Color; use bevy::prelude::Entity; +use renderer::render::command::DrawCommand; + +use crate::color::Color; mod color; mod error; @@ -72,15 +74,40 @@ pub extern "C" fn processing_background_color(window_id: u64, color: Color) { error::check(|| renderer::background_color(window_entity, color.into())); } -/// Step the application forward. +/// Begins the draw for the given window. /// /// SAFETY: /// - Init has been called and exit has not been called. /// - This is called from the same thread as init. #[unsafe(no_mangle)] -pub extern "C" fn processing_update() { +pub extern "C" fn processing_begin_draw(window_id: u64) { error::clear_error(); - error::check(|| renderer::update()); + let window_entity = Entity::from_bits(window_id); + error::check(|| renderer::processing_begin_draw(window_entity)); +} + +/// Flushes recorded draw commands for the given window. +/// +/// SAFETY: +/// - Init has been called and exit has not been called. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_flush(window_id: u64) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + error::check(|| renderer::processing_flush(window_entity)); +} + +/// Ends the draw for the given window and presents the frame. +/// +/// SAFETY: +/// - Init has been called and exit has not been called. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_end_draw(window_id: u64) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + error::check(|| renderer::processing_end_draw(window_entity)); } /// Shuts down internal resources with given exit code, but does *not* terminate the process. @@ -93,3 +120,104 @@ pub extern "C" fn processing_exit(exit_code: u8) { error::clear_error(); error::check(|| renderer::exit(exit_code)); } + +/// Set the fill color. +/// +/// SAFETY: +/// - Init and create_surface have been called. +/// - window_id is a valid ID returned from create_surface. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_set_fill(window_id: u64, r: f32, g: f32, b: f32, a: f32) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + let color = bevy::color::Color::srgba(r, g, b, a); + error::check(|| renderer::record_command(window_entity, DrawCommand::Fill(color))); +} + +/// Set the stroke color. +/// +/// SAFETY: +/// - Init and create_surface have been called. +/// - window_id is a valid ID returned from create_surface. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_set_stroke_color(window_id: u64, r: f32, g: f32, b: f32, a: f32) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + let color = bevy::color::Color::srgba(r, g, b, a); + error::check(|| renderer::record_command(window_entity, DrawCommand::StrokeColor(color))); +} + +/// Set the stroke weight. +/// +/// SAFETY: +/// - Init and create_surface have been called. +/// - window_id is a valid ID returned from create_surface. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +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(|| renderer::record_command(window_entity, DrawCommand::StrokeWeight(weight))); +} + +/// Disable fill for subsequent shapes. +/// +/// SAFETY: +/// - Init and create_surface have been called. +/// - window_id is a valid ID returned from create_surface. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_no_fill(window_id: u64) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + error::check(|| renderer::record_command(window_entity, DrawCommand::NoFill)); +} + +/// Disable stroke for subsequent shapes. +/// +/// SAFETY: +/// - Init and create_surface have been called. +/// - window_id is a valid ID returned from create_surface. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_no_stroke(window_id: u64) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + error::check(|| renderer::record_command(window_entity, DrawCommand::NoStroke)); +} + +/// Draw a rectangle. +/// +/// SAFETY: +/// - Init and create_surface have been called. +/// - window_id is a valid ID returned from create_surface. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_rect( + window_id: u64, + x: f32, + y: f32, + w: f32, + h: f32, + tl: f32, + tr: f32, + br: f32, + bl: f32, +) { + error::clear_error(); + let window_entity = Entity::from_bits(window_id); + error::check(|| { + renderer::record_command( + window_entity, + DrawCommand::Rect { + x, + y, + w, + h, + radii: [tl, tr, br, bl], + }, + ) + }); +} diff --git a/libProcessing/renderer/Cargo.toml b/libProcessing/renderer/Cargo.toml index afda9a5da2..c78a2f9767 100644 --- a/libProcessing/renderer/Cargo.toml +++ b/libProcessing/renderer/Cargo.toml @@ -4,11 +4,12 @@ version = "0.1.0" edition = "2024" [dependencies] -tracing = "0.1" -tracing-subscriber = "0.3" bevy = { workspace = true } -thiserror = "2" +lyon = "1.0" raw-window-handle = "0.6" +thiserror = "2" +tracing = "0.1" +tracing-subscriber = "0.3" [target.'cfg(target_os = "macos")'.dependencies] objc2 = { version = "0.6", default-features = false } diff --git a/libProcessing/renderer/src/error.rs b/libProcessing/renderer/src/error.rs index aae0092ab2..9eedd9d268 100644 --- a/libProcessing/renderer/src/error.rs +++ b/libProcessing/renderer/src/error.rs @@ -2,7 +2,6 @@ use thiserror::Error; pub type Result = std::result::Result; - #[derive(Error, Debug)] pub enum ProcessingError { #[error("App was accessed from multiple threads")] @@ -15,4 +14,4 @@ pub enum ProcessingError { HandleError(#[from] raw_window_handle::HandleError), #[error("Invalid window handle provided")] InvalidWindowHandle, -} \ No newline at end of file +} diff --git a/libProcessing/renderer/src/lib.rs b/libProcessing/renderer/src/lib.rs index 2536a35295..18c3162566 100644 --- a/libProcessing/renderer/src/lib.rs +++ b/libProcessing/renderer/src/lib.rs @@ -1,29 +1,39 @@ pub mod error; +pub mod render; -use crate::error::Result; -use bevy::app::{App, AppExit}; -use bevy::log::tracing_subscriber; -use bevy::prelude::*; -use bevy::window::{RawHandleWrapper, Window, WindowRef, WindowResolution, WindowWrapper}; +use std::{cell::RefCell, num::NonZero, sync::OnceLock}; + +use bevy::{ + app::{App, AppExit}, + asset::AssetEventSystems, + camera::{CameraOutputMode, RenderTarget, visibility::RenderLayers}, + log::tracing_subscriber, + prelude::*, + window::{RawHandleWrapper, Window, WindowRef, WindowResolution, WindowWrapper}, +}; use raw_window_handle::{ DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle, WindowHandle, }; -use std::cell::RefCell; -use std::num::NonZero; -use std::sync::atomic::AtomicU32; -use std::sync::OnceLock; -use bevy::camera::RenderTarget; -use bevy::camera::visibility::RenderLayers; +use render::{activate_cameras, clear_transient_meshes, flush_draw_commands}; use tracing::debug; +use crate::{ + error::Result, + render::command::{CommandBuffer, DrawCommand}, +}; + static IS_INIT: OnceLock<()> = OnceLock::new(); -static WINDOW_COUNT: AtomicU32 = AtomicU32::new(0); thread_local! { static APP: OnceLock> = OnceLock::default(); } +#[derive(Resource, Default)] +struct WindowCount(u32); + +#[derive(Component)] +pub struct Flush; fn app(cb: impl FnOnce(&App) -> Result) -> Result { let res = APP.with(|app_lock| { @@ -180,29 +190,55 @@ pub fn create_surface( let handle_wrapper = RawHandleWrapper::new(&window_wrapper)?; let entity_id = app_mut(|app| { - let mut window = app - .world_mut() - .spawn(( - Window { - resolution: WindowResolution::new(width, height) - .with_scale_factor_override(scale_factor), - ..default() - }, - handle_wrapper, - )); - - let count = WINDOW_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + 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(), + )); + 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 + let half_width = width as f32 / 2.0; + let half_height = height as f32 / 2.0; + + let projection = OrthographicProjection { + near: -1000.0, + far: 1000.0, + viewport_origin: Vec2::new(0.0, 0.0), // top left + scaling_mode: bevy::camera::ScalingMode::Fixed { + width: width as f32, + height: height as f32, + }, + scale: 1.0, + ..OrthographicProjection::default_3d() + }; + parent.spawn(( Camera3d::default(), Camera { target: RenderTarget::Window(WindowRef::Entity(window_entity)), ..default() }, - Projection::Orthographic(OrthographicProjection::default_3d()), + Projection::Orthographic(projection), + // position camera to match coordinate system + Transform::from_xyz(half_width, -half_height, 999.0) + .looking_at(Vec3::new(half_width, -half_height, 0.0), Vec3::Y), render_layer, )); }); @@ -213,11 +249,12 @@ pub fn create_surface( Ok(entity_id) } -pub fn destroy_surface(window_entity: Entity) -> Result<()>{ +pub fn destroy_surface(window_entity: Entity) -> Result<()> { app_mut(|app| { if app.world_mut().get::(window_entity).is_some() { app.world_mut().despawn(window_entity); - WINDOW_COUNT.fetch_sub(1, std::sync::atomic::Ordering::SeqCst); + let mut window_count = app.world_mut().resource_mut::(); + window_count.0 = window_count.0.saturating_sub(1); } Ok(()) }) @@ -267,6 +304,13 @@ pub fn init() -> Result<()> { }), ); + // resources + app.init_resource::(); + + // rendering + app.add_systems(First, (clear_transient_meshes, activate_cameras)) + .add_systems(Update, flush_draw_commands.before(AssetEventSystems)); + // this does not mean, as one might imagine, that the app is "done", but rather is part // of bevy's plugin lifecycle prior to "starting" the app. we are manually driving the app // so we don't need to call `app.run()` @@ -278,9 +322,63 @@ pub fn init() -> Result<()> { Ok(()) } -pub fn update() -> Result<()> { + +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)? + }; +} + +macro_rules! window_mut { + ($app:expr, $window_entity:expr) => { + $app.world_mut() + .get_entity_mut($window_entity) + .map_err(|_| error::ProcessingError::WindowNotFound)? + }; +} + +pub fn begin_draw(_window_entity: Entity) -> Result<()> { + app_mut(|_app| Ok(())) +} + +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::(); + + // ensure that the intermediate texture is not cleared + camera_mut!(app, window_entity).clear_color = ClearColorConfig::None; + Ok(()) + }) +} + +pub fn end_draw(window_entity: Entity) -> Result<()> { + // since we are ending the draw, set the camera to write to the output render target + 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 + app_mut(|app| { + camera_mut!(app, window_entity).output_mode = CameraOutputMode::Skip; Ok(()) }) } @@ -315,3 +413,15 @@ fn setup_tracing() -> Result<()> { tracing::subscriber::set_global_default(subscriber)?; Ok(()) } + +/// 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(()) + }) +} diff --git a/libProcessing/renderer/src/render/command.rs b/libProcessing/renderer/src/render/command.rs new file mode 100644 index 0000000000..8965aaea04 --- /dev/null +++ b/libProcessing/renderer/src/render/command.rs @@ -0,0 +1,38 @@ +use bevy::prelude::*; + +#[derive(Debug, Clone)] +pub enum DrawCommand { + Fill(Color), + NoFill, + StrokeColor(Color), + NoStroke, + StrokeWeight(f32), + Rect { + x: f32, + y: f32, + w: f32, + h: f32, + radii: [f32; 4], // [tl, tr, br, bl] + }, +} + +#[derive(Debug, Default, Component)] +pub struct CommandBuffer { + pub commands: Vec, +} + +impl CommandBuffer { + pub fn new() -> Self { + Self { + commands: Vec::new(), + } + } + + pub fn push(&mut self, cmd: DrawCommand) { + self.commands.push(cmd); + } + + pub fn clear(&mut self) { + self.commands.clear(); + } +} diff --git a/libProcessing/renderer/src/render/material.rs b/libProcessing/renderer/src/render/material.rs new file mode 100644 index 0000000000..9529e8ec41 --- /dev/null +++ b/libProcessing/renderer/src/render/material.rs @@ -0,0 +1,22 @@ +use bevy::{prelude::*, render::alpha::AlphaMode}; + +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub struct MaterialKey { + pub transparent: bool, +} + +impl MaterialKey { + pub fn to_material(&self) -> StandardMaterial { + StandardMaterial { + base_color: Color::WHITE, + unlit: true, + cull_mode: None, + alpha_mode: if self.transparent { + AlphaMode::Blend + } else { + AlphaMode::Opaque + }, + ..default() + } + } +} diff --git a/libProcessing/renderer/src/render/mesh_builder.rs b/libProcessing/renderer/src/render/mesh_builder.rs new file mode 100644 index 0000000000..ce3a5ae58a --- /dev/null +++ b/libProcessing/renderer/src/render/mesh_builder.rs @@ -0,0 +1,101 @@ +use bevy::{ + mesh::{Indices, VertexAttributeValues}, + prelude::*, +}; +use lyon::tessellation::{ + FillVertex, StrokeVertex, VertexId, + geometry_builder::{ + FillGeometryBuilder, GeometryBuilder, GeometryBuilderError, StrokeGeometryBuilder, + }, +}; + +pub struct MeshBuilder<'a> { + mesh: &'a mut Mesh, + color: Color, + begin_vertex_count: u32, +} + +impl<'a> MeshBuilder<'a> { + pub fn new(mesh: &'a mut Mesh, color: Color) -> Self { + Self { + mesh, + color, + begin_vertex_count: 0, + } + } + + fn push_vertex(&mut self, position: [f32; 3]) -> VertexId { + let id = VertexId::from_usize(self.vertex_count()); + + if let Some(VertexAttributeValues::Float32x3(positions)) = + self.mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION) + { + positions.push(position); + } + + if let Some(VertexAttributeValues::Float32x4(colors)) = + self.mesh.attribute_mut(Mesh::ATTRIBUTE_COLOR) + { + colors.push(self.color.to_srgba().to_f32_array()); + } + + if let Some(VertexAttributeValues::Float32x3(normals)) = + self.mesh.attribute_mut(Mesh::ATTRIBUTE_NORMAL) + { + normals.push([0.0, 0.0, 1.0]); // flat normal for 2d + } + + id + } + + fn push_index(&mut self, index: u32) { + if let Some(Indices::U32(indices)) = self.mesh.indices_mut() { + indices.push(index); + } + } + + fn vertex_count(&self) -> usize { + if let Some(VertexAttributeValues::Float32x3(positions)) = + self.mesh.attribute(Mesh::ATTRIBUTE_POSITION) + { + positions.len() + } else { + 0 + } + } +} + +impl<'a> GeometryBuilder for MeshBuilder<'a> { + fn begin_geometry(&mut self) { + self.begin_vertex_count = self.vertex_count() as u32; + } + + fn add_triangle(&mut self, a: VertexId, b: VertexId, c: VertexId) { + self.push_index(a.to_usize() as u32); + self.push_index(b.to_usize() as u32); + self.push_index(c.to_usize() as u32); + } + + fn abort_geometry(&mut self) { + todo!("Implement abort_geometry if needed"); + } +} + +impl<'a> FillGeometryBuilder for MeshBuilder<'a> { + fn add_fill_vertex(&mut self, vertex: FillVertex) -> Result { + let pos = vertex.position(); + let position = [pos.x, pos.y, 0.0]; + Ok(self.push_vertex(position)) + } +} + +impl<'a> StrokeGeometryBuilder for MeshBuilder<'a> { + fn add_stroke_vertex( + &mut self, + vertex: StrokeVertex, + ) -> Result { + let pos = vertex.position(); + let position = [pos.x, pos.y, 0.0]; + Ok(self.push_vertex(position)) + } +} diff --git a/libProcessing/renderer/src/render/mod.rs b/libProcessing/renderer/src/render/mod.rs new file mode 100644 index 0000000000..98222a5366 --- /dev/null +++ b/libProcessing/renderer/src/render/mod.rs @@ -0,0 +1,238 @@ +pub mod command; +pub mod material; +pub mod mesh_builder; +mod primitive; + +use bevy::{camera::visibility::RenderLayers, ecs::system::SystemParam, prelude::*}; +use command::{CommandBuffer, DrawCommand}; +use material::MaterialKey; +use primitive::{TessellationMode, empty_mesh}; + +use crate::{Flush, render::primitive::rect}; + +#[derive(Component)] +pub struct TransientMesh; + +#[derive(SystemParam)] +pub struct RenderContext<'w, 's> { + commands: Commands<'w, 's>, + meshes: ResMut<'w, Assets>, + materials: ResMut<'w, Assets>, + batch: Local<'s, BatchState>, + state: Local<'s, RenderState>, +} + +#[derive(Default)] +struct BatchState { + current_mesh: Option, + material_key: Option, + draw_index: u32, + render_layers: RenderLayers, + surface_entity: Option, +} + +#[derive(Debug)] +pub struct RenderState { + // drawing state + pub fill_color: Option, + pub stroke_color: Option, + pub stroke_weight: f32, +} + +impl Default for RenderState { + fn default() -> Self { + Self { + fill_color: Some(Color::WHITE), + stroke_color: Some(Color::BLACK), + stroke_weight: 1.0, + } + } +} + +impl RenderState { + pub fn new() -> Self { + Self::default() + } + + pub fn has_fill(&self) -> bool { + self.fill_color.is_some() + } + + pub fn has_stroke(&self) -> bool { + self.stroke_color.is_some() + } + + pub fn fill_is_transparent(&self) -> bool { + self.fill_color.map(|c| c.alpha() < 1.0).unwrap_or(false) + } + + pub fn stroke_is_transparent(&self) -> bool { + self.stroke_color.map(|c| c.alpha() < 1.0).unwrap_or(false) + } +} + +pub fn flush_draw_commands( + mut ctx: RenderContext, + mut query: Query<(Entity, &mut CommandBuffer, &RenderLayers), With>, +) { + for (surface_entity, mut cmd_buffer, render_layers) in query.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.draw_index = 0; // Reset draw index for each flush + + for cmd in draw_commands { + match cmd { + DrawCommand::Fill(color) => { + ctx.state.fill_color = Some(color); + } + DrawCommand::NoFill => { + ctx.state.fill_color = None; + } + DrawCommand::StrokeColor(color) => { + ctx.state.stroke_color = Some(color); + } + DrawCommand::NoStroke => { + ctx.state.stroke_color = None; + } + DrawCommand::StrokeWeight(weight) => { + ctx.state.stroke_weight = weight; + } + DrawCommand::Rect { x, y, w, h, radii } => { + add_fill(&mut ctx, |mesh, color| { + rect(mesh, x, y, w, h, radii, color, TessellationMode::Fill) + }); + + add_stroke(&mut ctx, |mesh, color, weight| { + rect( + mesh, + x, + y, + w, + h, + radii, + color, + TessellationMode::Stroke(weight), + ) + }); + } + } + } + + flush_batch(&mut ctx); + } +} + +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 clear_transient_meshes( + mut commands: Commands, + surfaces: Query<&Children, With>, + transient_meshes: Query<(), With>, +) { + // for all flushing surfaces, despawn all transient meshes that rendered in a previous frame + for children in surfaces.iter() { + for child in children.iter() { + if transient_meshes.contains(child) { + commands.entity(child).despawn(); + } + } + } +} + +fn spawn_mesh(ctx: &mut RenderContext, mesh: Mesh, z_offset: Option) { + let Some(material_key) = &ctx.batch.material_key else { + return; + }; + let Some(surface_entity) = ctx.batch.surface_entity else { + return; + }; + + let mesh_handle = ctx.meshes.add(mesh); + let material_handle = ctx.materials.add(material_key.to_material()); + + let components = ( + Mesh3d(mesh_handle), + MeshMaterial3d(material_handle), + TransientMesh, + ctx.batch.render_layers.clone(), + ); + + let mesh_id = if let Some(z) = z_offset { + ctx.commands + .spawn((components, Transform::from_xyz(0.0, 0.0, z))) + .id() + } else { + ctx.commands.spawn(components).id() + }; + + ctx.commands.entity(surface_entity).add_child(mesh_id); +} + +fn add_fill(ctx: &mut RenderContext, tessellate: impl FnOnce(&mut Mesh, Color)) { + let Some(color) = ctx.state.fill_color else { + return; + }; + let material_key = MaterialKey { + transparent: ctx.state.fill_is_transparent(), + }; + + // when the material changes, flush the current batch + if ctx.batch.material_key.as_ref() != Some(&material_key) { + flush_batch(ctx); + ctx.batch.material_key = Some(material_key); + ctx.batch.current_mesh = Some(empty_mesh()); + } + + // accumulate geometry into the current mega mesh + if let Some(ref mut mesh) = ctx.batch.current_mesh { + tessellate(mesh, color); + } +} + +fn add_stroke(ctx: &mut RenderContext, tessellate: impl FnOnce(&mut Mesh, Color, f32)) { + let Some(color) = ctx.state.stroke_color else { + return; + }; + let stroke_weight = ctx.state.stroke_weight; + let material_key = MaterialKey { + transparent: ctx.state.stroke_is_transparent(), + }; + + // when the material changes, flush the current batch + if ctx.batch.material_key.as_ref() != Some(&material_key) { + flush_batch(ctx); + ctx.batch.material_key = Some(material_key); + ctx.batch.current_mesh = Some(empty_mesh()); + } + + // accumulate geometry into the current mega mesh + if let Some(ref mut mesh) = ctx.batch.current_mesh { + tessellate(mesh, color, stroke_weight); + } +} + +fn flush_batch(ctx: &mut RenderContext) { + if let Some(mesh) = ctx.batch.current_mesh.take() { + // we defensively apply a small z-offset based on draw_index to preserve painter's algorithm + let z_offset = ctx.batch.draw_index as f32 * -0.001; + spawn_mesh(ctx, mesh, Some(z_offset)); + ctx.batch.draw_index += 1; + } + ctx.batch.material_key = None; +} diff --git a/libProcessing/renderer/src/render/primitive/mod.rs b/libProcessing/renderer/src/render/primitive/mod.rs new file mode 100644 index 0000000000..3d47dcd94e --- /dev/null +++ b/libProcessing/renderer/src/render/primitive/mod.rs @@ -0,0 +1,58 @@ +mod rect; + +use bevy::{ + asset::RenderAssetUsages, + mesh::{Indices, PrimitiveTopology}, + prelude::*, +}; +use lyon::{ + path::Path, + tessellation::{ + FillOptions, FillTessellator, LineCap, LineJoin, StrokeOptions, StrokeTessellator, + }, +}; +pub use rect::rect; + +use super::mesh_builder::MeshBuilder; + +pub enum TessellationMode { + Fill, + Stroke(f32), +} + +pub fn tessellate_path(mesh: &mut Mesh, path: &Path, color: Color, mode: TessellationMode) { + let mut builder = MeshBuilder::new(mesh, color); + match mode { + TessellationMode::Fill => { + let mut tessellator = FillTessellator::new(); + tessellator + .tessellate_path(path, &FillOptions::default(), &mut builder) + .expect("Failed to tessellate fill"); + } + TessellationMode::Stroke(weight) => { + let mut tessellator = StrokeTessellator::new(); + let options = StrokeOptions::default() + .with_line_width(weight) + .with_line_cap(LineCap::Round) + .with_line_join(LineJoin::Round); + + tessellator + .tessellate_path(path, &options, &mut builder) + .expect("Failed to tessellate stroke"); + } + } +} + +pub fn empty_mesh() -> Mesh { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ); + + mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, Vec::<[f32; 3]>::new()); + mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, Vec::<[f32; 4]>::new()); + mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, Vec::<[f32; 3]>::new()); + mesh.insert_indices(Indices::U32(Vec::new())); + + mesh +} diff --git a/libProcessing/renderer/src/render/primitive/rect.rs b/libProcessing/renderer/src/render/primitive/rect.rs new file mode 100644 index 0000000000..b176df0a21 --- /dev/null +++ b/libProcessing/renderer/src/render/primitive/rect.rs @@ -0,0 +1,112 @@ +use bevy::{ + mesh::{Indices, VertexAttributeValues}, + prelude::*, +}; +use lyon::{geom::Point, path::Path}; + +use crate::render::primitive::{TessellationMode, tessellate_path}; + +fn rect_path(x: f32, y: f32, w: f32, h: f32, radii: [f32; 4]) -> Path { + let mut path_builder = Path::builder(); + let [tl, tr, br, bl] = radii; + + // tl + path_builder.begin(Point::new(x + tl, y)); + + // tl -> tr + if tr > 0.0 { + path_builder.line_to(Point::new(x + w - tr, y)); + path_builder.quadratic_bezier_to(Point::new(x + w, y), Point::new(x + w, y + tr)); + } else { + path_builder.line_to(Point::new(x + w, y)); + } + + // tr -> br + if br > 0.0 { + path_builder.line_to(Point::new(x + w, y + h - br)); + path_builder.quadratic_bezier_to(Point::new(x + w, y + h), Point::new(x + w - br, y + h)); + } else { + path_builder.line_to(Point::new(x + w, y + h)); + } + + // br -> bl + if bl > 0.0 { + path_builder.line_to(Point::new(x + bl, y + h)); + path_builder.quadratic_bezier_to(Point::new(x, y + h), Point::new(x, y + h - bl)); + } else { + path_builder.line_to(Point::new(x, y + h)); + } + + // bl -> tl + if tl > 0.0 { + path_builder.line_to(Point::new(x, y + tl)); + path_builder.quadratic_bezier_to(Point::new(x, y), Point::new(x + tl, y)); + } + + path_builder.end(true); + path_builder.build() +} + +pub fn rect( + mesh: &mut Mesh, + x: f32, + y: f32, + w: f32, + h: f32, + radii: [f32; 4], // [tl, tr, br, bl] + color: Color, + mode: TessellationMode, +) { + if radii == [0.0; 4] && matches!(mode, TessellationMode::Fill) { + simple_rect(mesh, x, y, w, h, color); + } else { + let path = rect_path(x, y, w, h, radii); + tessellate_path(mesh, &path, color, mode); + } +} + +fn simple_rect(mesh: &mut Mesh, x: f32, y: f32, w: f32, h: f32, color: Color) { + let base_idx = if let Some(VertexAttributeValues::Float32x3(positions)) = + mesh.attribute(Mesh::ATTRIBUTE_POSITION) + { + positions.len() as u32 + } else { + 0 + }; + + if let Some(VertexAttributeValues::Float32x3(positions)) = + mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION) + { + positions.push([x, y, 0.0]); + positions.push([x + w, y, 0.0]); + positions.push([x + w, y + h, 0.0]); + positions.push([x, y + h, 0.0]); + } + + if let Some(VertexAttributeValues::Float32x4(colors)) = + mesh.attribute_mut(Mesh::ATTRIBUTE_COLOR) + { + let color_array = color.to_srgba().to_f32_array(); + for _ in 0..4 { + colors.push(color_array); + } + } + + if let Some(VertexAttributeValues::Float32x3(normals)) = + mesh.attribute_mut(Mesh::ATTRIBUTE_NORMAL) + { + for _ in 0..4 { + normals.push([0.0, 0.0, 1.0]); + } + } + + if let Some(Indices::U32(indices)) = mesh.indices_mut() { + indices.push(base_idx + 0); + indices.push(base_idx + 1); + indices.push(base_idx + 2); + + indices.push(base_idx + 0); + indices.push(base_idx + 2); + indices.push(base_idx + 3); + } +}