diff --git a/Cargo.lock b/Cargo.lock index 9724b0b..d0f134c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,6 +507,30 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" +[[package]] +name = "bitcode" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6ed1b54d8dc333e7be604d00fa9262f4635485ffea923647b6521a5fff045d" +dependencies = [ + "arrayvec", + "bitcode_derive", + "bytemuck", + "glam", + "serde", +] + +[[package]] +name = "bitcode_derive" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238b90427dfad9da4a9abd60f3ec1cdee6b80454bde49ed37f1781dd8e9dc7f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -599,17 +623,6 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" -[[package]] -name = "bus" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b7118d0221d84fada881b657c2ddb7cd55108db79c8764c9ee212c0c259b783" -dependencies = [ - "crossbeam-channel", - "num_cpus", - "parking_lot_core", -] - [[package]] name = "bytemuck" version = "1.24.0" @@ -1422,24 +1435,24 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1456,9 +1469,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1467,15 +1480,15 @@ dependencies = [ [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", @@ -1483,7 +1496,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1497,7 +1509,6 @@ dependencies = [ "async-trait", "az", "bitflags 2.10.0", - "bus", "bytemuck", "cgmath", "crossbeam", @@ -1508,6 +1519,8 @@ dependencies = [ "egui-winit", "either", "env_logger", + "futures-util", + "fuzzpaint-connection", "fuzzpaint-core", "hashbrown", "human_bytes", @@ -1518,7 +1531,6 @@ dependencies = [ "oneshot", "parking_lot", "png", - "rayon", "rfd", "rustybuzz", "serde", @@ -1536,6 +1548,24 @@ dependencies = [ "winit", ] +[[package]] +name = "fuzzpaint-collections" +version = "0.1.0" +dependencies = [ + "fuzzpaint-types", +] + +[[package]] +name = "fuzzpaint-connection" +version = "0.1.0" +dependencies = [ + "bitcode", + "fuzzpaint-collections", + "fuzzpaint-types", + "log", + "tokio", +] + [[package]] name = "fuzzpaint-core" version = "0.1.0" @@ -1567,6 +1597,34 @@ dependencies = [ "uuid", ] +[[package]] +name = "fuzzpaint-io" +version = "0.1.0" +dependencies = [ + "fuzzpaint-collections", + "fuzzpaint-types", +] + +[[package]] +name = "fuzzpaint-render" +version = "0.1.0" +dependencies = [ + "fuzzpaint-types", +] + +[[package]] +name = "fuzzpaint-types" +version = "0.1.0" + +[[package]] +name = "fuzzpaint-ui" +version = "0.1.0" +dependencies = [ + "egui", + "fuzzpaint-collections", + "fuzzpaint-types", +] + [[package]] name = "gethostname" version = "1.1.0" @@ -1616,6 +1674,12 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "glam" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34627c5158214743a374170fed714833fdf4e4b0cbcc1ea98417866a4c5d4441" + [[package]] name = "half" version = "2.7.1" @@ -2128,6 +2192,17 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c505b3e17ed6b70a7ed2e67fbb2c560ee327353556120d6e72f5232b6880d536" +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "moxcms" version = "0.7.11" @@ -2260,16 +2335,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_enum" version = "0.7.5" @@ -2725,12 +2790,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "piper" version = "0.2.4" @@ -3497,6 +3556,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27207bb65232eda1f588cf46db2fee75c0808d557f6b3cf19a75f5d6d7c94df1" +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3762,13 +3831,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ + "bytes", + "libc", + "mio", "parking_lot", "pin-project-lite", + "signal-hook-registry", + "socket2", "tokio-macros", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1d599e3..da299ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [workspace] resolver = "2" members = [ - "fuzzpaint", + "fuzzpaint", "fuzzpaint-collections", "fuzzpaint-connection", "fuzzpaint-core" -] +, "fuzzpaint-io", "fuzzpaint-render", "fuzzpaint-types", "fuzzpaint-ui"] [profile.profiling] inherits = "release" diff --git a/fuzzpaint-collections/Cargo.toml b/fuzzpaint-collections/Cargo.toml new file mode 100644 index 0000000..60d04d8 --- /dev/null +++ b/fuzzpaint-collections/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "fuzzpaint-collections" +version = "0.1.0" +edition = "2024" + +[dependencies] +fuzzpaint-types = { path = "../fuzzpaint-types" } \ No newline at end of file diff --git a/fuzzpaint-collections/src/lib.rs b/fuzzpaint-collections/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/fuzzpaint-collections/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/fuzzpaint-connection/Cargo.toml b/fuzzpaint-connection/Cargo.toml new file mode 100644 index 0000000..c4c1ed5 --- /dev/null +++ b/fuzzpaint-connection/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "fuzzpaint-connection" +version = "0.1.0" +edition = "2024" + +[dependencies] +fuzzpaint-collections = { path = "../fuzzpaint-collections" } +fuzzpaint-types = { path = "../fuzzpaint-types" } +bitcode = "0.6.9" +log = "0.4.29" +# Tokio is smoller than smol (probably cuz more eyes on it to optimize) +# It has a worse (more-implicit, panick-y) API tho :V +# Thas okay because we aggressively keep it compartmentalized. +tokio = { version = "1.49.0", optional = true } + +[features] +default = ["client", "server", "tcp", "channel"] +client = [] +server = [] + +# TCP-based connection. +tcp = ["dep:tokio", "tokio/net", "tokio/io-util"] + +# A channel-based connection for a server and client within the same process. +channel = ["dep:tokio", "tokio/sync"] + +# An interprocess-channel-based connection, using unidirectional shared mem to +# share blobs in an insulated manner. +# ipc-shm = [] + +[dev-dependencies] +# Needs a runtime and `join` for some tests. +tokio = { version = "1.49.0", features = ["rt", "macros"] } diff --git a/fuzzpaint-connection/src/channel/client.rs b/fuzzpaint-connection/src/channel/client.rs new file mode 100644 index 0000000..7248f35 --- /dev/null +++ b/fuzzpaint-connection/src/channel/client.rs @@ -0,0 +1,22 @@ +pub struct Client { + pub(super) channel: super::BidiBitcodeChannel, +} +impl crate::client::Connection for Client { + type Error = super::ConnectionError; + /// Cancel-safe. + async fn send( + &mut self, + message: &crate::client_msg::Message<'_>, + ) -> Result<&mut Self, Self::Error> { + self.channel.send(message).await?; + Ok(self) + } + /// Cancel-safe. + async fn flush(&mut self) -> Result<&mut Self, Self::Error> { + Ok(self) + } + /// Cancel-safe. + async fn recv(&mut self) -> Result, Self::Error> { + self.channel.recv().await + } +} diff --git a/fuzzpaint-connection/src/channel/mod.rs b/fuzzpaint-connection/src/channel/mod.rs new file mode 100644 index 0000000..d93576d --- /dev/null +++ b/fuzzpaint-connection/src/channel/mod.rs @@ -0,0 +1,82 @@ +#[cfg(feature = "client")] +pub mod client; +#[cfg(feature = "server")] +pub mod server; +use tokio::sync::mpsc::{Receiver, Sender, error::SendError}; + +/// Fixme: use a byte-channel. Every message allocating is rough uwu BUT: This +/// is a sealed implementation detail, soooooooo doesn't matter rn. Heck, the +/// fact that the messages even get serialized a deserialized at all is an +/// implementation detail! +type Message = Box<[u8]>; + +struct BidiBitcodeChannel { + pub(super) send: Sender, + pub(super) recv: Receiver, + pub(super) recv_buffer: Box<[u8]>, + pub(super) buffer: bitcode::Buffer, +} +impl BidiBitcodeChannel { + async fn recv<'a, T: bitcode::Decode<'a>>(&'a mut self) -> Result { + self.recv_buffer = self.recv.recv().await.ok_or(ConnectionError::Closed)?; + self.buffer + .decode(&self.recv_buffer) + .map_err(ConnectionError::DecodeErr) + } + async fn send(&mut self, message: &T) -> Result<(), ConnectionError> { + // Tests if the channel has closed before spending the work on encoding + // the message. + let permit = self + .send + .reserve() + .await + .map_err(|SendError(())| ConnectionError::Closed)?; + let message = self.buffer.encode(message).into(); + permit.send(message); + Ok(()) + } + fn pair(capacity: usize) -> [Self; 2] { + let a_to_b = tokio::sync::mpsc::channel(capacity); + let b_to_a = tokio::sync::mpsc::channel(capacity); + [ + BidiBitcodeChannel { + send: a_to_b.0, + recv: a_to_b.1, + recv_buffer: Box::new([]), + buffer: bitcode::Buffer::new(), + }, + BidiBitcodeChannel { + send: b_to_a.0, + recv: b_to_a.1, + recv_buffer: Box::new([]), + buffer: bitcode::Buffer::new(), + }, + ] + } +} + +#[derive(Debug)] +pub enum ConnectionError { + Closed, + DecodeErr(bitcode::Error), +} +impl std::fmt::Display for ConnectionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConnectionError::Closed => write!(f, "channel closed"), + ConnectionError::DecodeErr(e) => write!(f, "failed to decode message, {e}"), + } + } +} +impl std::error::Error for ConnectionError {} + +#[cfg(all(feature = "server", feature = "client"))] +pub fn pair() -> (server::Server, client::Client) { + let [server, client] = BidiBitcodeChannel::pair(64); + ( + server::Server { + takable_client: Some(server::Client { channel: server }), + }, + client::Client { channel: client }, + ) +} diff --git a/fuzzpaint-connection/src/channel/server.rs b/fuzzpaint-connection/src/channel/server.rs new file mode 100644 index 0000000..44eaa18 --- /dev/null +++ b/fuzzpaint-connection/src/channel/server.rs @@ -0,0 +1,50 @@ +pub struct Server { + pub(super) takable_client: Option, +} +#[derive(Debug)] +pub enum WaitClientError { + Consumed, +} +impl std::fmt::Display for WaitClientError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Consumed => write!( + f, + "server in one-shot mode, no longer accepting connections" + ), + } + } +} +impl std::error::Error for WaitClientError {} +impl crate::server::Connection for Server { + type Error = WaitClientError; + type Client = Client; + fn allows_incoming(&self) -> bool { + self.takable_client.is_some() + } + async fn wait_client(&mut self) -> Result { + self.takable_client.take().ok_or(WaitClientError::Consumed) + } +} +pub struct Client { + pub(super) channel: super::BidiBitcodeChannel, +} +impl crate::server::ClientConnection for Client { + type Error = super::ConnectionError; + /// Cancel-safe. + async fn send( + &mut self, + message: &crate::server_msg::Message<'_>, + ) -> Result<&mut Self, Self::Error> { + self.channel.send(message).await?; + Ok(self) + } + /// Cancel-safe. + async fn flush(&mut self) -> Result<&mut Self, Self::Error> { + Ok(self) + } + /// Cancel-safe. + async fn recv(&mut self) -> Result, Self::Error> { + self.channel.recv().await + } +} diff --git a/fuzzpaint-connection/src/client.rs b/fuzzpaint-connection/src/client.rs new file mode 100644 index 0000000..c130dc4 --- /dev/null +++ b/fuzzpaint-connection/src/client.rs @@ -0,0 +1,16 @@ +pub trait Connection { + type Error: std::error::Error; + /// Send a message to the server. + async fn send( + &mut self, + message: &crate::client_msg::Message<'_>, + ) -> Result<&mut Self, Self::Error>; + /// Ensure all previous calls to [`Self::send`] have been executed. + async fn flush(&mut self) -> Result<&mut Self, Self::Error>; + /// Recieve a message to the server. + async fn recv(&mut self) -> Result, Self::Error>; +} +/// From the client side, represents a connection to a server. +struct Client { + conn: Conn, +} diff --git a/fuzzpaint-connection/src/lib.rs b/fuzzpaint-connection/src/lib.rs new file mode 100644 index 0000000..5c011bb --- /dev/null +++ b/fuzzpaint-connection/src/lib.rs @@ -0,0 +1,183 @@ +//! # `fuzzpaint-connection` +//! Implements various wire protocols for connecting servers to clients as well +//! as higher-level interfaces for controlling remote documents. **This crate +//! does not expose a stable wire protocol,** all protocols are considered +//! implementation details and may change at any time (for now~). + +#[cfg(feature = "channel")] +pub mod channel; +#[cfg(feature = "client")] +pub mod client; +#[cfg(feature = "server")] +pub mod server; +#[cfg(feature = "tcp")] +pub mod tcp; + +/// An ID, in the server's namespace. Refers to the same object across clients +/// and connections during the lifetime of the server. +pub type ID = (); +/// An ID, in a unique namespace for each direction of each client-server +/// connection. Reusable after discarded +pub type StreamID = (); +/// ID number of a client message. +pub type Serial = (); + +#[macro_use] +pub mod macro_use { + #[macro_export] + macro_rules! message_enum { + { + $(#[$meta:meta])* + pub enum $enum_name:ident$(<$($enum_lt:lifetime),+>)? { + $( + $name:ident$(<$($variant_lt:lifetime),+>)? + ),* + $(,)? + } + } => { + $(#[$meta])* + pub enum $enum_name$(<$($enum_lt,)*>)? { + $( + $name($name$(<$($variant_lt),*>)?) + ),* + } + }; +} +} + +pub mod client_msg { + use super::{ID, StreamID}; + + super::message_enum!( + #[cfg_attr(feature = "client", derive(bitcode::Encode))] + #[cfg_attr(feature = "server", derive(bitcode::Decode))] + pub enum Message<'a> { + ClientMessage<'a>, + //ClientHello<'a>, + } + ); + + /// Initial message sent from client to server to introduce itself + #[derive(bitcode::Decode)] + #[cfg_attr(feature = "client", derive(bitcode::Encode))] + pub struct ClientHello<'a> { + pub software: &'a str, + pub username: &'a str, + pub color: [u8; 3], + } + #[cfg_attr(feature = "client", derive(bitcode::Encode))] + #[cfg_attr(feature = "server", derive(bitcode::Decode))] + pub struct ClientMessage<'a> { + pub message: &'a str, + } + #[cfg_attr(feature = "client", derive(bitcode::Encode))] + #[cfg_attr(feature = "server", derive(bitcode::Decode))] + struct RequestDocument { + document: ID, + } + #[cfg_attr(feature = "client", derive(bitcode::Encode))] + #[cfg_attr(feature = "server", derive(bitcode::Decode))] + struct Motion { + position: Option<(f32, f32)>, + } + #[cfg_attr(feature = "client", derive(bitcode::Encode))] + #[cfg_attr(feature = "server", derive(bitcode::Decode))] + struct BeginStroke { + name: StreamID, + aspects: u16, + } + #[cfg_attr(feature = "client", derive(bitcode::Encode))] + #[cfg_attr(feature = "server", derive(bitcode::Decode))] + struct InlineBlob { + name: StreamID, + // FIXME: Why doesn't byte slice work here? &str works... + data: Vec, + finish: bool, + } +} +pub mod server_msg { + use super::{ID, Serial, StreamID}; + + #[cfg_attr(feature = "server", derive(bitcode::Encode))] + #[cfg_attr(feature = "client", derive(bitcode::Decode))] + pub struct Message<'a> { + pub last_processed: Serial, + pub message: MessageKind<'a>, + } + super::message_enum!( + #[cfg_attr(feature = "server", derive(bitcode::Encode))] + #[cfg_attr(feature = "client", derive(bitcode::Decode))] + pub enum MessageKind<'a> { + ServerMessage<'a>, + //Log<'a>, + //Error, + } + ); + + #[cfg_attr(feature = "server", derive(bitcode::Encode))] + #[cfg_attr(feature = "client", derive(bitcode::Decode))] + enum ErrorKind { + UnknownID(ID), + UnknownStreamID(StreamID), + PermissionDenied, + EnhanceYourChill, + } + #[cfg_attr(feature = "server", derive(bitcode::Encode))] + #[cfg_attr(feature = "client", derive(bitcode::Decode))] + struct Error { + message: Serial, + kind: ErrorKind, + } + + #[cfg_attr(feature = "server", derive(bitcode::Encode))] + #[cfg_attr(feature = "client", derive(bitcode::Decode))] + enum LogLevel { + Warn, + Error, + Info, + Debug, + Trace, + } + #[cfg_attr(feature = "server", derive(bitcode::Encode))] + #[cfg_attr(feature = "client", derive(bitcode::Decode))] + struct Log<'a> { + level: LogLevel, + text: &'a str, + } + /// Initial message sent from server to client to introduce itself + #[cfg_attr(feature = "server", derive(bitcode::Encode))] + #[cfg_attr(feature = "client", derive(bitcode::Decode))] + pub struct ServerHello<'a> { + pub software: &'a str, + pub motd: &'a str, + } + #[cfg_attr(feature = "server", derive(bitcode::Encode))] + #[cfg_attr(feature = "client", derive(bitcode::Decode))] + pub struct ServerMessage<'a> { + pub user_id: Option, + pub message: &'a str, + } + + #[cfg_attr(feature = "server", derive(bitcode::Encode))] + #[cfg_attr(feature = "client", derive(bitcode::Decode))] + pub struct AdvertiseOtherUser<'a> { + pub id: ID, + pub hello: super::client_msg::ClientHello<'a>, + } + #[cfg_attr(feature = "server", derive(bitcode::Encode))] + #[cfg_attr(feature = "client", derive(bitcode::Decode))] + pub struct OtherUserRemoved { + pub id: ID, + } + #[cfg_attr(feature = "server", derive(bitcode::Encode))] + #[cfg_attr(feature = "client", derive(bitcode::Decode))] + pub struct AdvertiseDocument<'a> { + pub id: ID, + pub name: Option<&'a str>, + } + #[cfg_attr(feature = "server", derive(bitcode::Encode))] + #[cfg_attr(feature = "client", derive(bitcode::Decode))] + pub struct DocumentRemoved { + pub id: ID, + } +} diff --git a/fuzzpaint-connection/src/server.rs b/fuzzpaint-connection/src/server.rs new file mode 100644 index 0000000..8d6b6c1 --- /dev/null +++ b/fuzzpaint-connection/src/server.rs @@ -0,0 +1,27 @@ +pub trait Connection { + type Error; + type Client: ClientConnection; + /// Returns whether the connection is still able to accept clients. E.g. for + /// single-user servers, this will immediately become false after the first + /// client connection. Returned value may be immediately out of date if + /// connections are occuring on other threads. + fn allows_incoming(&self) -> bool; + async fn wait_client(&mut self) -> Result; +} +pub trait ClientConnection { + type Error; + async fn send( + &mut self, + message: &crate::server_msg::Message<'_>, + ) -> Result<&mut Self, Self::Error>; + async fn flush(&mut self) -> Result<&mut Self, Self::Error>; + async fn recv(&mut self) -> Result, Self::Error>; +} +/// Represents a server with the ability to accept client(s). +pub struct Server { + conn: Conn, +} +/// From the server's side, represents a client of the server. +pub struct Client { + client: Conn::Client, +} diff --git a/fuzzpaint-connection/src/tcp/client.rs b/fuzzpaint-connection/src/tcp/client.rs new file mode 100644 index 0000000..5707b5d --- /dev/null +++ b/fuzzpaint-connection/src/tcp/client.rs @@ -0,0 +1,69 @@ +use std::io::{Error, ErrorKind, Result}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net, +}; +pub struct Client { + stream: net::TcpStream, + buffer: bitcode::Buffer, + // may contain residual bytes from half-recieved messages. + recv_staging: Vec, + // may contain residual bytes from half-sent messages. + send_staging: Vec, +} +impl Client { + /// This function is *not* cancel safe. Cancelling the future may result in + /// a spurious connection to the address. + pub async fn connect(addr: A) -> Result { + let mut stream = net::TcpStream::connect(addr).await?; + // "negotiate" a protocol "version" + let mut protocol = [0u8; super::PROTOCOL_VERSION.len()]; + stream.read_exact(&mut protocol).await?; + stream.write_all(super::PROTOCOL_VERSION).await?; + if protocol != super::PROTOCOL_VERSION { + return Err(Error::new( + ErrorKind::InvalidData, + "server connected with unsupported protocol", + )); + } + Ok(Self { + stream, + send_staging: Vec::new(), + recv_staging: Vec::new(), + buffer: bitcode::Buffer::new(), + }) + } + /// Push a message to be sent on the next call to [`Connection::send`] + pub fn defer_send(&mut self, message: &crate::client_msg::Message<'_>) -> Result<&mut Self> { + super::encode_append(&mut self.buffer, &mut self.send_staging, message)?; + Ok(self) + } +} +impl crate::client::Connection for Client { + type Error = Error; + /// Cancel-safe. + async fn send( + &mut self, + message: &crate::client_msg::Message<'_>, + ) -> std::result::Result<&mut Self, Self::Error> { + super::streaming_write( + &mut self.stream, + &mut self.buffer, + &mut self.send_staging, + message, + ) + .await?; + Ok(self) + } + /// Cancel-safe. + async fn flush(&mut self) -> std::result::Result<&mut Self, Self::Error> { + self.stream.write_all(&self.send_staging).await?; + self.send_staging.clear(); + self.stream.flush().await?; + Ok(self) + } + /// Cancel-safe. + async fn recv(&mut self) -> std::result::Result, Self::Error> { + super::streaming_read(&mut self.stream, &mut self.buffer, &mut self.recv_staging).await + } +} diff --git a/fuzzpaint-connection/src/tcp/mod.rs b/fuzzpaint-connection/src/tcp/mod.rs new file mode 100644 index 0000000..7df6d20 --- /dev/null +++ b/fuzzpaint-connection/src/tcp/mod.rs @@ -0,0 +1,208 @@ +#[cfg(feature = "client")] +pub mod client; +#[cfg(feature = "server")] +pub mod server; + +const PROTOCOL_VERSION: &[u8] = b"owo lmao hai this is a fuzzpaint connection"; + +async fn streaming_read<'a, T: bitcode::Decode<'a>>( + mut stream: impl tokio::io::AsyncReadExt + Unpin, + buffer: &mut bitcode::Buffer, + staging: &'a mut Vec, +) -> std::io::Result { + use std::io::{Error, ErrorKind}; + // Clean up previous recv, if was long enough to parse. + if let &[a, b, ..] = staging.as_slice() { + let reported_length = usize::from(u16::from_le_bytes([a, b])); + if staging.len() >= reported_length + 2 { + staging.drain(..reported_length + 2); + } + } + // Read more data in a loop until long enough to parse + let message_length = loop { + let grow_by = if let &[a, b, ..] = staging.as_slice() { + let reported_length = usize::from(u16::from_le_bytes([a, b])); + // There's already a whole message in here!! + if staging.len() >= reported_length + 2 { + break reported_length; + } + let grow_by = reported_length - staging.len() + 2; + + // Ensure we don't get caught in an infinite loop (grow_by = 0) + // or get overzealous with our allocation. reported_length is + // untrusted data, remember~ + grow_by.clamp(32, 256) + } else { + // Not enough data to know. + 128 + }; + staging.reserve(grow_by); + let spare = staging.spare_capacity_mut(); + // Infinite loop!!!! prevented above. + debug_assert!(!spare.is_empty()); + // Read accepts ref to bytes, so they must be valid (i.e. init) + // bytes. FIXME: this is redundant most of the time uwu, as it just + // repeatedly zeros already-init bytes. + spare.fill(std::mem::MaybeUninit::zeroed()); + let spare = unsafe { spare.assume_init_mut() }; + + let read = stream.read(spare).await; + let read = match read { + Ok(read) => read, + Err(e) => { + if e.kind() == ErrorKind::Interrupted { + continue; + } else { + return Err(e); + } + } + }; + if read == 0 { + // closed. We know that there's no residual complete messages, + // so error out. + return Err(Error::new(ErrorKind::NotConnected, "connection closed")); + } + let new_len = staging.len() + read; + // This is against the contract of Read, but, being a safe + // trait, we mustn't make unsafe assertions based on it's + // contracts. + if new_len > staging.capacity() { + return Err(Error::other("read reported invalid length")); + } + unsafe { + // Even if `read` didn't actually write these bytes (it may not + // have!) they've been safely zeroed above and so can be assumed + // valid. + staging.set_len(new_len); + } + let &[a, b, ..] = staging.as_slice() else { + continue; + }; + let message_length = usize::from(u16::from_le_bytes([a, b])); + // Ready to attempt decode? + if staging.len() >= message_length + 2 { + break message_length; + } + }; + let Some(data) = staging.get(2..2 + message_length) else { + // Loop above only breaks when this codition is met. Can't break + // with the data slice to make this infallible, for some inscrutable + // lifetime reason :o + return Err(Error::other("unreachable")); + }; + buffer + .decode(data) + .map_err(|e| Error::new(ErrorKind::InvalidData, e)) +} +fn encode_append( + buffer: &mut bitcode::Buffer, + staging: &mut Vec, + t: &T, +) -> std::io::Result<()> { + use std::io::{Error, ErrorKind}; + let bytes = buffer.encode(t); + // Bitcode doesn't know the length of it's own messages (slices passed + // to decode must contain exactly the correct number of bytes). So, we + // must prefix each message with a length on the wire-level. + staging.reserve(bytes.len() + 2); + // cant use usize here, since that differs in length by platform. + let len = u16::try_from(bytes.len()) + .map_err(|_| Error::new(ErrorKind::InvalidData, "message too long"))?; + staging.extend_from_slice(&len.to_le_bytes()); + staging.extend_from_slice(bytes); + + Ok(()) +} +async fn streaming_write( + mut stream: impl tokio::io::AsyncWriteExt + Unpin, + buffer: &mut bitcode::Buffer, + staging: &mut Vec, + t: &T, +) -> std::io::Result<()> { + use std::io::{Error, ErrorKind}; + encode_append(buffer, staging, t)?; + + // Send as much as we can, buffer the rest for later. + let sent = stream.write(staging).await?; + if sent == 0 { + // 0 = unlikely to ever accept bytes again + return Err(Error::new(ErrorKind::NotConnected, "connection closed")); + } + staging.drain(..sent.min(staging.len())); + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + client::Connection as _, + server::{ClientConnection as _, Connection as _}, + }; + use std::io::Result; + + #[cfg(all(feature = "client", feature = "server"))] + #[test] + fn wawa() -> Result<()> { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_io() + .build()?; + let server = rt.block_on(server::Builder::default().bind_local())?; + let addr = server.local_addr()?; + let (client, server) = rt.block_on(async { tokio::join!(client(addr), serve(server)) }); + return client.and(server); + + async fn serve(mut server: server::Server) -> Result<()> { + let mut client = server.wait_client().await?; + + client + .send(&crate::server_msg::Message { + last_processed: (), + message: crate::server_msg::MessageKind::ServerMessage( + crate::server_msg::ServerMessage { + user_id: None, + message: "hai from server :3", + }, + ), + }) + .await? + .flush() + .await?; + let message = client.recv().await?; + assert!(matches!( + message, + crate::client_msg::Message::ClientMessage(crate::client_msg::ClientMessage { + message: "hello in turn ;3", + },) + )); + Ok(()) + } + async fn client(addr: std::net::SocketAddr) -> Result<()> { + let mut client = client::Client::connect(addr).await?; + let message = client.recv().await?; + assert!(matches!( + message, + crate::server_msg::Message { + last_processed: (), + message: crate::server_msg::MessageKind::ServerMessage( + crate::server_msg::ServerMessage { + user_id: None, + message: "hai from server :3", + }, + ), + } + )); + client + .send(&crate::client_msg::Message::ClientMessage( + crate::client_msg::ClientMessage { + message: "hello in turn ;3", + }, + )) + .await? + .flush() + .await?; + + Ok(()) + } + } +} diff --git a/fuzzpaint-connection/src/tcp/server.rs b/fuzzpaint-connection/src/tcp/server.rs new file mode 100644 index 0000000..8e3a6f4 --- /dev/null +++ b/fuzzpaint-connection/src/tcp/server.rs @@ -0,0 +1,156 @@ +use std::{ + io::{Error, ErrorKind, Result}, + net::SocketAddr, +}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net, +}; + +const ONESHOT_ERROR: &str = "server in one-shot mode, no longer accepting connections"; + +#[derive(Default)] +pub struct Builder { + oneshot: bool, + allow_loopback_nodelay: bool, +} +impl Builder { + pub fn oneshot(self, oneshot: bool) -> Self { + Self { oneshot, ..self } + } + pub fn allow_loopback_nodelay(self, allow: bool) -> Self { + Self { + allow_loopback_nodelay: allow, + ..self + } + } + pub async fn bind_local(self) -> Result { + use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; + self.bind( + [ + SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 0, 0, 0)), + SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)), + ] + .as_slice(), + ) + .await + } + pub async fn bind(self, addr: A) -> Result { + // Async because of DNS? woe. + let listener = net::TcpListener::bind(addr).await?; + Ok(Server { + listener, + oneshot: self.oneshot, + allows_incoming: true.into(), + allow_loopback_nodelay: self.allow_loopback_nodelay, + }) + } +} + +pub struct Server { + listener: net::TcpListener, + /// Stop accepting connections after the first successful connection. + oneshot: bool, + allows_incoming: std::sync::atomic::AtomicBool, + allow_loopback_nodelay: bool, +} +impl Server { + pub fn local_addr(&self) -> Result { + self.listener.local_addr() + } +} + +impl crate::server::Connection for Server { + type Error = Error; + type Client = Client; + fn allows_incoming(&self) -> bool { + !self.oneshot + || self + .allows_incoming + .load(std::sync::atomic::Ordering::Relaxed) + } + /// This function is *not* cancel safe. Cancelling the future may result in + /// losing pending connections. + async fn wait_client(&mut self) -> Result { + if self.oneshot && !self.allows_incoming() { + return Err(Error::new(ErrorKind::NotConnected, ONESHOT_ERROR)); + } + let (mut stream, address) = self.listener.accept().await?; + if self.allow_loopback_nodelay && address.ip().is_loopback() { + let _ = stream.set_nodelay(true); + } + // "Negotiate" a protocol version. + // Tell the client, so that they may adjust to our maximum version. + stream.write_all(super::PROTOCOL_VERSION).await?; + // Read the client's preferred version. May respond be an older version. + let mut client_protocol_version = [0; super::PROTOCOL_VERSION.len()]; + stream.read_exact(&mut client_protocol_version).await?; + // Check if we can support this protocol version (trivial single-version + // logic for now). + if client_protocol_version != *super::PROTOCOL_VERSION { + return Err(Error::new( + ErrorKind::InvalidData, + "client connected with unsupported protocol", + )); + } + if self.oneshot { + // Successfully connected and one-shot mode, set the flag + // disallowing further clients. May have changed since first check. + let allow_connection = self + .allows_incoming + .swap(true, std::sync::atomic::Ordering::Relaxed); + if !allow_connection { + return Err(Error::new(ErrorKind::NotConnected, ONESHOT_ERROR)); + } + } + Ok(Client { + stream, + address, + buffer: bitcode::Buffer::new(), + recv_staging: Vec::new(), + send_staging: Vec::new(), + }) + } +} +pub struct Client { + stream: net::TcpStream, + address: SocketAddr, + buffer: bitcode::Buffer, + recv_staging: Vec, + send_staging: Vec, +} +impl Client { + /// Push a message to be sent on the next call to [`Connection::send`] + pub fn defer_send(&mut self, message: &crate::server_msg::Message<'_>) -> Result<&mut Self> { + super::encode_append(&mut self.buffer, &mut self.send_staging, message)?; + Ok(self) + } +} +impl crate::server::ClientConnection for Client { + type Error = Error; + /// Cancel-safe. + async fn send( + &mut self, + message: &crate::server_msg::Message<'_>, + ) -> std::result::Result<&mut Self, Self::Error> { + super::streaming_write( + &mut self.stream, + &mut self.buffer, + &mut self.send_staging, + message, + ) + .await?; + Ok(self) + } + /// Cancel-safe. + async fn flush(&mut self) -> std::result::Result<&mut Self, Self::Error> { + self.stream.write_all(&self.send_staging).await?; + self.send_staging.clear(); + self.stream.flush().await?; + Ok(self) + } + /// Cancel-safe. + async fn recv(&mut self) -> std::result::Result, Self::Error> { + super::streaming_read(&mut self.stream, &mut self.buffer, &mut self.recv_staging).await + } +} diff --git a/fuzzpaint-core/src/blend.rs b/fuzzpaint-core/src/blend.rs index fe806a4..177b030 100644 --- a/fuzzpaint-core/src/blend.rs +++ b/fuzzpaint-core/src/blend.rs @@ -1,18 +1,11 @@ -#[derive( - strum::AsRefStr, - PartialEq, - Eq, - strum::EnumIter, - Copy, - Clone, - Hash, - Debug, - /*serde::Serialize, - serde::Deserialize,*/ -)] +#[derive(strum::AsRefStr, PartialEq, Eq, strum::EnumIter, Copy, Clone, Hash, Debug, Default)] +// Krita, which has the most blend modes of any software i've ever seen, only +// has ~100 blend modes. This smol repr is more than enough :3 #[repr(u8)] pub enum BlendMode { - Normal, + // 0 is reserved for "passthrough" or "none" on nodes that support it. + #[default] + Normal = 1, Add, Multiply, Screen, @@ -20,14 +13,9 @@ pub enum BlendMode { Lighten, Erase, } -impl Default for BlendMode { - fn default() -> Self { - Self::Normal - } -} /// Blend mode for an object, including a mode, opacity modulate, and alpha clip -#[derive(Copy, Clone, Debug, PartialEq /*serde::Serialize, serde::Deserialize*/)] +#[derive(Copy, Clone, Debug, PartialEq)] pub struct Blend { pub mode: BlendMode, pub opacity: f32, diff --git a/fuzzpaint-io/Cargo.toml b/fuzzpaint-io/Cargo.toml new file mode 100644 index 0000000..ac1e8d9 --- /dev/null +++ b/fuzzpaint-io/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "fuzzpaint-io" +version = "0.1.0" +edition = "2024" + +[dependencies] +fuzzpaint-collections = { path = "../fuzzpaint-collections" } +fuzzpaint-types = { path = "../fuzzpaint-types" } diff --git a/fuzzpaint-io/src/lib.rs b/fuzzpaint-io/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/fuzzpaint-io/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/fuzzpaint-render/Cargo.toml b/fuzzpaint-render/Cargo.toml new file mode 100644 index 0000000..8ad747a --- /dev/null +++ b/fuzzpaint-render/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "fuzzpaint-render" +version = "0.1.0" +edition = "2024" + +[dependencies] +fuzzpaint-types = { path = "../fuzzpaint-types" } + +[features] +ui = [] diff --git a/fuzzpaint-render/src/lib.rs b/fuzzpaint-render/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/fuzzpaint-render/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/fuzzpaint-types/Cargo.toml b/fuzzpaint-types/Cargo.toml new file mode 100644 index 0000000..706f4f6 --- /dev/null +++ b/fuzzpaint-types/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "fuzzpaint-types" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/fuzzpaint-types/src/lib.rs b/fuzzpaint-types/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/fuzzpaint-types/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/fuzzpaint-ui/Cargo.toml b/fuzzpaint-ui/Cargo.toml new file mode 100644 index 0000000..d3d3dd2 --- /dev/null +++ b/fuzzpaint-ui/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "fuzzpaint-ui" +version = "0.1.0" +edition = "2024" + +[dependencies] +egui = "0.33.3" +fuzzpaint-collections = { path = "../fuzzpaint-collections" } +fuzzpaint-types = { path = "../fuzzpaint-types" } diff --git a/fuzzpaint-ui/src/lib.rs b/fuzzpaint-ui/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/fuzzpaint-ui/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/fuzzpaint/Cargo.toml b/fuzzpaint/Cargo.toml index b4c3ddc..d853f3e 100644 --- a/fuzzpaint/Cargo.toml +++ b/fuzzpaint/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "fuzzpaint" version = "0.1.0" -edition = "2021" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] fuzzpaint-core = { path = "../fuzzpaint-core" } +fuzzpaint-connection = { path = "../fuzzpaint-connection" } ahash = { version = "0.8.12", default-features = false, features = ["std"] } anyhow = "1.0.100" async-trait = "0.1.89" @@ -30,7 +31,6 @@ log = "0.4.29" lyon_tessellation = "1.0.16" octotablet = "0.1.0" parking_lot = "0.12.5" -rayon = "1.11.0" rfd = "0.16.0" rustybuzz = "0.20.1" serde = { version = "1.0.228", features = ["derive", "rc"] } @@ -42,6 +42,7 @@ tokio = { version = "1.48.0", features = [ "rt", "macros", "parking_lot", + "process" ] } toml = "0.9.10" try-block = "0.1.0" @@ -51,12 +52,12 @@ vulkano-shaders = { version = "0.34.0", git = "https://github.com/fuzzyzilla/vul # Can't inherit from egui_winit since it doesn't enable serde feature. # Also need legacy RWH version for `vulkano` + new version for `octotablet` winit = { version = "0.30.12", features = ["serde", "rwh_05", "rwh_06"] } -bus = "2.4.1" strum = { version = "0.27.2", features = ["derive"] } png = "0.18.0" # For low-level vulkan crimes, use the same version as vulkano. ash = "^0.37.3" oneshot = "0.1.11" +futures-util = "0.3.32" [target.'cfg(not(target_env = "msvc"))'.dependencies] tikv-jemallocator = { version = "0.6.1", optional = true, default-features = false } diff --git a/fuzzpaint/src/connections.rs b/fuzzpaint/src/connections.rs new file mode 100644 index 0000000..41c0e04 --- /dev/null +++ b/fuzzpaint/src/connections.rs @@ -0,0 +1,205 @@ +pub trait Waker: Sync { + /// This should not block. + fn wake(&self, which: ConnectionID); +} + +mod inner { + use super::Waker; + use fuzzpaint_connection::{client::Connection, tcp::client}; + + pub struct Client { + conn: client::Client, + needs_flush: bool, + pub messages: Vec, + } + impl Client { + pub fn defer_send( + &mut self, + message: &fuzzpaint_connection::client_msg::Message<'_>, + ) -> std::io::Result<()> { + self.conn.defer_send(message)?; + self.needs_flush = true; + Ok(()) + } + } + pub enum Request { + /// Doesn't do anything, but wakes the thread. + Poke, + Exit, + ConnectTcp(std::net::SocketAddr), + } + + pub struct Inner { + pub clients: tokio::sync::Mutex>, + // Acts semaphore with only one permit, but between async and sync contexts. + pub exclusion: tokio::sync::Mutex<()>, + pub on_recv: Box, + } + impl Inner { + pub fn daemon( + &self, + mut requests: tokio::sync::mpsc::UnboundedReceiver, + ) -> anyhow::Result<()> { + let rt = { + let mut builder = tokio::runtime::Builder::new_current_thread(); + // Implant a killchip into it's brain that sets off a small embedded + // explosive should it think naughty thoughts: + #[cfg(debug_assertions)] + builder.thread_name_fn(|| { + panic!("connection-poller tokio instance should not spawn extra threads") + }); + builder.enable_io().build()? + }; + let block = async { + loop { + // Allows the controller thread to block the daemon at will. + drop(self.exclusion.lock().await); + let mut lock = self.clients.lock().await; + while let Ok(request) = requests.try_recv() { + match request { + Request::Poke => (), + Request::Exit => return Ok(()), + Request::ConnectTcp(addr) => { + match client::Client::connect(addr).await { + Ok(client) => lock.push(Client { + conn: client, + messages: Vec::new(), + needs_flush: false, + }), + Err(e) => log::error!("failed to connect to {addr}: {e}"), + } + } + } + } + futures_util::future::join_all(lock.iter_mut().map(|client| async { + if client.needs_flush { + let res = client.conn.flush().await; + client.needs_flush = false; + Some(res) + } else { + None + } + })) + .await + .into_iter() + .for_each(|res| { + if let Some(res) = res { + res.expect("todo"); + } + }); + let fs = + lock.iter_mut() + .map(|client| async { + use fuzzpaint_connection::server_msg; + if let server_msg::Message { + last_processed: (), + message: + server_msg::MessageKind::ServerMessage( + server_msg::ServerMessage { + user_id: _, + message, + }, + ), + } = client.conn.recv().await.expect("todo") + { + client.messages.push(message.to_owned()); + } + }) + .collect::>(); + let race = crate::my_futures::race(fs); + let request = requests.recv(); + tokio::select! { + biased; + Some(()) = race => { + self.on_recv.wake(crate::connections::ConnectionID(0)); + }, + _ = request => (), + }; + } + }; + rt.block_on(block) + } + } +} +use inner::{Inner, Request}; + +pub struct ClientConnectionsManager { + inner: std::sync::Arc, + daemon: std::thread::JoinHandle>, + requests: tokio::sync::mpsc::UnboundedSender, +} +impl ClientConnectionsManager { + /// Spawns the connection manager daemon, returning a handle to it. + pub fn spawn(on_recv: Box) -> anyhow::Result { + let (send, recv) = tokio::sync::mpsc::unbounded_channel(); + let inner = std::sync::Arc::new(Inner { + clients: Vec::new().into(), + exclusion: ().into(), + on_recv, + }); + let daemon = { + let inner = inner.clone(); + std::thread::Builder::new() + .name("connection-poller".to_owned()) + .spawn(move || inner.daemon(recv))? + }; + + Ok(Self { + inner, + daemon, + requests: send, + }) + } + pub fn connect_tcp(&self, addr: std::net::SocketAddr) { + self.requests.send(Request::ConnectTcp(addr)); + } + /// Kills the daemon, returning its result. + /// # Panics + /// If the daemon panicked. + pub fn kill(self) -> anyhow::Result<()> { + let _ = self.requests.send(Request::Exit); + self.daemon + .join() + .unwrap_or_else(|e| std::panic::panic_any(e)) + } + /// Get mutable access to all connections, may block. + pub fn lock(&self) -> ConnectionsLock<'_> { + // Prevent the daemon from continuing and immediately re-locking the + // mutex we're trying to fetch. + let _exclusion = self.inner.exclusion.blocking_lock(); + let _ = self.requests.send(Request::Poke); + ConnectionsLock { + clients: self.inner.clients.blocking_lock(), + } + } +} +pub struct ConnectionsLock<'a> { + clients: tokio::sync::MutexGuard<'a, Vec>, +} +#[derive(Debug)] +pub struct ConnectionID(usize); +pub struct ConnectionLock<'a> { + client: &'a mut inner::Client, +} +impl ConnectionsLock<'_> { + pub fn iter_connections( + &'_ mut self, + ) -> impl Iterator)> { + self.clients + .iter_mut() + .enumerate() + .map(|(idx, client)| (ConnectionID(idx), ConnectionLock { client })) + } +} +impl ConnectionLock<'_> { + pub fn message<'m>(&'_ mut self, message: &'m str) -> std::io::Result<&'_ mut Self> { + self.client + .defer_send(&fuzzpaint_connection::client_msg::Message::ClientMessage( + fuzzpaint_connection::client_msg::ClientMessage { message }, + ))?; + Ok(self) + } + pub fn messages(&self) -> impl Iterator { + self.client.messages.iter().map(std::ops::Deref::deref) + } +} diff --git a/fuzzpaint/src/document_viewport_proxy.rs b/fuzzpaint/src/document_viewport_proxy.rs index 8d4df1b..b00438e 100644 --- a/fuzzpaint/src/document_viewport_proxy.rs +++ b/fuzzpaint/src/document_viewport_proxy.rs @@ -1,7 +1,7 @@ use crate::vulkano_prelude::*; use std::sync::Arc; -use crate::{gizmos::GizmoTree, pen_tools, render_device, view_transform, AnyResult}; +use crate::{AnyResult, gizmos::GizmoTree, pen_tools, render_device, view_transform}; /// Proxy called into by the window renderer to perform the necessary synchronization and such to render the screen /// behind the Egui content. @@ -178,6 +178,7 @@ impl SurfaceData { ) -> Self { let framebuffers: AnyResult> = render_surface .swapchain_images() + .unwrap() .iter() .map(|image| -> AnyResult<_> { // Todo: duplication of view resources. @@ -197,7 +198,7 @@ impl SurfaceData { let framebuffers = framebuffers.unwrap().into_boxed_slice(); let mut prerecorded_command_buffers = - Vec::with_capacity(render_surface.swapchain_images().len()); + Vec::with_capacity(render_surface.swapchain_images().unwrap().len()); prerecorded_command_buffers.resize_with(prerecorded_command_buffers.capacity(), || { [std::sync::OnceLock::new(), std::sync::OnceLock::new()] }); diff --git a/fuzzpaint/src/egui_impl.rs b/fuzzpaint/src/egui_impl.rs index 1599645..b48407e 100644 --- a/fuzzpaint/src/egui_impl.rs +++ b/fuzzpaint/src/egui_impl.rs @@ -4,6 +4,13 @@ use std::sync::Arc; use egui_winit::{egui, winit}; +pub struct Callback { + pub kind: CallbackKind, +} +pub enum CallbackKind { + DocumentView { dummy_color: [u8; 4] }, +} + /// Merge the textures data from one egui output into another. Useful for discarding Egui geomety /// while maintaining its side-effects. pub fn prepend_textures_delta(into: &mut egui::TexturesDelta, mut from: egui::TexturesDelta) { @@ -415,6 +422,7 @@ impl Render { ) -> anyhow::Result<()> { let framebuffers: anyhow::Result> = surface .swapchain_images() + .unwrap() .iter() .map(|image| -> anyhow::Result<_> { let fb = vk::Framebuffer::new( @@ -449,9 +457,16 @@ impl Render { vert_buff_size += mesh.vertices.len(); index_buff_size += mesh.indices.len(); } - egui::epaint::Primitive::Callback(..) => { - //Todo. But I'm not sure I mind this feature being unimplemented :P - unimplemented!("Primitive Callback is not supported."); + egui::epaint::Primitive::Callback(callback) => { + let Some(callback) = callback.callback.downcast_ref::() else { + log::error!( + "unknown callback type {}", + std::any::type_name_of_val(callback.callback.as_ref()) + ); + continue; + }; + let CallbackKind::DocumentView { dummy_color } = callback.kind; + log::debug!("{dummy_color:?}"); } } } @@ -525,9 +540,9 @@ impl Render { if clear { command_buffer_builder.clear_color_image(vk::ClearColorImageInfo { clear_value: [0.0, 0.0, 0.0, 1.0].into(), - regions: smallvec::smallvec![framebuffer.attachments()[0] - .subresource_range() - .clone()], + regions: smallvec::smallvec![ + framebuffer.attachments()[0].subresource_range().clone() + ], ..vk::ClearColorImageInfo::image(framebuffer.attachments()[0].image().clone()) })?; } diff --git a/fuzzpaint/src/global/provider.rs b/fuzzpaint/src/global/provider.rs index 8bb992d..c92b278 100644 --- a/fuzzpaint/src/global/provider.rs +++ b/fuzzpaint/src/global/provider.rs @@ -12,7 +12,7 @@ struct PerDocument { } /// A provider that keeps documents in-memory. pub struct Local { - on_change: parking_lot::Mutex>, + on_change: tokio::sync::broadcast::Sender, // We don't expect high contention - will only be locked for writing when a new queue is inserted. documents: parking_lot::RwLock>, } @@ -28,9 +28,7 @@ impl Local { }; self.documents.write().insert(new_id, new_document); - self.on_change - .lock() - .broadcast(ChangeMessage::Opened(new_id)); + let _ = self.on_change.send(ChangeMessage::Opened(new_id)); new_id } @@ -47,7 +45,7 @@ impl Local { } } - self.on_change.lock().broadcast(ChangeMessage::Opened(id)); + let _ = self.on_change.send(ChangeMessage::Opened(id)); Ok(()) } @@ -67,7 +65,7 @@ impl Local { drop(read); if let Ok(true) = cursor.forward() { - self.on_change.lock().broadcast(ChangeMessage::Modified(id)); + let _ = self.on_change.send(ChangeMessage::Modified(id)); } Some(result) @@ -81,21 +79,21 @@ impl Local { /// Ensures the ID is valid before sending. pub fn touch(&self, id: ID) { if self.documents.read().contains_key(&id) { - self.on_change.lock().broadcast(ChangeMessage::Modified(id)); + let _ = self.on_change.send(ChangeMessage::Modified(id)); } } /// Get a reciever of messages describing changes to the provider or it's documents. /// Does not recieve old messages, use [`Self::document_iter`] to get up-to-date! - pub fn change_listener(&self) -> bus::BusReader { - self.on_change.lock().add_rx() + pub fn change_listener(&self) -> tokio::sync::broadcast::Receiver { + self.on_change.subscribe() } } impl Default for Local { fn default() -> Self { // Blocks on full, so choose a large number to avoid blocking user thread. - let on_change = bus::Bus::new(256); + let on_change = tokio::sync::broadcast::Sender::new(64); Self { - on_change: on_change.into(), + on_change, documents: parking_lot::RwLock::default(), } } diff --git a/fuzzpaint/src/main.rs b/fuzzpaint/src/main.rs index 472ee74..43817e8 100644 --- a/fuzzpaint/src/main.rs +++ b/fuzzpaint/src/main.rs @@ -8,6 +8,7 @@ #![allow(clippy::too_many_lines)] use std::sync::Arc; +pub mod connections; mod egui_impl; pub mod renderer; pub mod vulkano_prelude; @@ -17,6 +18,7 @@ pub mod actions; pub mod document_viewport_proxy; pub mod gizmos; pub mod global; +pub mod my_futures; pub mod pen_tools; pub mod picker; pub mod render_device; @@ -115,81 +117,37 @@ async fn stylus_event_collector( } } -//If we return, it was due to an error. -//convert::Infallible is a quite ironic name for this useage, isn't it? :P -fn main() -> AnyResult<()> { - let has_term = std::io::IsTerminal::is_terminal(&std::io::stdin()); - // Log to a terminal, if available. Else, log to "log.out" in the working directory. - if has_term { - env_logger::Builder::new() - .filter_level(log::LevelFilter::Debug) - .parse_default_env() - .init(); - } else { - let _ = simple_logging::log_to_file("log.out", log::LevelFilter::Debug); - } - #[cfg(feature = "dhat_heap")] - let _profiler = { - log::trace!("Installed dhat"); - dhat::Profiler::new_heap() - }; - - let loading_succeeded = { - use rayon::iter::{IntoParallelIterator, ParallelIterator}; - // Args are a simple list of paths to open at startup. - // Paths are OSStrings, let the system handle character encoding restrictions. - // Todo: Expand glob patterns on windows (on unix this is handled by shell) - let paths: Vec = std::env::args_os().skip(1).map(Into::into).collect(); - // Did we have at least one success? No paths is a success. - let had_success: std::sync::atomic::AtomicBool = paths.is_empty().into(); - let repo = crate::global::points(); - paths.into_par_iter().for_each(|path| { - let try_block = - || -> Result { - fuzzpaint_core::io::read_path(&path, repo) - }; - - match try_block() { - Err(e) => { - log::error!("failed to open file {path:?}: {e:#}"); - } - Ok(queue) => { - // We don't care when it's stored, so long as it gets there eventually. - had_success.store(true, std::sync::atomic::Ordering::Relaxed); - // Defaulted ID, can't fail - let _ = global::provider().insert(queue); - } - } - }); +struct InitialConnection { + // lazy: bool, + ty: InitialConnectionType, +} +enum InitialConnectionType { + Tcp(std::net::SocketAddr), + // IPC(), + // Inprocess, +} - had_success.into_inner() - }; - // False if every file failed. - // This should abort the startup if ran from commandline, or give a visual warning and continue - // if using a GUI. - if !loading_succeeded { - log::warn!("Failed to load any provided document."); +fn client(connection: InitialConnection) -> AnyResult<()> { + let mut application = window::Application::new()?; + match connection.ty { + InitialConnectionType::Tcp(addr) => { + application.connections().connect_tcp(addr); + } } - let mut application = window::Application::new(); let recievers = application.take_renderer_reciever().unwrap(); + let render_context = application.render_context().clone(); std::thread::Builder::new() .name("Stylus+Render worker".to_owned()) .spawn(move || { - #[cfg(feature = "dhat_heap")] - // Keep alive. Winit takes ownership of main, and may never drop - // this unless we steal it. - let _profiler = _profiler; - let Ok(receivers) = recievers.recv() else { log::error!("Didn't recieve a renderer. Exiting."); return; }; let result: Result<((), ()), anyhow::Error> = 'block: { - let tools = match pen_tools::ToolState::new_from_renderer(&receivers.render_context) - { + let tools = match pen_tools::ToolState::new_from_renderer(&render_context) { Ok(tools) => tools, Err(e) => break 'block Err(e), }; @@ -205,7 +163,7 @@ fn main() -> AnyResult<()> { runtime.block_on(async { tokio::try_join!( renderer::render_worker( - receivers.render_context, + render_context, recv, receivers.document_view.clone(), ), @@ -228,3 +186,139 @@ fn main() -> AnyResult<()> { application.run() } +fn server() -> AnyResult<()> { + use fuzzpaint_connection::{ + server::{ClientConnection, Connection}, + tcp::server::Client, + }; + let executor = tokio::runtime::Builder::new_current_thread() + .enable_io() + .build()?; + // Tokio globals *weeps* + let _guard = executor.enter(); + let server = fuzzpaint_connection::tcp::server::Builder::default() + .allow_loopback_nodelay(true) + .oneshot(true) + .bind_local(); + let mut server = executor.block_on(server)?; + let addr = server.local_addr()?; + + // Crappy cross-platform `fork()` + let mut child = tokio::process::Command::new(std::env::current_exe()?) + .arg("--client") + .arg("--tcp") + .arg(format!("{addr}")) + .spawn()?; + + let (new_connections, mut recv_new_connections) = tokio::sync::mpsc::channel(1); + + let new_client_loop = async { + loop { + match server.wait_client().await { + Ok(client) => { + if new_connections.send(client).await.is_err() { + break Ok(()); + } + } + Err(e) => break Err(e), + } + } + }; + let client_poll = async { + use fuzzpaint_connection::{client_msg, server_msg}; + let mut clients = Vec::::new(); + let mut messages = Vec::new(); + loop { + let await_new_client = recv_new_connections.recv(); + let mut new_client = None; + let recv_any = clients + .iter_mut() + .map(|client| async { client.recv().await.expect("todo") }) + .collect::>(); + let recv_any = my_futures::race(recv_any); + tokio::select! { + biased; + Some(message) = recv_any => { + match message { + client_msg::Message::ClientMessage(client_msg::ClientMessage{message}) => messages.push(message.to_owned()), + } + }, + // If returns None, this branch is decarded and does not + // participate in the selection. + Some(client) = await_new_client => { + new_client = Some(client); + } + } + if let Some(new_client) = new_client { + clients.push(new_client); + } + if !messages.is_empty() { + for message in messages.drain(..) { + let message = server_msg::Message { + last_processed: (), + message: server_msg::MessageKind::ServerMessage( + server_msg::ServerMessage { + user_id: Some(()), + message: &message, + }, + ), + }; + for client in &mut clients { + client.defer_send(&message).unwrap(); + } + } + futures_util::future::join_all( + clients + .iter_mut() + .map(|client| async { client.flush().await.expect("todo") }), + ) + .await; + } + } + }; + let res = executor.block_on(async { + tokio::join! {biased; new_client_loop, client_poll} + }); + res.0.map_err(Into::into) +} + +fn main() -> AnyResult<()> { + let has_term = std::io::IsTerminal::is_terminal(&std::io::stdout()); + let default_log_level = if cfg!(debug_assertions) { + log::LevelFilter::Debug + } else { + log::LevelFilter::Info + }; + // Log to a terminal, if available. Else, log to "log.out" in the working directory. + if has_term { + env_logger::Builder::new() + .filter_level(default_log_level) + .parse_default_env() + .init(); + } else { + let _ = simple_logging::log_to_file("log.out", default_log_level); + } + #[cfg(feature = "dhat_heap")] + let _profiler = { + log::trace!("Installed dhat"); + dhat::Profiler::new_heap(); + // Concurrent process filenames. + todo!() + }; + + let mut args = std::env::args().fuse(); + let _exec = args.next(); + // :3 grog not care (these are not public facing) + if args.next().as_deref() == Some("--client") + && args.next().as_deref() == Some("--tcp") + && let Some(addr) = args.next() + { + client(InitialConnection { + ty: InitialConnectionType::Tcp(addr.parse()?), + }) + } else if std::env::args().count() == 1 { + server() + } else { + Err(anyhow::anyhow!("invalid arguments")) + } +} diff --git a/fuzzpaint/src/my_futures.rs b/fuzzpaint/src/my_futures.rs new file mode 100644 index 0000000..0217fd0 --- /dev/null +++ b/fuzzpaint/src/my_futures.rs @@ -0,0 +1,51 @@ +use std::{future::Future, pin::Pin, task::Poll}; +mod pinvec { + use std::pin::Pin; + // Unpin regardless of T's Unpin-ness. (overrides auto-implementation where F: Unpin) + impl Unpin for PinVec {} + pub struct PinVec { + // MUST be private to uphold the Pinning contract + vec: Vec, + } + impl From> for PinVec { + fn from(value: Vec) -> Self { + Self { vec: value } + } + } + impl PinVec { + pub fn is_empty(&self) -> bool { + self.vec.is_empty() + } + pub fn iter_pinned_mut(&mut self) -> impl Iterator> { + self.vec + .iter_mut() + .map(|f| unsafe { Pin::new_unchecked(f) }) + } + } +} +struct Race { + fs: pinvec::PinVec, +} +impl Future for Race { + type Output = Option; + fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + if self.fs.is_empty() { + return Poll::Ready(None); + } + for f in self.fs.iter_pinned_mut() { + match f.poll(cx) { + Poll::Pending => (), + Poll::Ready(r) => return Poll::Ready(Some(r)), + } + } + Poll::Pending + } +} +/// Races a list of futures, yielding the results of the first future to +/// complete, cancelling the rest. Yields `None` if the list is empty. The +/// returned future is not Fused. +pub fn race( + fs: impl Into>, +) -> impl Future> { + Race { fs: fs.into() } +} diff --git a/fuzzpaint/src/pen_tools/brush.rs b/fuzzpaint/src/pen_tools/brush.rs index 90f0cf7..107d5b3 100644 --- a/fuzzpaint/src/pen_tools/brush.rs +++ b/fuzzpaint/src/pen_tools/brush.rs @@ -155,8 +155,7 @@ impl StrokeBuilder { self.current_archetype = Archetype::POSITION; } pub fn transform(&mut self, mat: &ultraviolet::Mat3) { - use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; - self.position.par_iter_mut().for_each(|[x, y]| { + self.position.iter_mut().for_each(|[x, y]| { let xformed = *mat * ultraviolet::Vec3 { x: *x, @@ -500,7 +499,7 @@ fn make_trail( size_factor: f32, color: Option, ) -> crate::gizmos::Gizmo { - use crate::gizmos::{transform::Transform, Gizmo, MeshMode, TextureMode, Visual}; + use crate::gizmos::{Gizmo, MeshMode, TextureMode, Visual, transform::Transform}; // Make trail: let mut points = Vec::with_capacity(stroke.len()); diff --git a/fuzzpaint/src/render_device.rs b/fuzzpaint/src/render_device.rs index 648167c..78a1c5b 100644 --- a/fuzzpaint/src/render_device.rs +++ b/fuzzpaint/src/render_device.rs @@ -1,6 +1,7 @@ use crate::vulkano_prelude::*; use anyhow::Result as AnyResult; use std::sync::Arc; +use winit::raw_window_handle_05::HasRawDisplayHandle; /// Check that every index is less than the number of vertices in debug builds, nop in release mode. /// # Panics @@ -15,6 +16,83 @@ pub fn debug_assert_indices_safe(vertices: &[Vertex], indices: &[u16]) { } } +unsafe fn physical_device_display_support( + instance: &vk::Instance, + phys: &vk::PhysicalDevice, + queue_family_idx: u32, + dpy: &winit::raw_window_handle_05::RawDisplayHandle, +) -> bool { + use vulkano::VulkanObject; + assert_eq!(phys.instance().handle(), instance.handle()); + let phys = phys.handle(); + match dpy { + winit::raw_window_handle_05::RawDisplayHandle::UiKit(_) => false, + winit::raw_window_handle_05::RawDisplayHandle::AppKit(_) => false, + winit::raw_window_handle_05::RawDisplayHandle::Orbital(_) => false, + winit::raw_window_handle_05::RawDisplayHandle::Xlib(xlib_display_handle) + if instance.enabled_extensions().khr_xlib_surface + && !xlib_display_handle.display.is_null() => + unsafe { + (instance + .fns() + .khr_xlib_surface + .get_physical_device_xlib_presentation_support_khr)( + phys, + queue_family_idx, + xlib_display_handle.display.cast(), + xlib_display_handle.screen.cast_unsigned(), + ) != 0 + }, + winit::raw_window_handle_05::RawDisplayHandle::Xcb(xcb_display_handle) + if instance.enabled_extensions().khr_xcb_surface + && !xcb_display_handle.connection.is_null() => + unsafe { + (instance + .fns() + .khr_xcb_surface + .get_physical_device_xcb_presentation_support_khr)( + phys, + queue_family_idx, + xcb_display_handle.connection, + xcb_display_handle.screen.cast_unsigned(), + ) != 0 + }, + winit::raw_window_handle_05::RawDisplayHandle::Wayland(wayland_display_handle) + if instance.enabled_extensions().khr_wayland_surface + && !wayland_display_handle.display.is_null() => + unsafe { + (instance + .fns() + .khr_wayland_surface + .get_physical_device_wayland_presentation_support_khr)( + phys, + queue_family_idx, + wayland_display_handle.display, + ) != 0 + }, + winit::raw_window_handle_05::RawDisplayHandle::Drm(_) => false, + winit::raw_window_handle_05::RawDisplayHandle::Gbm(_) => false, + winit::raw_window_handle_05::RawDisplayHandle::Windows(_windows_display_handle) + if instance.enabled_extensions().khr_win32_surface => + unsafe { + (instance + .fns() + .khr_win32_surface + .get_physical_device_win32_presentation_support_khr)( + phys, queue_family_idx + ) != 0 + }, + winit::raw_window_handle_05::RawDisplayHandle::Web(_) => false, + // On android, all devices are required to be able to present to any + // window. + winit::raw_window_handle_05::RawDisplayHandle::Android(_) => { + instance.enabled_extensions().khr_android_surface + } + winit::raw_window_handle_05::RawDisplayHandle::Haiku(_) => false, + _ => false, + } +} + /// High level description of how physical device limits translate into /// limits for fuzzpaint. Note that limits may still be larger than reasonable. pub struct HighLevelLimits { @@ -59,7 +137,6 @@ enum QueueSrc { #[derive(Clone, Copy, Debug)] struct QueueIndices { - /// Also present, if that was required. graphics: u32, graphics_can_present: bool, compute: u32, @@ -116,12 +193,15 @@ impl Queues { } } } - -pub struct RenderSurface { - context: Arc, +pub struct RenderSwapchain { swapchain: Arc, - _surface: Arc, swapchain_images: Vec>, +} +pub struct RenderSurface { + context: Arc, + surface: Arc, + // May be none if surface is zero-sized. + swapchain: Option, swapchain_create_info: vk::SwapchainCreateInfo, } @@ -135,23 +215,28 @@ impl RenderSurface { self.swapchain_create_info.image_format } #[must_use] - pub fn swapchain(&self) -> &Arc { - &self.swapchain + pub fn swapchain(&self) -> Option<&Arc> { + self.swapchain.as_ref().map(|s| &s.swapchain) } #[must_use] - pub fn swapchain_images(&self) -> &[Arc] { - &self.swapchain_images + pub fn swapchain_images(&self) -> Option<&[Arc]> { + self.swapchain + .as_ref() + .map(|s| s.swapchain_images.as_slice()) } #[must_use] pub fn context(&self) -> &Arc { &self.context } - fn new( - context: Arc, - surface: Arc, - size: [u32; 2], - ) -> AnyResult { + pub fn new(context: Arc, window: Arc) -> AnyResult { + let size = window.inner_size(); + let size = [size.width, size.height]; + + let surface = vk::Surface::from_window(context.instance.clone(), window)?; let physical_device = context.physical_device(); + if !physical_device.surface_support(context.queues.graphics().idx(), &surface)? { + anyhow::bail!("does not support present"); + } let surface_info = vk::SurfaceInfo::default(); let capabilies = physical_device.surface_capabilities(&surface, surface_info.clone())?; @@ -204,34 +289,41 @@ impl RenderSurface { clipped: true, // We wont read the framebuffer. ..Default::default() }; - - let (swapchain, images) = vk::Swapchain::new( - context.device().clone(), - surface.clone(), - swapchain_create_info.clone(), - )?; - - Ok(Self { + let mut this = Self { context, - swapchain, - _surface: surface, - swapchain_images: images, + surface, + swapchain: None, swapchain_create_info, - }) + }; + this.recreate(size)?; + Ok(this) } - pub fn recreate(self, new_size: Option<[u32; 2]>) -> AnyResult { - let mut new_info = self.swapchain_create_info; - if let Some(new_size) = new_size { - new_info.image_extent = new_size; + pub fn recreate(&mut self, new_size: [u32; 2]) -> AnyResult<()> { + let old_swapchain = self.swapchain.take(); + self.swapchain_create_info.image_extent = new_size; + + if new_size[0] == 0 || new_size[1] == 0 { + return Ok(()); } - let (swapchain, swapchain_images) = self.swapchain.recreate(new_info.clone())?; - Ok(Self { + self.swapchain_create_info.image_extent = new_size; + let (swapchain, swapchain_images) = if let Some(old_swapchain) = old_swapchain { + drop(old_swapchain.swapchain_images); + old_swapchain + .swapchain + .recreate(self.swapchain_create_info.clone())? + } else { + vk::Swapchain::new( + self.context.device().clone(), + self.surface.clone(), + self.swapchain_create_info.clone(), + )? + }; + self.swapchain = Some(RenderSwapchain { swapchain, swapchain_images, - swapchain_create_info: new_info, - ..self - }) + }); + Ok(()) } } @@ -255,7 +347,7 @@ impl Allocators { pub struct RenderContext { _library: Arc, - _instance: Arc, + instance: Arc, physical_device: Arc, high_level_limits: HighLevelLimits, device: Arc, @@ -267,24 +359,38 @@ pub struct RenderContext { } impl RenderContext { - pub fn new_headless() -> AnyResult { - unimplemented!() - } - pub fn new_with_window_surface< - Window: winit::raw_window_handle_05::HasRawDisplayHandle - + winit::raw_window_handle_05::HasRawWindowHandle - + Send - + Sync - + 'static, - >( - win: Arc, - image_size: [u32; 2], - ) -> AnyResult<(Arc, RenderSurface)> { + /// Create a device without any display or window compatibility. + pub fn new_headless() -> AnyResult> { + // Trivially safe. + unsafe { Self::new_with_display_handle(None) } + } + /// Create a device compatible with windows from the given display. + pub fn new_with_display(dpy: Option<&impl HasRawDisplayHandle>) -> AnyResult> { + // Safe by unsafe precondition of HasRawDisplayHandle + unsafe { Self::new_with_display_handle(dpy.map(|dpy| dpy.raw_display_handle())) } + } + /// #Safety: `dpy` must represent a valid display handle for the duration of + /// the call. + unsafe fn new_with_display_handle( + dpy: Option, + ) -> AnyResult> { use vulkano::instance::debug as vkDebug; let library = vk::VulkanLibrary::new()?; - let mut required_instance_extensions = vk::Surface::required_extensions(&win); + let mut required_instance_extensions = if let Some(dpy) = dpy { + struct Carrier { + dpy: winit::raw_window_handle_05::RawDisplayHandle, + } + unsafe impl HasRawDisplayHandle for Carrier { + fn raw_display_handle(&self) -> winit::raw_window_handle_05::RawDisplayHandle { + self.dpy + } + } + vk::Surface::required_extensions(&Carrier { dpy }) + } else { + vulkano::instance::InstanceExtensions::empty() + }; required_instance_extensions.ext_debug_utils = true; let instance = vk::Instance::new( @@ -352,9 +458,8 @@ impl RenderContext { }, )?; - let surface = vk::Surface::from_window(instance.clone(), win)?; let required_device_extensions = vk::DeviceExtensions { - khr_swapchain: true, + khr_swapchain: dpy.is_some(), ext_line_rasterization: true, ..Default::default() }; @@ -365,15 +470,15 @@ impl RenderContext { ..Default::default() }; - let Some((physical_device, queue_indices)) = Self::choose_physical_device( - &instance, - &required_device_extensions, - &required_device_extensions_lt_1_3, - Some(&surface), - )? - else { - return Err(anyhow::anyhow!("Failed to find a suitable Vulkan device.")); - }; + let (physical_device, queue_indices) = unsafe { + Self::choose_physical_device( + &instance, + &required_device_extensions, + &required_device_extensions_lt_1_3, + dpy, + ) + }? + .ok_or_else(|| anyhow::anyhow!("Failed to find a suitable Vulkan device."))?; log::info!( "Chose physical device {} ({:?})", @@ -406,16 +511,15 @@ impl RenderContext { }, high_level_limits: HighLevelLimits::from_device(&device), _library: library, - _instance: instance, + instance, device, physical_device, queues, _debugger: Some(debugger), }); - let render_surface = RenderSurface::new(context.clone(), surface.clone(), image_size)?; - Ok((context, render_surface)) + Ok(context) } fn create_device( physical_device: Arc, @@ -515,11 +619,12 @@ impl RenderContext { } /// Find a device that fits our needs, including the ability to present to the surface if in non-headless mode. /// Horrible signature - Returns Ok(None) if no device found, Ok(Some((device, queue indices))) if suitable device found. - fn choose_physical_device( + #[deny(unsafe_op_in_unsafe_fn)] + unsafe fn choose_physical_device( instance: &Arc, required_extensions: &vk::DeviceExtensions, required_extensions_lt_1_3: &vk::DeviceExtensions, - compatible_surface: Option<&vk::Surface>, + supports_display: Option, ) -> AnyResult, QueueIndices)>> { //TODO: does not respect queue family max queue counts. This will need to be redone in some sort of //multi-pass shenanigan to properly find a good queue setup. Also requires that graphics and compute queues be transfer as well. @@ -539,30 +644,17 @@ impl RenderContext { let families = device.queue_family_properties(); - //Find a queue that supports the requested surface, if any - let present_queue = compatible_surface.and_then(|surface| { - families.iter().enumerate().find(|(family_idx, _)| { - //Assume error is false. Todo? - device - .surface_support(*family_idx as u32, surface) - .unwrap_or(false) - }) - }); - - //We needed a present queue, but none was found. Disqualify this device! - if compatible_surface.is_some() && present_queue.is_none() { - return None; - } - // We need a graphics queue, always! Otherwise, disqualify. - // If we require present to the surface, ensure that this queue can also do it. - let graphics_queue = families.iter().enumerate().find(|q| { - let is_graphics = q.1.queue_flags.contains(QueueFlags::GRAPHICS); - let can_present = compatible_surface.is_none() - || device - .surface_support(q.0 as u32, compatible_surface.unwrap()) - .unwrap_or(false); - is_graphics && can_present + // Additionally, our graphics queue should be able to present. + let graphics_queue = families.iter().enumerate().find(|(i, properties)| { + if let Some(dpy) = &supports_display + && !unsafe { + physical_device_display_support(instance, &device, *i as _, dpy) + } + { + return false; + } + properties.queue_flags.contains(QueueFlags::GRAPHICS) })?; //We need a compute queue. This can be the same as graphics, but preferably not. @@ -587,7 +679,7 @@ impl RenderContext { QueueIndices { compute: compute_queue.unwrap_or(graphics_queue).0 as u32, // Would have bailed if not! - graphics_can_present: compatible_surface.is_some(), + graphics_can_present: supports_display.is_some(), graphics: graphics_queue.0 as u32, }, )) diff --git a/fuzzpaint/src/renderer/mod.rs b/fuzzpaint/src/renderer/mod.rs index f905284..d2a4577 100644 --- a/fuzzpaint/src/renderer/mod.rs +++ b/fuzzpaint/src/renderer/mod.rs @@ -549,10 +549,10 @@ impl Engines { Which(B), } impl< - 'a, - A: Iterator, - B: Iterator, - > Iterator for EitherIter<'a, A, B> + 'a, + A: Iterator, + B: Iterator, + > Iterator for EitherIter<'a, A, B> { type Item = &'a state::stroke_collection::ImmutableStroke; fn next(&mut self) -> Option { @@ -674,16 +674,13 @@ impl Engines { outer_transform, .. }) => { - let data = - document_data - .graph_render_data - .leaves - .get(&id) - .ok_or_else(|| { - anyhow::anyhow!( + let data = document_data.graph_render_data.leaves.get(&id).ok_or_else( + || { + anyhow::anyhow!( "Expected image to be created by allocate_prune_graph for {id:?}" ) - })?; + }, + )?; let strokes = reader .stroke_collections() @@ -706,16 +703,13 @@ impl Engines { Some(LeafType::Text { text, px_per_em, .. }) => { - let data = - document_data - .graph_render_data - .leaves - .get(&id) - .ok_or_else(|| { - anyhow::anyhow!( + let data = document_data.graph_render_data.leaves.get(&id).ok_or_else( + || { + anyhow::anyhow!( "Expected image to be created by allocate_prune_graph for {id:?}" ) - })?; + }, + )?; fences.push(self.text_layer(text, *px_per_em, data)?); } // No rendering or lazily rendered. @@ -891,61 +885,25 @@ async fn render_changes( renderer: Arc, document_preview: Arc, ) -> anyhow::Result<()> { - // Sync -> Async bridge for change notification. Bleh.. - let (send, mut changes_recv) = tokio::sync::mpsc::unbounded_channel(); - let exit_flag = Arc::new(std::sync::atomic::AtomicBool::new(false)); - let exit_flag_move = exit_flag.clone(); - let _thread = std::thread::spawn(move || { - let mut change_listener = crate::global::provider().change_listener(); - loop { - // Parent requested child exit. - if exit_flag_move.load(std::sync::atomic::Ordering::Relaxed) { - return; - } - // Poll every so often, so an assertion of the exit flag is not missed. - match change_listener.recv_timeout(std::time::Duration::from_millis(250)) { - Ok(change) => { - // Got a change. Broadcast this one (and all others that are ready now) - if send.send(change.id()).is_err() { - // Disconnected! - return; - } - while let Ok(change) = change_listener.try_recv() { - if send.send(change.id()).is_err() { - // Disconnected! - return; - } - } - } - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => (), - Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => return, - } - } - }); - // Drop order - this will run before thread is joined, otherwise deadlock occurs! - defer::defer!(exit_flag.store(true, std::sync::atomic::Ordering::Relaxed)); + let mut change_listener = crate::global::provider().change_listener(); let mut changes: Vec<_> = crate::global::provider().document_iter().collect(); let mut renderer = Renderer::new(renderer)?; loop { - let changes = async { - // Already has some! Report immediately. - if !changes.is_empty() { - return Some(&mut changes); - } - let first = changes_recv.recv().await?; - changes.push(first); - // Collect all others that are available without blocking as well: - while let Ok(next) = changes_recv.try_recv() { - changes.push(next); - } - Some(&mut changes) - }; + if changes.is_empty() { + let first_change = loop { + match change_listener.recv().await { + Ok(change) => break change.id(), + Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => (), + Err(tokio::sync::broadcast::error::RecvError::Closed) => return Ok(()), + } + }; + changes.push(first_change); - let Some(changes) = changes.await else { - // Channel closed - return Ok(()); + while let Ok(change) = change_listener.try_recv() { + changes.push(change.id()); + } }; // Implicitly handles deletion - when the renderer goes to fetch changes, // it will see that the document has closed. @@ -1016,6 +974,8 @@ mod stroke_renderer { texture_descriptors: fuzzpaint_core::brush::UniqueIDMap>, gpu_tess: super::gpu_tess::GpuStampTess, pipeline: Arc, + // Array of 1D R8 textures, used for brush curve LUTs. + // curve_luts: Arc, } impl StrokeLayerRenderer { pub fn new(context: Arc) -> AnyResult { diff --git a/fuzzpaint/src/ui/brush_ui.rs b/fuzzpaint/src/ui/brush_ui.rs index bbfec62..0c8a035 100644 --- a/fuzzpaint/src/ui/brush_ui.rs +++ b/fuzzpaint/src/ui/brush_ui.rs @@ -1,4 +1,5 @@ use fuzzpaint_core::brush::{Brush, Texture, UniqueID}; +use hashbrown::HashMap; use super::ResponseExt; @@ -7,11 +8,551 @@ const FULL_UV: egui::Rect = egui::Rect { max: egui::Pos2 { x: 1.0, y: 1.0 }, }; -#[derive(Copy, Clone, Default, PartialEq, Eq)] +#[derive(strum::IntoStaticStr, strum::EnumIter, Copy, Clone, Default, PartialEq, Eq)] enum CreationTab { #[default] Settings, Texture, + Dynamics(DynamicDestination, DynamicSourceNormalized), +} +#[derive(strum::IntoStaticStr, strum::EnumIter, Debug, PartialEq, Eq, Clone, Copy, Default)] +enum DynamicDestination { + #[default] + Size, + Rotation, + Offset, + Flow, + Spacing, + // Hue, + // Lightness, +} +/// Sources with a constant min an max, mapped to [0, 1]. +#[derive( + strum::IntoStaticStr, strum::EnumIter, Debug, PartialEq, Eq, Clone, Copy, Default, Hash, +)] +enum DynamicSourceNormalized { + #[default] + Pressure, + Roll, + TiltX, + TiltY, + Altitude, + Azimuth, + Direction, + StampRandom, + StrokeRandom, +} +impl DynamicSourceNormalized { + fn domain_labels(self) -> &'static [&'static str] { + match self { + // Unitless, [0, 1] + Self::Pressure | Self::StampRandom | Self::StrokeRandom => &["Min", "Max"], + Self::Roll => &["0deg", "360deg"], + // [-90, 90deg], but more descriptive of their directions. Negative + // is towards top/left. + Self::TiltX => &["Left", "Center", "Right"], + Self::TiltY => &["Forward", "Center", "Back"], + // [0, 90deg], but more descriptive of the directions: + Self::Altitude => &["Horizontal", "Vertical"], + // any angle, in canvas space. 0 = Right, increasing clockwise. + // (This is a consequence of the y axis increasing downwards!) + Self::Azimuth | Self::Direction => &["Right", "Down", "Left", "Up", "Right"], + } + } + fn range(self) -> [f32; 2] { + match self { + Self::Pressure | Self::StampRandom | Self::StrokeRandom => [0.0, 1.0], + Self::Roll | Self::Azimuth | Self::Direction => [0.0, 360.0], + Self::TiltX | Self::TiltY => [-90.0, 90.0], + Self::Altitude => [0.0, 90.0], + } + } + fn description(self) -> &'static str { + match self { + DynamicSourceNormalized::Pressure => "How hard the stylus is pressed into the surface.", + DynamicSourceNormalized::Roll => "Rotation of the stylus along it's own axis", + DynamicSourceNormalized::TiltX => "Left-right tilt from vertical.", + DynamicSourceNormalized::TiltY => "Forward-backward tilt from vertical.", + DynamicSourceNormalized::Altitude => "Verticality of the pen relative to the surface.", + DynamicSourceNormalized::Azimuth => "Direction of tilt.", + DynamicSourceNormalized::Direction => "Stroke movement direction.", + DynamicSourceNormalized::StampRandom => "A random value per-stamp.", + DynamicSourceNormalized::StrokeRandom => "A random value per-stroke.", + } + } +} +/// Unnormalized Units of `[length] * [something]`, including where `something = +/// 1`. These are separated because the different kinds of DPI relations +/// influence how these are calculated depending on which length unit is used. +#[derive(strum::IntoStaticStr, strum::EnumIter, Debug, PartialEq, Eq, Clone, Copy)] +enum DynamicSourceUnnormalizedLengthNumerator { + Speed, + Distance, +} +impl DynamicSourceUnnormalizedLengthNumerator { + /// Just the end of the unit, without the `length` part. + fn unit_suffix(self) -> &'static str { + match self { + Self::Speed => "/s", + Self::Distance => "", + } + } +} +#[derive(Clone, Copy, strum::EnumIter, Default)] +pub enum PhysicalUnit { + #[default] + Centimeter, + Inch, + Point, +} +impl PhysicalUnit { + fn per_centimeter(self) -> f32 { + match self { + Self::Point => 28.346_457, + Self::Inch => 2.54, + Self::Centimeter => 1.0, + } + } + fn unit(self) -> &'static str { + match self { + Self::Point => "pt", + Self::Inch => "in", + Self::Centimeter => "cm", + } + } +} +#[derive(Clone, Copy, strum::EnumIter)] +pub enum LengthUnit { + /// Logical pixels, scale-factor aware. (e.g., if you render at twice the + /// scale factor, this will scale up 2x.) + LogicalPx, + /// Screen pixels, non-scale-factor aware. (e.g., if you render at twice the + /// scale factor, this will not scale up, thus becoming proportionally + /// smaller) + PhysicalPx, + /// Depends on the DPI of the document. Just centimeters times a constant. + Physical(PhysicalUnit), +} +impl LengthUnit { + fn unit(self) -> &'static str { + match self { + Self::LogicalPx => "px", + Self::PhysicalPx => "ppx", + Self::Physical(unit) => unit.unit(), + } + } +} +/// Unnormalized Units that do not involve length. +#[derive(strum::IntoStaticStr, strum::EnumIter, Debug, PartialEq, Eq, Clone, Copy)] +enum DynamicSourceUnnormalized { + Time, +} +impl DynamicSourceUnnormalized { + fn unit(self) -> &'static str { + match self { + Self::Time => "s", + } + } +} +struct CurveWithMax { + max: f32, + curve: CurveNormalized, +} + +#[derive(Clone, Copy)] +struct CurvePoint { + norm_x: f32, + norm_y: f32, +} +struct CurveNormalized { + min_y: f32, + max_y: f32, + // Always ordered by norm_x, always at least two elements. The first element + // will have norm_x = 0.0, the last will have norm_x = 1.0. + points: Vec, + dragged_idx: Option, +} +impl CurveNormalized { + const POINT_RADIUS_PX: f32 = 7.0; + fn new(min: f32, max: f32) -> Self { + Self { + min_y: min, + max_y: max, + points: vec![ + CurvePoint { + norm_x: 0.0, + norm_y: 0.0, + }, + CurvePoint { + norm_x: 1.0, + norm_y: 1.0, + }, + ], + dragged_idx: None, + } + } + fn insert_and_drag(&mut self, norm_x: f32, norm_y: f32) { + match self + .points + .binary_search_by(|point| point.norm_x.total_cmp(&norm_x)) + { + Ok(found_idx) => { + self.points[found_idx].norm_y = norm_y; + self.dragged_idx = Some(found_idx); + } + Err(would_be_idx) => { + self.points + .insert(would_be_idx, CurvePoint { norm_x, norm_y }); + self.dragged_idx = Some(would_be_idx); + } + } + let dragged_idx = self.dragged_idx.unwrap(); + if dragged_idx == 0 { + self.points[dragged_idx].norm_x = 0.0; + } else if dragged_idx == self.points.len() - 1 { + self.points[dragged_idx].norm_x = 1.0; + } + } + fn show( + &mut self, + x_unit: DynamicSourceNormalized, + y_unit: &str, + ui: &mut egui::Ui, + ) -> egui::Response { + let width = ui.available_width(); + let (rect, mut response) = + ui.allocate_at_least(egui::Vec2::splat(width), egui::Sense::click_and_drag()); + if response.is_pointer_button_down_on() { + let pos = response.interact_pointer_pos().unwrap(); + let mut norm_pos = (pos - rect.min) / rect.size(); + norm_pos = norm_pos.clamp(egui::Vec2::ZERO, egui::Vec2::ONE); + norm_pos.y = 1.0 - norm_pos.y; + + if let Some(dragged_idx) = self.dragged_idx { + // Remove, update, then re-insert. This way, the slice remains + // sorted. + let _ = self.points.remove(dragged_idx); + self.insert_and_drag(norm_pos.x, norm_pos.y); + } else { + // New click! Find which is grabbed. + for (i, point) in self.points.iter().enumerate() { + let delta = egui::Vec2::new(point.norm_x, point.norm_y) - norm_pos; + if (delta * rect.size()).length_sq() + < Self::POINT_RADIUS_PX * Self::POINT_RADIUS_PX + { + // Grabbed this one! + self.dragged_idx = Some(i); + break; + } + } + if self.dragged_idx.is_none() { + // Didn't grab any, add one! + self.insert_and_drag(norm_pos.x, norm_pos.y); + } + } + response.mark_changed(); + } else { + self.dragged_idx = None; + } + if ui.ctx().will_discard() { + // Skip drawing, we've already done all the layout egui needs :3 + return response; + } + let painter = ui.painter_at(rect); + painter.rect_filled(rect, 0.0, ui.visuals().extreme_bg_color); + for (i, point) in self.points.iter().enumerate() { + let pos = rect.min + egui::Vec2::new(point.norm_x, 1.0 - point.norm_y) * rect.size(); + let is_dragged = self.dragged_idx == Some(i); + let color = ui.visuals().text_color(); + if is_dragged { + painter.circle_filled(pos, Self::POINT_RADIUS_PX, color); + } else { + painter.circle_stroke(pos, Self::POINT_RADIUS_PX, egui::Stroke::new(2.0, color)); + } + } + { + let text_height = ui.text_style_height(&egui::TextStyle::Small); + let font_id = egui::FontId::monospace(text_height); + let text_color = ui.visuals().weak_text_color(); + painter.text( + rect.right_top(), + egui::Align2::RIGHT_TOP, + format!("{}{y_unit}", self.max_y), + font_id.clone(), + text_color, + ); + painter.text( + rect.right_bottom(), + egui::Align2::RIGHT_BOTTOM, + format!("{}{y_unit}", self.min_y), + font_id.clone(), + text_color, + ); + let labels = x_unit.domain_labels(); + for (i, label) in labels.iter().enumerate() { + let norm_x = i as f32 / (labels.len() - 1) as f32; + let pos = egui::Pos2::new(norm_x * rect.width() + rect.min.x, rect.bottom()); + let anchor = if i == 0 { + egui::Align2::LEFT_BOTTOM + } else if i == labels.len() - 1 { + egui::Align2::RIGHT_BOTTOM + } else { + painter.line_segment( + [ + egui::Pos2::new(pos.x, rect.bottom()), + egui::Pos2::new(pos.x, rect.top()), + ], + // more semantically correct would be weak_bg, but + // for some reason it's invisible on extreme_bg + egui::Stroke::new(1.0, ui.visuals().window_fill), + ); + egui::Align2::CENTER_BOTTOM + }; + painter.text(pos, anchor, label, font_id.clone(), text_color); + } + if let Some(hover) = response.hover_pos() { + let norm_pos = if let Some(dragged_idx) = self.dragged_idx { + let dragged = self.points[dragged_idx]; + egui::Vec2::new(dragged.norm_x, dragged.norm_y) + } else { + (hover - rect.min) / rect.size() + }; + let [min, max] = x_unit.range(); + let human_readable_x_value = norm_pos.x * (max - min) + min; + painter.text( + rect.left_top(), + egui::Align2::LEFT_TOP, + format!("{human_readable_x_value}"), + font_id.clone(), + text_color, + ); + } + } + let points = self + .points + .iter() + .map(|point| rect.min + egui::Vec2::new(point.norm_x, 1.0 - point.norm_y) * rect.size()) + .collect::>(); + painter.line(points, egui::Stroke::new(2.0, ui.visuals().text_color())); + + response + } +} + +#[derive(Copy, Clone, strum::EnumIter, Default)] +pub enum DynamicCombinator { + #[default] + Multipy, + Add, + Min, + Max, + Width, + Average, +} +#[derive(Default)] +pub struct CurveSetNormalized { + curves: HashMap, +} +impl CurveSetNormalized { + fn show(&mut self, selected_source: &mut DynamicSourceNormalized, ui: &mut egui::Ui) { + egui::SidePanel::new( + egui::panel::Side::Left, + egui::Id::new("dynamic_sources_panel"), + ) + .resizable(false) + .show_inside(ui, |ui| { + for source in ::iter() { + ui.horizontal(|ui| { + let exists = self.curves.contains_key(&source); + let mut checked = exists; + ui.checkbox(&mut checked, ()); + if checked != exists { + if checked { + self.curves.insert(source, CurveNormalized::new(0.0, 1.0)); + *selected_source = source; + } else { + self.curves.remove(&source); + } + } + ui.selectable_value(selected_source, source, <&'static str>::from(source)) + .on_hover_text(source.description()); + }); + } + }); + + ui.label(selected_source.description()); + if let Some(curve) = self.curves.get_mut(selected_source) { + curve.show(*selected_source, "", ui); + } + } +} +pub struct NormalizedDynamic { + base: f32, + combinator: DynamicCombinator, + curves: CurveSetNormalized, +} +pub struct LengthDynamic { + unit: LengthUnit, + base: f32, + combinator: DynamicCombinator, +} +pub enum SpacingMode { + /// Expressed as a ratio of the resulting `size` after dynamics. + /// i.e. 1.0 = stamps perfectly side-by-side, 0.5 = 50% overlap, etc. + Ratio(f32), + /// Expressed as a regular dynamic. + Custom(LengthDynamic), +} +pub struct Dynamics { + size: LengthDynamic, + rotation: NormalizedDynamic, + offset: LengthDynamic, + flow: NormalizedDynamic, + spacing: SpacingMode, +} +pub enum StampKind { + Circle(CircleStamp), + Texture(TextureStamp), +} +impl Default for StampKind { + fn default() -> Self { + Self::Circle(CircleStamp::default()) + } +} +impl StampKind { + fn show(&mut self, ui: &mut egui::Ui) { + egui::ComboBox::from_id_salt(ui.id()) + .selected_text(match self { + StampKind::Circle(_) => "Circle", + StampKind::Texture(_) => "Texture", + }) + .show_ui(ui, |ui| { + if ui + .selectable_label(matches!(self, StampKind::Circle(_)), "Circle") + .clicked() + { + *self = StampKind::Circle(CircleStamp::default()); + } + if ui + .selectable_label(matches!(self, StampKind::Texture(_)), "Texture") + .clicked() + { + *self = StampKind::Texture(TextureStamp::default()); + } + }); + match self { + StampKind::Circle(circle) => { + circle.show(ui); + } + StampKind::Texture(texture) => { + texture.show(ui); + } + } + } +} +pub struct TextureStamp { + // Imported texture handle, frees on drop! + texture: Option, + uv_rect: egui::Rect, +} +impl TextureStamp { + fn show(&mut self, ui: &mut egui::Ui) { + if ui.button(super::GROUP_ICON).clicked() { + if let Some(file) = rfd::FileDialog::default().pick_file() { + let try_load = || -> anyhow::Result { + // `image` crate is probably not the choice here. It sweeps + // a lot of details under the rug, like colorspaces. + let image = image::open(file)?.to_rgba8(); + let manager = ui.ctx().tex_manager(); + let mut write = manager.write(); + + let size = [image.width() as usize, image.height() as usize]; + + // Create a reference-counted image out of it, refs = 1 + let texture_id = write.alloc( + "Preview brush texture".to_owned(), + egui::ImageData::Color( + egui::ColorImage { + pixels: image + .pixels() + .map(|rgba| { + egui::Color32::from_rgba_unmultiplied( + rgba.0[0], rgba.0[1], rgba.0[2], rgba.0[3], + ) + }) + .collect(), + size, + source_size: egui::Vec2 { + x: size[0] as f32, + y: size[1] as f32, + }, + } + .into(), + ), + egui::TextureOptions { + magnification: egui::TextureFilter::Nearest, + minification: egui::TextureFilter::Linear, + wrap_mode: egui::TextureWrapMode::ClampToEdge, + mipmap_mode: None, + }, + ); + + drop(write); + + // This handle takes the only existing ref, dropping it destroys the image. + Ok(egui::TextureHandle::new(manager, texture_id)) + }; + + match try_load() { + Ok(image) => self.texture = Some(image), + Err(err) => log::error!("Failed to load image: {err}"), + } + } + } + + if let Some(texture) = self.texture.as_ref() { + let width = ui.available_width(); + + uv_picker( + ui, + egui::Vec2::splat(width), + &mut self.uv_rect, + FULL_UV, + texture.id(), + ); + } + } +} +impl Default for TextureStamp { + fn default() -> Self { + Self { + texture: None, + uv_rect: FULL_UV, + } + } +} +#[derive(Default)] +pub struct CircleStamp { + smoothness: Option, +} +impl CircleStamp { + const DEFUALT_SMOOTHNESS: f32 = 1.0; + fn show(&mut self, ui: &mut egui::Ui) -> () { + let mut smoothed = self.smoothness.is_some(); + ui.checkbox(&mut smoothed, "Smooth"); + if smoothed { + self.smoothness = self.smoothness.or(Some(Self::DEFUALT_SMOOTHNESS)); + } else { + self.smoothness = None; + } + + let mut dont_care = Self::DEFUALT_SMOOTHNESS; + let smoothness_mut = self.smoothness.as_mut().unwrap_or(&mut dont_care); + ui.add_enabled( + smoothed, + egui::Slider::new(smoothness_mut, 0.0..=10.0).clamping(egui::SliderClamping::Never), + ); + } } pub struct CreationOutput { @@ -20,20 +561,17 @@ pub struct CreationOutput { } pub struct CreationModal { tab: CreationTab, - // Imported texture handle, frees on drop! - texture: Option, - uv_rect: egui::Rect, name: String, - spacing_proportion: f32, + stamp: StampKind, + test_curves: CurveSetNormalized, } impl Default for CreationModal { fn default() -> Self { Self { tab: CreationTab::default(), - texture: None, - uv_rect: FULL_UV, name: "New Brush".to_owned(), - spacing_proportion: 5.0, + stamp: StampKind::default(), + test_curves: CurveSetNormalized::default(), } } } @@ -47,88 +585,64 @@ impl super::Modal for CreationModal { ui: &mut egui::Ui, ) -> super::modal::Response { ui.horizontal(|ui| { - ui.selectable_value(&mut self.tab, CreationTab::Settings, "Settings"); - ui.selectable_value(&mut self.tab, CreationTab::Texture, "Texture"); + for tab in ::iter() { + // Can't use selectable label here, as it incorrectly checks the + // fields of the enum for equality too! + let same_discriminant = + std::mem::discriminant(&tab) == std::mem::discriminant(&self.tab); + if ui + .selectable_label(same_discriminant, <&'static str>::from(tab)) + .clicked() + { + self.tab = tab; + } + } }); ui.separator(); + let cancel_response = egui::panel::TopBottomPanel::new( + egui::panel::TopBottomSide::Bottom, + egui::Id::new("brush-cancel-panel"), + ) + .show_inside(ui, |ui| { + if ui.button("Cancel").clicked_or_escape() { + super::modal::Response::Cancel(()) + } else { + super::modal::Response::Continue + } + }) + .inner; + if !matches!(cancel_response, super::modal::Response::Continue) { + return cancel_response; + } match self.tab { CreationTab::Settings => { ui.text_edit_singleline(&mut self.name); - ui.add( - egui::Slider::new(&mut self.spacing_proportion, 2.0..=100.0) - .text("Spacing") - .clamping(egui::SliderClamping::Edits) - .suffix("%"), - ); } CreationTab::Texture => { - if ui.button(super::GROUP_ICON).clicked() { - if let Some(file) = rfd::FileDialog::default().pick_file() { - let try_load = || -> anyhow::Result { - // `image` crate is probably not the choice here. It sweeps a lot of details under the rug and doesn't - // exactly do those details justice lol (colorspaces are wayy off) - let image = image::open(file)?.to_rgba8(); - let manager = ui.ctx().tex_manager(); - let mut write = manager.write(); - - let size = [image.width() as usize, image.height() as usize]; - - // Create a reference-counted image out of it, refs = 1 - let texture_id = write.alloc( - "Preview brush texture".to_owned(), - egui::ImageData::Color( - egui::ColorImage { - pixels: image - .pixels() - .map(|rgba| { - egui::Color32::from_rgba_unmultiplied( - rgba.0[0], rgba.0[1], rgba.0[2], rgba.0[3], - ) - }) - .collect(), - size, - source_size: egui::Vec2 { - x: size[0] as f32, - y: size[1] as f32, - }, - } - .into(), - ), - egui::TextureOptions { - magnification: egui::TextureFilter::Nearest, - minification: egui::TextureFilter::Linear, - wrap_mode: egui::TextureWrapMode::ClampToEdge, - mipmap_mode: None, - }, - ); - - drop(write); - - // This handle takes the only existing ref, dropping it destroys the image. - Ok(egui::TextureHandle::new(manager, texture_id)) - }; - - match try_load() { - Ok(image) => self.texture = Some(image), - Err(err) => log::error!("Failed to load image: {err}"), - } + self.stamp.show(ui); + } + CreationTab::Dynamics(mut selected_dynamic, mut selected_source) => { + egui::SidePanel::new( + egui::panel::Side::Left, + egui::Id::new("selected_dynamic_panel"), + ) + .frame(egui::Frame::new().fill(ui.visuals().extreme_bg_color)) + .resizable(false) + .show_inside(ui, |ui| { + for dynamic in ::iter() { + ui.selectable_value( + &mut selected_dynamic, + dynamic, + <&'static str>::from(dynamic), + ); } - } - - if let Some(texture) = self.texture.as_ref() { - let width = ui.available_width(); - - uv_picker( - ui, - egui::Vec2::splat(width), - &mut self.uv_rect, - FULL_UV, - texture.id(), - ); - } + }); + self.test_curves.show(&mut selected_source, ui); + self.tab = CreationTab::Dynamics(selected_dynamic, selected_source); } } + /* if let Some(texture) = self.texture.as_ref() { let width = ui.available_width(); let height = width / 3.0; @@ -146,13 +660,8 @@ impl super::Modal for CreationModal { ); painter.rect_filled(response.rect, 0.0, egui::Color32::BLACK); painter.add(egui::Shape::mesh(mesh)); - } - ui.separator(); - if ui.button("Cancel").clicked_or_escape() { - super::modal::Response::Cancel(()) - } else { - super::modal::Response::Continue - } + }*/ + super::modal::Response::Continue } } diff --git a/fuzzpaint/src/ui/color_palette.rs b/fuzzpaint/src/ui/color_palette.rs index b4c49bf..b498995 100644 --- a/fuzzpaint/src/ui/color_palette.rs +++ b/fuzzpaint/src/ui/color_palette.rs @@ -115,6 +115,9 @@ impl egui::Widget for ColorSquare { text_selection: None, hint_text: None, }); + if ui.ctx().will_discard() { + return this; + } // false if all in the normal range of colors let out_of_gammut = self diff --git a/fuzzpaint/src/ui/drag.rs b/fuzzpaint/src/ui/drag.rs index 38416d2..76f32a8 100644 --- a/fuzzpaint/src/ui/drag.rs +++ b/fuzzpaint/src/ui/drag.rs @@ -5,6 +5,9 @@ impl egui::Widget for Handle { let interact_height = ui.style().spacing.interact_size.y; let size = egui::vec2(interact_height * 2.0 / 3.0, interact_height); let response = ui.allocate_response(size, egui::Sense::drag()); + if ui.ctx().will_discard() { + return response; + } // Paint six dots, a somewhat universal drag icon. let painter = ui.painter(); diff --git a/fuzzpaint/src/ui/mod.rs b/fuzzpaint/src/ui/mod.rs index 9ae8ee5..82fe9a2 100644 --- a/fuzzpaint/src/ui/mod.rs +++ b/fuzzpaint/src/ui/mod.rs @@ -32,6 +32,17 @@ const PIN_ICON: char = '📌'; const ALPHA_ICON: &str = "α"; const RESET_ICON: &str = "⟲"; +struct Main { + executor: tokio::runtime::Runtime, + connections: Vec, + on_recv: tokio::sync::Notify, +} +impl Main { + async fn wait_recv(&mut self) { + self.on_recv.notified().await; + } +} + /// Justify `(available_size, size, margin)` -> `(size', margin')`, such that `count` elements /// will fill available space completely. /// @@ -215,7 +226,11 @@ impl MainUI { /// Main UI and any modals, with the top bar, layers, brushes, color, etc. To be displayed in front of the document and it's gizmos. /// Returns the size of the document's viewport space - that is, the size of the rect not covered by any side/top/bottom panels. /// None if a full-screen menu is shown. - pub fn ui(&mut self, ctx: &egui::Context) -> Option<(ultraviolet::Vec2, ultraviolet::Vec2)> { + pub fn ui( + &mut self, + ctx: &egui::Context, + connections: &mut crate::connections::ConnectionsLock, + ) -> Option<(ultraviolet::Vec2, ultraviolet::Vec2)> { // Close modal, on top of everything. if self.modal_enable() { self.do_close_modal(ctx); @@ -225,7 +240,36 @@ impl MainUI { self.do_modal(ctx, !self.modal_enable()); // Show, but disable if modal exists. - self.main_ui(ctx, !self.background_enable()) + let res = self.main_ui(ctx, !self.background_enable()); + + for (id, mut connection) in connections.iter_connections() { + egui::Window::new(format!("{id:?}")).show(ctx, |ui| { + egui::TopBottomPanel::bottom(ui.id().with("text-input")).show_inside(ui, |ui| { + latch::latch(ui, "text", String::new(), |ui, string| { + let response = ui.text_edit_singleline(string); + if response.lost_focus() || ui.input(|i| i.key_pressed(egui::Key::Enter)) { + // Entered, return the text. + latch::Latch::Finish + } else if string.is_empty() { + // Nothing to store + latch::Latch::None + } else { + // Retain typing progress + latch::Latch::Continue + } + }) + .on_finish(|string| connection.message(&string)); + }); + egui::Grid::new("text").num_columns(1).show(ui, |ui| { + for message in connection.messages() { + ui.label(message); + ui.end_row(); + } + }); + }); + } + + res } fn get_cur_interface(&mut self) -> Option<&mut PerDocumentData> { // Get the document's interface, or reset to none if not found. @@ -418,6 +462,14 @@ impl MainUI { ui.disable(); } self.menu_bar(ui); + ui.painter().add(egui::PaintCallback { + rect: egui::Rect::ZERO, + callback: std::sync::Arc::new(crate::egui_impl::Callback { + kind: crate::egui_impl::CallbackKind::DocumentView { + dummy_color: [1, 2, 3, 4], + }, + }), + }) }); if self.cur_document.is_none() { @@ -1973,8 +2025,8 @@ fn graph_edit_recurse< let header_response = ui.horizontal(|ui| { let data = graph.get(id).unwrap(); - // Disable everything if dragging a layer around. - if dnd_state.is_none() { + // Disable everything else if dragging a layer. + if dnd_state.is_some_and(|state| state.drag_target != id) { ui.disable(); } diff --git a/fuzzpaint/src/window.rs b/fuzzpaint/src/window.rs index 6b4f67e..334d5d6 100644 --- a/fuzzpaint/src/window.rs +++ b/fuzzpaint/src/window.rs @@ -3,7 +3,8 @@ use crate::egui_impl; use crate::render_device; use crate::vulkano_prelude::*; -type UserEvent = std::convert::Infallible; +struct RemoteChanged(); +type UserEvent = RemoteChanged; use std::sync::{Arc, Weak}; @@ -16,29 +17,65 @@ enum State { } pub struct Application { + pre_setup_loop: Option>, // Objects that depend on a window, including the window itself. window_objects: State, + connections: crate::connections::ClientConnectionsManager, + render_context: Arc, // Channel that will be notified when the renderer is made, once the window // is ready. renderer_sender: Option>, renderer_reciever: Option>, } impl Application { - pub fn new() -> Self { + pub fn new() -> AnyResult { + impl crate::connections::Waker for winit::event_loop::EventLoopProxy { + fn wake(&self, which: crate::connections::ConnectionID) { + self.send_event(RemoteChanged()); + } + } + // This is needed to coerce T: Trait -> Box. For some reason + // `as` unsizing syntax breaks, it's genuinely haunted. + fn unsize_waker( + t: T, + ) -> Box { + Box::new(t) + } + + let pre_setup_loop = + winit::event_loop::EventLoop::::with_user_event().build()?; + let render_context = render_device::RenderContext::new_with_display(Some(&pre_setup_loop))?; let (send, recv) = oneshot::channel(); - Self { + + let connections = crate::connections::ClientConnectionsManager::spawn(unsize_waker( + pre_setup_loop.create_proxy(), + ))?; + + Ok(Self { + pre_setup_loop: Some(pre_setup_loop), window_objects: State::Deferred, + connections, + render_context, renderer_sender: Some(send), renderer_reciever: Some(recv), - } + }) + } + pub fn connections(&mut self) -> &mut crate::connections::ClientConnectionsManager { + &mut self.connections + } + pub fn render_context(&self) -> &Arc { + &self.render_context } /// Take a channel that will recieve the rendering context, once it is created. pub fn take_renderer_reciever(&mut self) -> Option> { self.renderer_reciever.take() } pub fn run(mut self) -> AnyResult<()> { - let event_loop = winit::event_loop::EventLoop::::with_user_event().build()?; - event_loop.run_app(&mut self).map_err(Into::into) + self.pre_setup_loop + .take() + .unwrap() + .run_app(&mut self) + .map_err(Into::into) } } impl winit::application::ApplicationHandler for Application { @@ -47,7 +84,7 @@ impl winit::application::ApplicationHandler for Application { // Always emitted first, even on platforms without a suspend-resume // cycle. Only recreate if it's the first time (i.e. dont attempt to // recreate after a suspend.) - match WindowObjects::new(event_loop) { + match WindowObjects::new(self.render_context.clone(), event_loop) { Ok(window_objects) => { if let Some(send) = self.renderer_sender.take() { let _ = send.send(window_objects.receivers()); @@ -70,28 +107,40 @@ impl winit::application::ApplicationHandler for Application { } fn window_event( &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, + _event_loop: &winit::event_loop::ActiveEventLoop, _window_id: winit::window::WindowId, event: winit::event::WindowEvent, ) { let State::Extant(window_objects) = &mut self.window_objects else { return; }; - window_objects.window_event(event_loop, event); + match event { + winit::event::WindowEvent::RedrawRequested => { + window_objects.redraw_requested(self.connections.lock()) + } + event => window_objects.window_event(event), + } } fn device_event( &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, + _event_loop: &winit::event_loop::ActiveEventLoop, device_id: winit::event::DeviceId, event: winit::event::DeviceEvent, ) { let State::Extant(window_objects) = &mut self.window_objects else { return; }; - window_objects.device_event(event_loop, device_id, event); + window_objects.device_event(device_id, event); } fn user_event(&mut self, _event_loop: &winit::event_loop::ActiveEventLoop, event: UserEvent) { - match event {} + let State::Extant(window_objects) = &mut self.window_objects else { + return; + }; + match event { + // Must be called by the main thread for portability, hence the use + // of a user event. + RemoteChanged() => window_objects.win.request_redraw(), + } } fn about_to_wait(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { let State::Extant(window_objects) = &mut self.window_objects else { @@ -108,14 +157,11 @@ pub struct Receivers { pub actions: crate::actions::ActionListener, pub ui_actions: crossbeam::channel::Receiver, pub stylus_events: tokio::sync::broadcast::Receiver, - pub render_context: Arc, pub document_view: Arc, } pub struct WindowObjects { win: Arc, - /// Always Some. This is to allow it to be take-able to be remade. - /// Could None represent a temporary loss of surface that can be recovered from? - render_surface: Option, + render_surface: render_device::RenderSurface, render_context: Arc, egui_ctx: egui_impl::Ctx, ui: crate::ui::MainUI, @@ -134,7 +180,10 @@ pub struct WindowObjects { preview_renderer: Arc, } impl WindowObjects { - fn new(event_loop: &winit::event_loop::ActiveEventLoop) -> AnyResult { + fn new( + render_context: Arc, + event_loop: &winit::event_loop::ActiveEventLoop, + ) -> AnyResult { const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); let win = event_loop.create_window( winit::window::Window::default_attributes() @@ -144,11 +193,8 @@ impl WindowObjects { )?; let win = Arc::new(win); - let (render_context, render_surface) = - render_device::RenderContext::new_with_window_surface( - win.clone(), - win.inner_size().into(), - )?; + let render_surface = + render_device::RenderSurface::new(render_context.clone(), win.clone())?; let preview_renderer = Arc::new(crate::document_viewport_proxy::Proxy::new(&render_surface)?); @@ -164,7 +210,7 @@ impl WindowObjects { Ok(Self { win, - render_surface: Some(render_surface), + render_surface, swapchain_generation: 0, render_context, last_frame_fence: None, @@ -187,29 +233,21 @@ impl WindowObjects { actions: self.action_stream.listen(), ui_actions: self.ui.listen_requests(), stylus_events: self.stylus_events.frame_receiver(), - render_context: self.render_context.clone(), document_view: self.preview_renderer.clone(), } } pub fn render_surface(&self) -> &render_device::RenderSurface { - //this will ALWAYS be Some. The option is for taking from a mutable reference for recreation. - self.render_surface.as_ref().unwrap() + &self.render_surface } /// Recreate surface after loss or out-of-date. Todo: This only handles out-of-date and resize. pub fn recreate_surface(&mut self) -> AnyResult<()> { - let new_surface = self - .render_surface - .take() - .unwrap() - .recreate(Some(self.window().inner_size().into()))?; + self.render_surface + .recreate(self.window().inner_size().into())?; + self.egui_ctx.replace_surface(&self.render_surface)?; - self.egui_ctx.replace_surface(&new_surface)?; - - self.render_surface = Some(new_surface); self.swapchain_generation = self.swapchain_generation.wrapping_add(1); - self.preview_renderer - .surface_changed(self.render_surface.as_ref().unwrap()); + self.preview_renderer.surface_changed(&self.render_surface); Ok(()) } @@ -231,11 +269,7 @@ impl WindowObjects { } } } - pub fn window_event( - &mut self, - _vent_loop: &winit::event_loop::ActiveEventLoop, - event: winit::event::WindowEvent, - ) { + pub fn window_event(&mut self, event: winit::event::WindowEvent) { use winit::event::WindowEvent; let consumed = self .egui_ctx @@ -274,24 +308,24 @@ impl WindowObjects { } } WindowEvent::RedrawRequested => { - // run UI logics - if self.egui_ctx.take_wants_update() { - self.do_ui(); - } - // Overwrite the Egui provided cursor over the doc area. - self.apply_document_cursor(); - - // Render and present the updated UI - if let Err(e) = self.paint() { - log::error!("{e:?}"); - } + // Handled externally with a call to Self::redraw_requested + unreachable!() } _ => (), } } + pub fn redraw_requested(&mut self, connections: crate::connections::ConnectionsLock) { + self.do_ui(connections); + // Overwrite the Egui provided cursor over the doc area. + self.apply_document_cursor(); + + // Render and present the updated UI + if let Err(e) = self.paint() { + log::error!("{e:?}"); + } + } pub fn device_event( &mut self, - _event_loop: &winit::event_loop::ActiveEventLoop, _device_id: winit::event::DeviceId, event: winit::event::DeviceEvent, ) { @@ -315,6 +349,9 @@ impl WindowObjects { // No need to redraw. return; } + if self.egui_ctx.take_wants_update() { + self.win.request_redraw(); + } let has_tablet_update = if let Some(tab_events) = self.tablet_manager.as_mut().and_then(|m| m.pump().ok()) @@ -393,10 +430,12 @@ impl WindowObjects { std::time::Duration::from_millis(50), )); } - fn do_ui(&mut self) { + fn do_ui(&mut self, mut connections: crate::connections::ConnectionsLock) { let viewport = self .egui_ctx - .update(self.win.as_ref(), |ctx| self.ui.ui(ctx)); + .update(self.win.as_ref(), |ctx| self.ui.ui(ctx, &mut connections)); + // Drop the lock ASAP. + drop(connections); // Todo: only change if... actually changed :P if let Some(viewport) = viewport { @@ -416,22 +455,24 @@ impl WindowObjects { } } fn paint(&mut self) -> AnyResult<()> { - let (idx, suboptimal, image_future) = - match vk::acquire_next_image(self.render_surface().swapchain().clone(), None) { - Err(vk::Validated::Error(vk::VulkanError::OutOfDate)) => { - log::info!("Swapchain unusable. Recreating"); - //We cannot draw on this surface as-is. Recreate and request another try next frame. - //TODO: Race condition, somehow! Surface is recreated with an out-of-date size. - self.recreate_surface()?; - self.window().request_redraw(); - return Ok(()); - } - Err(e) => { - //Todo. Many of these errors are recoverable! - anyhow::bail!("Surface image acquire failed! {e:?}"); - } - Ok(r) => r, - }; + let (idx, suboptimal, image_future) = match vk::acquire_next_image( + self.render_surface().swapchain().unwrap().clone(), + None, + ) { + Err(vk::Validated::Error(vk::VulkanError::OutOfDate)) => { + log::info!("Swapchain unusable. Recreating"); + //We cannot draw on this surface as-is. Recreate and request another try next frame. + //TODO: Race condition, somehow! Surface is recreated with an out-of-date size. + self.recreate_surface()?; + self.window().request_redraw(); + return Ok(()); + } + Err(e) => { + //Todo. Many of these errors are recoverable! + anyhow::bail!("Surface image acquire failed! {e:?}"); + } + Ok(r) => r, + }; // Print a warning if swapchain image future is dropped. Per a dire warning in the comments of vulkano, // dropping futures can result in that swapchain image being lost forever...! @@ -442,7 +483,7 @@ impl WindowObjects { let preview_commands = self.enable_document_view.then(|| unsafe { self.preview_renderer.render( - self.render_surface.as_ref().unwrap().swapchain_images()[idx as usize].clone(), + self.render_surface.swapchain_images().unwrap()[idx as usize].clone(), idx, ) }); @@ -529,7 +570,7 @@ impl WindowObjects { .queue() .clone(), vk::SwapchainPresentInfo::swapchain_image_index( - self.render_surface.as_ref().unwrap().swapchain().clone(), + self.render_surface.swapchain().unwrap().clone(), idx, ), )