From 6fcb496fd5d661729f94994e9c600d37ab730f11 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Wed, 18 Jun 2025 02:06:32 +0100 Subject: [PATCH] Upgrade Blitz to `0.1.0-alpha.3` and add wgpu-texture example (#4286) * Use Blitz 0.1.0-alpha.3 * Add wgpu-texture example * Native: Support configuring wgpu Features and Limits --- Cargo.lock | 165 +++++-------- Cargo.toml | 1 + examples/wgpu-texture/Cargo.toml | 21 ++ examples/wgpu-texture/src/demo_renderer.rs | 254 +++++++++++++++++++++ examples/wgpu-texture/src/main.rs | 106 +++++++++ examples/wgpu-texture/src/shader.wgsl | 111 +++++++++ examples/wgpu-texture/src/styles.css | 60 +++++ packages/native/Cargo.toml | 18 +- packages/native/src/dioxus_application.rs | 67 +++--- packages/native/src/dioxus_renderer.rs | 94 ++++++++ packages/native/src/lib.rs | 61 ++++- 11 files changed, 796 insertions(+), 162 deletions(-) create mode 100644 examples/wgpu-texture/Cargo.toml create mode 100644 examples/wgpu-texture/src/demo_renderer.rs create mode 100644 examples/wgpu-texture/src/main.rs create mode 100644 examples/wgpu-texture/src/shader.wgsl create mode 100644 examples/wgpu-texture/src/styles.css create mode 100644 packages/native/src/dioxus_renderer.rs diff --git a/Cargo.lock b/Cargo.lock index aa47f7c12..07763f2eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,9 +381,9 @@ checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" [[package]] name = "anyrender" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff8da5f4a3e508ed376118a7e04ed992df07630fb47d7b3dce8d1f92427975d" +checksum = "d137e07d59da00378857d3b740e34d9ebedd339e181a23e85d3f936c51b9797e" dependencies = [ "kurbo", "peniko", @@ -392,9 +392,9 @@ dependencies = [ [[package]] name = "anyrender_svg" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4197139c97b2d204e44fd79c58e16f1c6023d18fe3db1e3eebfa335f78e39072" +checksum = "eb57f18ae458825c213b5c359fb8bc1466a80b89f2c9e2f03812b021fc87b3ea" dependencies = [ "anyrender", "image", @@ -406,9 +406,9 @@ dependencies = [ [[package]] name = "anyrender_vello" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e99c7f0936960037aa11d64bf7c9c5cf2f624dd9faf8c57a6581b056e08852" +checksum = "3807802b3aeb615b281a393400fd31be883944334d153829c10cffb087c78b97" dependencies = [ "anyrender", "futures-intrusive", @@ -416,6 +416,7 @@ dependencies = [ "peniko", "pollster", "raw-window-handle 0.6.2", + "rustc-hash 1.1.0", "vello", "wgpu 24.0.5", ] @@ -1023,9 +1024,6 @@ name = "atomic_refcell" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" -dependencies = [ - "serde", -] [[package]] name = "atspi" @@ -1604,9 +1602,9 @@ dependencies = [ [[package]] name = "blitz-dom" -version = "0.1.0-alpha.2" +version = "0.1.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dc174ecca5c3e94b162335be4c9e17b35a0b421898ea559cb7f5926ff0fe5c6" +checksum = "c2cc790ddadcbdb833c644e39d1d0bbda4f83e6a45c513c04c91bbe093cd98a3" dependencies = [ "accesskit", "app_units", @@ -1626,7 +1624,6 @@ dependencies = [ "selectors 0.28.0", "slab", "smallvec", - "string_cache", "stylo", "stylo_config", "stylo_dom", @@ -1640,9 +1637,9 @@ dependencies = [ [[package]] name = "blitz-net" -version = "0.1.0-alpha.2" +version = "0.1.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b2788c54be9856d8a13a8bfcf0f184573d7169fdb6bf3f867cef87eca48874" +checksum = "5da042609b43982f5aa269f55c19a10579ecad2a18361b825f28c84981b64835" dependencies = [ "blitz-traits", "data-url 0.3.1", @@ -1652,9 +1649,9 @@ dependencies = [ [[package]] name = "blitz-paint" -version = "0.1.0-alpha.2" +version = "0.1.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dd45f19bf9db46e67282bd24e764904c9624458fc4f71ec6566cc916e43ef36" +checksum = "a9592d2235bdd5d840ed1e212d9337324d9e43fc11ff3fa5dd723d280c63f85c" dependencies = [ "anyrender", "anyrender_svg", @@ -1673,9 +1670,9 @@ dependencies = [ [[package]] name = "blitz-shell" -version = "0.1.0-alpha.2" +version = "0.1.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac5e9acd11cd37fee6819686eb4893ae175e2c2563865fb823a9811418a90541" +checksum = "5d120ea1f2676bf09ba5c324d0904acc1af2337931b9c32d68b68f1bf5160512" dependencies = [ "accesskit", "accesskit_winit", @@ -1686,16 +1683,15 @@ dependencies = [ "blitz-traits", "futures-util", "keyboard-types", - "muda 0.11.5", "tracing", "winit", ] [[package]] name = "blitz-traits" -version = "0.1.0-alpha.2" +version = "0.1.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d0ce7b7e5e2f49321dc4cec28ea7e83b7af0a287eb705674e36a9ac8f9691b9" +checksum = "6f7891c8d148d85371940a4baa48a29a09a039136f14e8c83714f855b3743e91" dependencies = [ "bitflags 2.9.1", "bytes", @@ -2414,22 +2410,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cocoa" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation 0.1.2", - "core-foundation 0.9.4", - "core-graphics 0.23.2", - "foreign-types 0.5.0", - "libc", - "objc", -] - [[package]] name = "cocoa" version = "0.26.1" @@ -2438,7 +2418,7 @@ checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" dependencies = [ "bitflags 2.9.1", "block", - "cocoa-foundation 0.2.1", + "cocoa-foundation", "core-foundation 0.10.0", "core-graphics 0.24.0", "foreign-types 0.5.0", @@ -2446,20 +2426,6 @@ dependencies = [ "objc", ] -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "libc", - "objc", -] - [[package]] name = "cocoa-foundation" version = "0.2.1" @@ -3954,7 +3920,7 @@ version = "0.7.0-alpha.1" dependencies = [ "async-trait", "base64 0.22.1", - "cocoa 0.26.1", + "cocoa", "core-foundation 0.10.0", "dioxus", "dioxus-asset-resolver", @@ -3979,7 +3945,7 @@ dependencies = [ "jni", "lazy-js-bundle", "libc", - "muda 0.16.1", + "muda", "ndk", "ndk-context", "ndk-sys 0.6.0+11769913", @@ -5413,23 +5379,23 @@ dependencies = [ [[package]] name = "fontdb" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a6f9af55fb97ad673fb7a69533eb2f967648a06fa21f8c9bb2cd6d33975716" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" dependencies = [ "fontconfig-parser", "log", "memmap2", "slotmap", "tinyvec", - "ttf-parser 0.24.1", + "ttf-parser", ] [[package]] name = "fontique" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64763d1f274c8383333851435b6cdf071c31cfcdb39fd5860d20943205a007a7" +checksum = "39f97079e1293b8c1e9fb03a2875d328bd2ee8f3b95ce62959c0acc04049c708" dependencies = [ "bytemuck", "fontconfig-cache-parser", @@ -8965,24 +8931,6 @@ dependencies = [ "nasm-rs", ] -[[package]] -name = "muda" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c47e7625990fc1af2226ea4f34fb2412b03c12639fcb91868581eb3a6893453" -dependencies = [ - "cocoa 0.25.0", - "crossbeam-channel", - "gtk", - "keyboard-types", - "objc", - "once_cell", - "png", - "serde", - "thiserror 1.0.69", - "windows-sys 0.52.0", -] - [[package]] name = "muda" version = "0.16.1" @@ -10007,7 +9955,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" dependencies = [ - "ttf-parser 0.25.1", + "ttf-parser", ] [[package]] @@ -10162,9 +10110,9 @@ dependencies = [ [[package]] name = "parley" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28dadbe655332fd7d996794ec8d0c376695f6ca47bc75aa01e0967c7f28e42a" +checksum = "13e57638545cf2ba4c3e72cc5715e53b1880b829cc3dbefda3d1700c58efe723" dependencies = [ "fontique", "hashbrown 0.15.3", @@ -11408,9 +11356,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.29.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e85935612710191461ec9df47b4b5880dd6359d4fad3b2f2ed696215f6f3146" +checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d" dependencies = [ "bytemuck", "font-types", @@ -12035,16 +11983,16 @@ checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "rustybuzz" -version = "0.18.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ "bitflags 2.9.1", "bytemuck", "core_maths", "log", "smallvec", - "ttf-parser 0.24.1", + "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", "unicode-properties", @@ -12762,9 +12710,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "skrifa" -version = "0.31.1" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c3bb8cab5196b98d70c866ce1ea81ab516104d5b396f84ae80f8766b5d5b4e" +checksum = "dbeb4ca4399663735553a09dd17ce7e49a0a0203f03b706b39628c4d913a8607" dependencies = [ "bytemuck", "read-fonts", @@ -13522,9 +13470,9 @@ checksum = "9600d11cdad6c275426272f35fbe085c4362be1d67bb9775f378fd2be7942d3a" [[package]] name = "stylo_taffy" -version = "0.1.0-alpha.2" +version = "0.1.0-alpha.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c0f66aad13b557aafcf971220a04150228604da2b377f0b2aa6a03113adfac7" +checksum = "d30240f1de39be7dc0b645febe4247bbe10f018c0e42875717ee1d45bb35cbdf" dependencies = [ "stylo", "taffy", @@ -15113,7 +15061,7 @@ dependencies = [ "crossbeam-channel", "dirs 6.0.0", "libappindicator", - "muda 0.16.1", + "muda", "objc2 0.6.1", "objc2-app-kit 0.3.1", "objc2-core-foundation", @@ -15157,20 +15105,14 @@ dependencies = [ "toml", ] -[[package]] -name = "ttf-parser" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a" -dependencies = [ - "core_maths", -] - [[package]] name = "ttf-parser" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] [[package]] name = "tungstenite" @@ -15363,9 +15305,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi-mirroring" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" [[package]] name = "unicode-bom" @@ -15375,9 +15317,9 @@ checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" [[package]] name = "unicode-ccc" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-id" @@ -15549,9 +15491,9 @@ dependencies = [ [[package]] name = "usvg" -version = "0.44.0" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" dependencies = [ "base64 0.22.1", "data-url 0.3.1", @@ -16572,6 +16514,17 @@ dependencies = [ "windows-core 0.58.0", ] +[[package]] +name = "wgpu-texture" +version = "0.0.0" +dependencies = [ + "bytemuck", + "color", + "dioxus", + "dioxus-native", + "wgpu 24.0.5", +] + [[package]] name = "wgpu-types" version = "0.19.2" @@ -17367,7 +17320,7 @@ checksum = "ac0099a336829fbf54c26b5f620c68980ebbe37196772aeaf6118df4931b5cb0" dependencies = [ "base64 0.22.1", "block", - "cocoa 0.26.1", + "cocoa", "core-graphics 0.24.0", "crossbeam-channel", "dpi", diff --git a/Cargo.toml b/Cargo.toml index 874c6f83e..d8ce18701 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,6 +113,7 @@ members = [ "examples/fullstack-desktop", "examples/fullstack-auth", "examples/fullstack-websockets", + "examples/wgpu-texture", # Playwright tests "packages/playwright-tests/liveview", diff --git a/examples/wgpu-texture/Cargo.toml b/examples/wgpu-texture/Cargo.toml new file mode 100644 index 000000000..870ed04bb --- /dev/null +++ b/examples/wgpu-texture/Cargo.toml @@ -0,0 +1,21 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: MIT + +[package] +name = "wgpu-texture" +version = "0.0.0" +edition = "2021" +license = "MIT" +publish = false + +[features] +default = ["desktop"] +desktop = ["dioxus/desktop"] +native = ["dioxus/native"] + +[dependencies] +dioxus-native = { path = "../../packages/native" } +dioxus = { workspace = true } +wgpu = "24" +bytemuck = "1" +color = "0.3" diff --git a/examples/wgpu-texture/src/demo_renderer.rs b/examples/wgpu-texture/src/demo_renderer.rs new file mode 100644 index 000000000..46497fc67 --- /dev/null +++ b/examples/wgpu-texture/src/demo_renderer.rs @@ -0,0 +1,254 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT +use crate::Color; +use dioxus_native::{CustomPaintCtx, CustomPaintSource, TextureHandle}; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::time::Instant; +use wgpu::{Device, Queue}; + +pub struct DemoPaintSource { + state: DemoRendererState, + start_time: std::time::Instant, + tx: Sender, + rx: Receiver, + color: Color, +} + +impl CustomPaintSource for DemoPaintSource { + fn resume(&mut self, device: &Device, queue: &Queue) { + // TODO: work out what to do about width/height + let active_state = ActiveDemoRenderer::new(device, queue); + self.state = DemoRendererState::Active(Box::new(active_state)); + } + + fn suspend(&mut self) { + self.state = DemoRendererState::Suspended; + } + + fn render( + &mut self, + ctx: CustomPaintCtx<'_>, + width: u32, + height: u32, + _scale: f64, + ) -> Option { + self.process_messages(); + self.render(ctx, width, height) + } +} + +pub enum DemoMessage { + // Color in RGB format + SetColor(Color), +} + +enum DemoRendererState { + Active(Box), + Suspended, +} + +#[derive(Clone)] +struct TextureAndHandle { + texture: wgpu::Texture, + handle: TextureHandle, +} + +struct ActiveDemoRenderer { + device: wgpu::Device, + queue: wgpu::Queue, + pipeline: wgpu::RenderPipeline, + displayed_texture: Option, + next_texture: Option, +} + +impl DemoPaintSource { + pub fn new() -> Self { + let (tx, rx) = channel(); + Self::with_channel(tx, rx) + } + + pub fn with_channel(tx: Sender, rx: Receiver) -> Self { + Self { + state: DemoRendererState::Suspended, + start_time: std::time::Instant::now(), + tx, + rx, + color: Color::WHITE, + } + } + + pub fn sender(&self) -> Sender { + self.tx.clone() + } + + fn process_messages(&mut self) { + loop { + match self.rx.try_recv() { + Err(_) => return, + Ok(msg) => match msg { + DemoMessage::SetColor(color) => self.color = color, + }, + } + } + } + + fn render( + &mut self, + ctx: CustomPaintCtx<'_>, + width: u32, + height: u32, + ) -> Option { + if width == 0 || height == 0 { + return None; + } + let DemoRendererState::Active(state) = &mut self.state else { + return None; + }; + + state.render(ctx, self.color.components, width, height, &self.start_time) + } +} + +impl ActiveDemoRenderer { + pub(crate) fn new(device: &Device, queue: &Queue) -> Self { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: None, + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!( + "shader.wgsl" + ))), + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[], + push_constant_ranges: &[wgpu::PushConstantRange { + stages: wgpu::ShaderStages::FRAGMENT, + range: 0..16, // full size in bytes, aligned + }], + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(wgpu::TextureFormat::Rgba8Unorm.into())], + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + + Self { + device: device.clone(), + queue: queue.clone(), + pipeline, + displayed_texture: None, + next_texture: None, + } + } + + pub(crate) fn render( + &mut self, + mut ctx: CustomPaintCtx<'_>, + light: [f32; 3], + width: u32, + height: u32, + start_time: &Instant, + ) -> Option { + // If "next texture" size doesn't match specified size then unregister and drop texture + if let Some(next) = &self.next_texture { + if next.texture.width() != width || next.texture.height() != height { + ctx.unregister_texture(next.handle); + self.next_texture = None; + } + } + + // If there is no "next texture" then create one and register it. + let texture_and_handle = match &self.next_texture { + Some(next) => next, + None => { + let texture = create_texture(&self.device, width, height); + let handle = ctx.register_texture(texture.clone()); + self.next_texture = Some(TextureAndHandle { texture, handle }); + self.next_texture.as_ref().unwrap() + } + }; + + let next_texture = &texture_and_handle.texture; + let next_texture_handle = texture_and_handle.handle; + + let elapsed: f32 = start_time.elapsed().as_millis() as f32 / 500.; + let [light_red, light_green, light_blue] = light; + let push_constants = PushConstants { + light_color_and_time: [light_red, light_green, light_blue, elapsed], + }; + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &next_texture.create_view(&wgpu::TextureViewDescriptor::default()), + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::GREEN), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + rpass.set_pipeline(&self.pipeline); + rpass.set_push_constants( + wgpu::ShaderStages::FRAGMENT, // Stage (your constants are for fragment shader) + 0, // Offset in bytes (start at 0) + bytemuck::bytes_of(&push_constants), + ); + rpass.draw(0..3, 0..1); + } + + self.queue.submit(Some(encoder.finish())); + + std::mem::swap(&mut self.next_texture, &mut self.displayed_texture); + Some(next_texture_handle) + } +} + +#[repr(C)] +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +struct PushConstants { + light_color_and_time: [f32; 4], +} + +fn create_texture(device: &wgpu::Device, width: u32, height: u32) -> wgpu::Texture { + device.create_texture(&wgpu::TextureDescriptor { + label: None, + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }) +} diff --git a/examples/wgpu-texture/src/main.rs b/examples/wgpu-texture/src/main.rs new file mode 100644 index 000000000..c145709e0 --- /dev/null +++ b/examples/wgpu-texture/src/main.rs @@ -0,0 +1,106 @@ +use color::{palette::css::WHITE, parse_color}; +use color::{OpaqueColor, Srgb}; +use demo_renderer::{DemoMessage, DemoPaintSource}; +use dioxus::prelude::*; +use dioxus_native::use_wgpu; +use std::any::Any; +use wgpu::{Features, Limits}; + +mod demo_renderer; + +// CSS Styles +static STYLES: &str = include_str!("./styles.css"); + +// WGPU settings required by this example +const FEATURES: Features = Features::PUSH_CONSTANTS; +fn limits() -> Limits { + Limits { + max_push_constant_size: 16, + ..Limits::default() + } +} + +type Color = OpaqueColor; + +fn main() { + let config: Vec> = vec![Box::new(FEATURES), Box::new(limits())]; + dioxus_native::launch_cfg(app, Vec::new(), config); +} + +fn app() -> Element { + let mut show_cube = use_signal(|| true); + + let color_str = use_signal(|| String::from("red")); + let color = use_memo(move || { + parse_color(&color_str()) + .map(|c| c.to_alpha_color()) + .unwrap_or(WHITE) + .split() + .0 + }); + + use_effect(move || println!("{:?}", color().components)); + + rsx!( + style { {STYLES} } + div { id:"overlay", + h2 { "Control Panel" }, + button { + onclick: move |_| *show_cube.write() = !show_cube(), + if show_cube() { + "Hide cube" + } else { + "Show cube" + } + } + br {} + ColorControl { label: "Color:", color_str }, + p { "This overlay demonstrates that the custom WGPU content can be rendered beneath layers of HTML content" } + } + div { id:"underlay", + h2 { "Underlay" }, + p { "This underlay demonstrates that the custom WGPU content can be rendered above layers and blended with the content underneath" } + } + header { + h2 { "Blitz WGPU Demo" } + } + if show_cube() { + SpinningCube { color } + } + ) +} + +#[component] +fn ColorControl(label: &'static str, color_str: Signal) -> Element { + rsx!(div { + class: "color-control", + { label }, + input { + value: color_str(), + oninput: move |evt| { + *color_str.write() = evt.value() + } + } + }) +} + +#[component] +fn SpinningCube(color: Memo) -> Element { + // Create custom paint source and register it with the renderer + let paint_source = DemoPaintSource::new(); + let sender = paint_source.sender(); + let paint_source_id = use_wgpu(move || paint_source); + + use_effect(move || { + sender.send(DemoMessage::SetColor(color())).unwrap(); + }); + + rsx!( + div { id:"canvas-container", + canvas { + id: "demo-canvas", + data: paint_source_id + } + } + ) +} diff --git a/examples/wgpu-texture/src/shader.wgsl b/examples/wgpu-texture/src/shader.wgsl new file mode 100644 index 000000000..b2d686c2f --- /dev/null +++ b/examples/wgpu-texture/src/shader.wgsl @@ -0,0 +1,111 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) frag_position: vec2, +}; + +@vertex +fn vs_main( + @builtin(vertex_index) vertex_index: u32 +) -> VertexOutput { + var output: VertexOutput; + + var positions = array, 3>( + vec2(-1.0, 3.0), + vec2(-1.0, -1.0), + vec2( 3.0, -1.0) + ); + + let pos = positions[vertex_index]; + output.position = vec4(pos.x, -pos.y, 0.0, 1.0); + output.frag_position = pos; + return output; +} + +struct PushConstants { + light_color_and_time: vec4, +}; + +var pc: PushConstants; + +fn sdRoundBox(p: vec3, b: vec3, r: f32) -> f32 { + let q = abs(p) - b; + return length(max(q, vec3(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0) - r; +} + +fn rotateY(r: vec3, angle: f32) -> vec3 { + let c = cos(angle); + let s = sin(angle); + let rotation_matrix = mat3x3( + vec3( c, 0.0, s), + vec3(0.0, 1.0, 0.0), + vec3(-s, 0.0, c) + ); + return rotation_matrix * r; +} + +fn rotateZ(r: vec3, angle: f32) -> vec3 { + let c = cos(angle); + let s = sin(angle); + let rotation_matrix = mat3x3( + vec3( c, -s, 0.0), + vec3( s, c, 0.0), + vec3(0.0, 0.0, 1.0) + ); + return rotation_matrix * r; +} + +// Distance from the scene +fn scene(r: vec3) -> f32 { + let iTime = pc.light_color_and_time.w; + let pos = rotateZ(rotateY(r + vec3(-1.0, -1.0, 4.0), iTime), iTime); + let cube = vec3(0.5, 0.5, 0.5); + let edge = 0.1; + return sdRoundBox(pos, cube, edge); +} + +// https://iquilezles.org/articles/normalsSDF +fn normal(pos: vec3) -> vec3 { + let e = vec2(1.0, -1.0) * 0.5773; + let eps = 0.0005; + return normalize( + e.xyy * scene(pos + e.xyy * eps) + + e.yyx * scene(pos + e.yyx * eps) + + e.yxy * scene(pos + e.yxy * eps) + + e.xxx * scene(pos + e.xxx * eps) + ); +} + +fn render(fragCoord: vec2, light_color: vec3) -> vec4 { + var color = vec4(0.0, 0.0, 0.0, 1.0); + + var camera = vec3(1.0, 2.0, 1.0); + var p = vec3(fragCoord.x, fragCoord.y + 1.0, -1.0); + var dir = normalize(p - camera); + + var i = 0; + loop { + if (i >= 90) { break; } + let dist = scene(p); + if (dist < 0.0001) { break; } + p = p + dir * dist; + i = i + 1; + } + + let surf_normal = normal(p); + let light_position = vec3(2.0, 4.0, -0.5); + var light = 7.0 + 2.0 * dot(surf_normal, light_position); + light = light / (0.2 * pow(length(light_position - p), 3.5)); + + let alpha = select(0.0, 1.0, i < 90); + return vec4(light * light_color, alpha) * 2.0; +} + +@fragment +fn fs_main(@location(0) frag_position: vec2) -> @location(0) vec4 { + let selected_light_color = pc.light_color_and_time.xyz; + let r = vec2(0.5 * frag_position.x + 1.0, 0.5 - 0.5 * frag_position.y); + return render(r, selected_light_color); +} \ No newline at end of file diff --git a/examples/wgpu-texture/src/styles.css b/examples/wgpu-texture/src/styles.css new file mode 100644 index 000000000..588ee1c98 --- /dev/null +++ b/examples/wgpu-texture/src/styles.css @@ -0,0 +1,60 @@ +* { + box-sizing: border-box; +} + +html, body, main { + height: 100%; + font-family: system-ui, sans; + margin: 0; +} + +main { + display: grid; + grid-template-rows: 100px 1fr; + grid-template-columns: 100%; + background: #f4e8d2; +} + +#canvas-container { + display: grid; + opacity: 0.8; +} + +header { + padding: 10px 40px; + background-color: white; + z-index: 100; +} + +#overlay { + position: absolute; + width: 33%; + height: 100%; + right: 0; + z-index: 10; + background-color: rgba(0, 0, 0, 0.5); + padding-top: 40%; + padding-inline: 20px; + color: white; +} + +#underlay { + position: absolute; + width: 33%; + height: 100%; + z-index: -10; + background-color: black; + padding-top: 40%; + padding-inline: 20px; + color: white; +} + +.color-control { + display: flex; + gap: 12px; + + > input { + width: 150px; + color: black; + } +} \ No newline at end of file diff --git a/packages/native/Cargo.toml b/packages/native/Cargo.toml index b25646377..2301f1a2b 100644 --- a/packages/native/Cargo.toml +++ b/packages/native/Cargo.toml @@ -9,13 +9,11 @@ repository = "https://github.com/DioxusLabs/dioxus/" homepage = "https://dioxuslabs.com/learn/0.6/getting_started" keywords = ["dom", "ui", "gui", "react"] - [features] -default = ["accessibility", "hot-reload", "menu", "tracing", "net", "svg", "system-fonts"] +default = ["accessibility", "hot-reload", "tracing", "net", "svg", "system-fonts"] svg = ["blitz-dom/svg", "blitz-paint/svg"] net = ["dep:tokio", "dep:blitz-net"] accessibility = ["blitz-shell/accessibility", "blitz-dom/accessibility"] -menu = ["blitz-shell/menu"] tracing = ["dep:tracing", "blitz-shell/tracing", "blitz-dom/tracing"] hot-reload = ["dep:dioxus-cli-config", "dep:dioxus-devtools"] system-fonts = ["blitz-dom/system_fonts"] @@ -23,13 +21,13 @@ autofocus = [] [dependencies] # Blitz dependencies -anyrender = { version = "0.1", default-features = false } -anyrender_vello = { version = "0.1", default-features = false } -blitz-dom = { version = "0.1.0-alpha.2", default-features = false } -blitz-net = { version = "0.1.0-alpha.2", optional = true } -blitz-paint = { version = "0.1.0-alpha.2", optional = true } -blitz-traits = { version = "0.1.0-alpha.2" } -blitz-shell = { version = "0.1.0-alpha.2", default-features = false } +anyrender = { version = "0.2", default-features = false } +anyrender_vello = { version = "0.2", default-features = false } +blitz-dom = { version = "0.1.0-alpha.3", default-features = false } +blitz-net = { version = "0.1.0-alpha.3", optional = true } +blitz-paint = { version = "0.1.0-alpha.3", optional = true } +blitz-traits = { version = "0.1.0-alpha.3" } +blitz-shell = { version = "0.1.0-alpha.3", default-features = false } # DioxusLabs dependencies dioxus-core = { workspace = true } diff --git a/packages/native/src/dioxus_application.rs b/packages/native/src/dioxus_application.rs index db80b8618..e904c129f 100644 --- a/packages/native/src/dioxus_application.rs +++ b/packages/native/src/dioxus_application.rs @@ -1,16 +1,16 @@ -use anyrender_vello::VelloWindowRenderer; -use blitz_shell::BlitzApplication; -use dioxus_core::{ScopeId, VirtualDom}; +use blitz_shell::{BlitzApplication, View}; +use dioxus_core::ScopeId; use dioxus_history::{History, MemoryHistory}; -use std::{collections::HashSet, rc::Rc}; +use std::rc::Rc; use winit::application::ApplicationHandler; use winit::event::{StartCause, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoopProxy}; use winit::window::WindowId; +use crate::DioxusNativeWindowRenderer; use crate::{ - assets::DioxusNativeNetProvider, contexts::DioxusNativeDocument, - mutation_writer::MutationWriter, BlitzShellEvent, DioxusDocument, WindowConfig, + contexts::DioxusNativeDocument, mutation_writer::MutationWriter, BlitzShellEvent, + DioxusDocument, WindowConfig, }; /// Dioxus-native specific event type @@ -36,21 +36,24 @@ pub enum DioxusNativeEvent { } pub struct DioxusNativeApplication { - pending_vdom: Option, - inner: BlitzApplication, + pending_window: Option>, + inner: BlitzApplication, proxy: EventLoopProxy, } impl DioxusNativeApplication { - pub fn new(proxy: EventLoopProxy, vdom: VirtualDom) -> Self { + pub fn new( + proxy: EventLoopProxy, + config: WindowConfig, + ) -> Self { Self { - pending_vdom: Some(vdom), + pending_window: Some(config), inner: BlitzApplication::new(proxy.clone()), proxy, } } - pub fn add_window(&mut self, window_config: WindowConfig) { + pub fn add_window(&mut self, window_config: WindowConfig) { self.inner.add_window(window_config); } @@ -112,42 +115,27 @@ impl DioxusNativeApplication { impl ApplicationHandler for DioxusNativeApplication { fn resumed(&mut self, event_loop: &ActiveEventLoop) { tracing::debug!("Injecting document provider into all windows"); - let vdom = self.pending_vdom.take().unwrap(); - #[cfg(feature = "net")] - let net_provider = { - let proxy = self.proxy.clone(); - let net_provider = DioxusNativeNetProvider::shared(proxy); - Some(net_provider) - }; - - #[cfg(not(feature = "net"))] - let net_provider = None; - - // Create document + window from the baked virtualdom - let doc = DioxusDocument::new(vdom, net_provider); - let window = WindowConfig::new(Box::new(doc) as _); - - // little hack since View::init is not public - fix this once alpha-2 is out - let old_windows = self.inner.windows.keys().copied().collect::>(); - self.add_window(window); - self.inner.resumed(event_loop); - let new_windows = self.inner.windows.keys().cloned().collect::>(); - - // todo(jon): we should actually mess with the pending windows instead of passing along the contexts - for window_id in new_windows.difference(&old_windows) { - let window = self.inner.windows.get_mut(window_id).unwrap(); + if let Some(config) = self.pending_window.take() { + let mut window = View::init(config, event_loop, &self.proxy); + let renderer = window.renderer.clone(); + let window_id = window.window_id(); let doc = window.downcast_doc_mut::(); + doc.vdom.in_runtime(|| { let shared: Rc = - Rc::new(DioxusNativeDocument::new(self.proxy.clone(), *window_id)); + Rc::new(DioxusNativeDocument::new(self.proxy.clone(), window_id)); ScopeId::ROOT.provide_context(shared); }); // Add history let history_provider: Rc = Rc::new(MemoryHistory::default()); doc.vdom - .in_runtime(|| ScopeId::ROOT.provide_context(history_provider)); + .in_runtime(move || ScopeId::ROOT.provide_context(history_provider)); + + // Add renderer + doc.vdom + .in_runtime(move || ScopeId::ROOT.provide_context(renderer)); // Queue rebuild let mut writer = MutationWriter::new(&mut doc.inner, &mut doc.vdom_state); @@ -156,7 +144,12 @@ impl ApplicationHandler for DioxusNativeApplication { // And then request redraw window.request_redraw(); + + // todo(jon): we should actually mess with the pending windows instead of passing along the contexts + self.inner.windows.insert(window_id, window); } + + self.inner.resumed(event_loop); } fn suspended(&mut self, event_loop: &ActiveEventLoop) { diff --git a/packages/native/src/dioxus_renderer.rs b/packages/native/src/dioxus_renderer.rs new file mode 100644 index 000000000..b6f50abd5 --- /dev/null +++ b/packages/native/src/dioxus_renderer.rs @@ -0,0 +1,94 @@ +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Arc; + +use anyrender::WindowRenderer; + +pub use anyrender_vello::{ + wgpu::{Features, Limits}, + CustomPaintSource, VelloWindowRenderer as InnerRenderer, +}; + +pub fn use_wgpu(create_source: impl FnOnce() -> T) -> u64 { + use dioxus_core::prelude::{consume_context, use_hook_with_cleanup}; + + let (_renderer, id) = use_hook_with_cleanup( + || { + let renderer = consume_context::(); + let source = Box::new(create_source()); + let id = renderer.register_custom_paint_source(source); + (renderer, id) + }, + |(renderer, id)| { + renderer.unregister_custom_paint_source(id); + }, + ); + + id +} + +#[derive(Clone)] +pub struct DioxusNativeWindowRenderer { + inner: Rc>, +} + +impl Default for DioxusNativeWindowRenderer { + fn default() -> Self { + Self::new() + } +} + +impl DioxusNativeWindowRenderer { + pub fn new() -> Self { + let vello_renderer = InnerRenderer::new(); + Self::with_inner_renderer(vello_renderer) + } + + pub fn with_features_and_limits(features: Option, limits: Option) -> Self { + let vello_renderer = InnerRenderer::with_features_and_limits(features, limits); + Self::with_inner_renderer(vello_renderer) + } + + fn with_inner_renderer(vello_renderer: InnerRenderer) -> Self { + Self { + inner: Rc::new(RefCell::new(vello_renderer)), + } + } +} + +impl DioxusNativeWindowRenderer { + pub fn register_custom_paint_source(&self, source: Box) -> u64 { + self.inner.borrow_mut().register_custom_paint_source(source) + } + + pub fn unregister_custom_paint_source(&self, id: u64) { + self.inner.borrow_mut().unregister_custom_paint_source(id) + } +} + +impl WindowRenderer for DioxusNativeWindowRenderer { + type ScenePainter<'a> + = ::ScenePainter<'a> + where + Self: 'a; + + fn resume(&mut self, window: Arc, width: u32, height: u32) { + self.inner.borrow_mut().resume(window, width, height) + } + + fn suspend(&mut self) { + self.inner.borrow_mut().suspend() + } + + fn is_active(&self) -> bool { + self.inner.borrow().is_active() + } + + fn set_size(&mut self, width: u32, height: u32) { + self.inner.borrow_mut().set_size(width, height) + } + + fn render)>(&mut self, draw_fn: F) { + self.inner.borrow_mut().render(draw_fn) + } +} diff --git a/packages/native/src/lib.rs b/packages/native/src/lib.rs index 8fb4c1944..f7dc4817a 100644 --- a/packages/native/src/lib.rs +++ b/packages/native/src/lib.rs @@ -13,12 +13,16 @@ mod assets; mod contexts; mod dioxus_application; mod dioxus_document; +mod dioxus_renderer; mod events; mod mutation_writer; -use blitz_dom::{ns, Atom, QualName}; +pub use anyrender_vello::{CustomPaintCtx, CustomPaintSource, TextureHandle}; +use assets::DioxusNativeNetProvider; +use blitz_dom::{ns, LocalName, Namespace, QualName}; pub use dioxus_application::{DioxusNativeApplication, DioxusNativeEvent}; pub use dioxus_document::DioxusDocument; +pub use dioxus_renderer::{use_wgpu, DioxusNativeWindowRenderer, Features, Limits}; use blitz_shell::{create_default_event_loop, BlitzShellEvent, Config, WindowConfig}; use dioxus_core::{ComponentFunction, Element, VirtualDom}; @@ -44,12 +48,36 @@ pub fn launch_cfg_with_props( app: impl ComponentFunction, props: P, contexts: Vec Box + Send + Sync>>, - _cfg: Vec>, + configs: Vec>, ) { - let _cfg = _cfg - .into_iter() - .find_map(|cfg| cfg.downcast::().ok()) - .unwrap_or_default(); + // Macro to attempt to downcast a type out of a Box + macro_rules! try_read_config { + ($input:ident, $store:ident, $kind:ty) => { + // Try to downcast the Box to type $kind + match $input.downcast::<$kind>() { + // If the type matches then write downcast value to variable $store + Ok(value) => { + $store = Some(*value); + continue; + } + // Else extract the original Box value out of the error type + // and return it so that we can try again with a different type. + Err(cfg) => cfg, + } + }; + } + + // Read config values + let mut features = None; + let mut limits = None; + let mut _config = None; + for mut cfg in configs { + cfg = try_read_config!(cfg, features, Features); + cfg = try_read_config!(cfg, limits, Limits); + cfg = try_read_config!(cfg, _config, Config); + let _ = cfg; + } + let event_loop = create_default_event_loop::(); // Turn on the runtime and enter it @@ -84,8 +112,23 @@ pub fn launch_cfg_with_props( vdom.insert_any_root_context(context()); } + #[cfg(feature = "net")] + let net_provider = { + let proxy = event_loop.create_proxy(); + let net_provider = DioxusNativeNetProvider::shared(proxy); + Some(net_provider) + }; + + #[cfg(not(feature = "net"))] + let net_provider = None; + + // Create document + window from the baked virtualdom + let doc = DioxusDocument::new(vdom, net_provider); + let renderer = DioxusNativeWindowRenderer::with_features_and_limits(features, limits); + let config = WindowConfig::new(Box::new(doc) as _, renderer.clone()); + // Create application - let mut application = DioxusNativeApplication::new(event_loop.create_proxy(), vdom); + let mut application = DioxusNativeApplication::new(event_loop.create_proxy(), config); // Run event loop event_loop.run_app(&mut application).unwrap(); @@ -94,8 +137,8 @@ pub fn launch_cfg_with_props( pub(crate) fn qual_name(local_name: &str, namespace: Option<&str>) -> QualName { QualName { prefix: None, - ns: namespace.map(Atom::from).unwrap_or(ns!(html)), - local: Atom::from(local_name), + ns: namespace.map(Namespace::from).unwrap_or(ns!(html)), + local: LocalName::from(local_name), } }