From 5daea645856ace06da2232abfdb078af30de8a44 Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 8 Jul 2026 23:34:00 +0000 Subject: [PATCH 1/6] Extract CEF rendered UI into a separate process and crate --- Cargo.lock | 214 ++++++++-- Cargo.toml | 1 + desktop/Cargo.toml | 8 +- desktop/bundle/src/common.rs | 6 +- desktop/bundle/src/linux.rs | 2 +- desktop/bundle/src/mac.rs | 4 +- desktop/bundle/src/win.rs | 2 +- desktop/platform/linux/src/main.rs | 4 +- desktop/platform/mac/Cargo.toml | 9 +- desktop/platform/mac/src/helper.rs | 4 +- desktop/platform/mac/src/main.rs | 4 +- desktop/platform/win/src/main.rs | 4 +- desktop/src/app.rs | 75 ++-- desktop/src/cef.rs | 245 ----------- desktop/src/cef/context.rs | 16 - desktop/src/cef/context/builder.rs | 221 ---------- desktop/src/cef/context/multithreaded.rs | 73 ---- desktop/src/cef/context/singlethreaded.rs | 61 --- desktop/src/cef/input.rs | 149 ------- desktop/src/cef/view.rs | 108 ----- desktop/src/consts.rs | 4 - desktop/src/dirs.rs | 49 +-- desktop/src/event.rs | 4 +- desktop/src/lib.rs | 93 +++-- desktop/src/main.rs | 4 +- desktop/src/window.rs | 31 +- desktop/ui/Cargo.toml | 54 +++ desktop/{src/cef => ui/src}/consts.rs | 16 +- desktop/ui/src/context.rs | 315 ++++++++++++++ desktop/ui/src/delegate.rs | 59 +++ desktop/ui/src/dirs.rs | 50 +++ desktop/ui/src/events.rs | 41 ++ desktop/ui/src/frames.rs | 12 + desktop/ui/src/frames/import.rs | 61 +++ desktop/ui/src/frames/import/d3d11.rs | 145 +++++++ desktop/ui/src/frames/import/dmabuf.rs | 202 +++++++++ desktop/ui/src/frames/import/iosurface.rs | 93 +++++ desktop/ui/src/frames/plane.rs | 35 ++ desktop/ui/src/frames/plane/linux.rs | 238 +++++++++++ desktop/ui/src/frames/plane/mac.rs | 275 +++++++++++++ desktop/ui/src/frames/plane/win.rs | 161 ++++++++ desktop/ui/src/frames/receive.rs | 131 ++++++ desktop/ui/src/frames/sequence.rs | 62 +++ desktop/ui/src/frames/sink.rs | 45 ++ desktop/ui/src/frames/streamer.rs | 155 +++++++ desktop/ui/src/frames/surface.rs | 129 ++++++ desktop/ui/src/input.rs | 258 ++++++++++++ desktop/{src/cef => ui/src}/input/keymap.rs | 0 desktop/{src/cef => ui/src}/input/state.rs | 30 +- desktop/{src/cef => ui/src}/internal.rs | 1 - .../src}/internal/browser_process_app.rs | 24 +- .../src}/internal/browser_process_client.rs | 36 +- .../src}/internal/browser_process_handler.rs | 33 +- .../src}/internal/context_menu_handler.rs | 0 .../src}/internal/display_handler.rs | 35 +- .../src}/internal/life_span_handler.rs | 0 .../cef => ui/src}/internal/load_handler.rs | 24 +- .../cef => ui/src}/internal/render_handler.rs | 42 +- .../src}/internal/render_process_app.rs | 17 +- .../src}/internal/render_process_handler.rs | 2 +- .../internal/render_process_v8_handler.rs | 2 +- .../src}/internal/request_handler.rs | 2 +- .../src}/internal/resource_handler.rs | 2 +- .../src}/internal/resource_request_handler.rs | 2 +- .../src}/internal/scheme_handler_factory.rs | 44 +- desktop/{src/cef => ui/src}/internal/task.rs | 0 desktop/{src/cef => ui/src}/ipc.rs | 0 desktop/ui/src/lib.rs | 255 ++++++++++++ desktop/{src/cef => ui/src}/platform.rs | 31 +- desktop/ui/src/platform/linux.rs | 35 ++ desktop/ui/src/platform/mac.rs | 63 +++ desktop/ui/src/platform/win.rs | 39 ++ desktop/ui/src/remote.rs | 34 ++ desktop/ui/src/remote/host.rs | 118 ++++++ desktop/ui/src/remote/messages.rs | 50 +++ desktop/ui/src/remote/spawn.rs | 388 ++++++++++++++++++ desktop/ui/src/resources.rs | 101 +++++ desktop/{src/cef => ui/src}/utility.rs | 0 desktop/ui/src/view.rs | 70 ++++ 79 files changed, 4157 insertions(+), 1255 deletions(-) delete mode 100644 desktop/src/cef.rs delete mode 100644 desktop/src/cef/context.rs delete mode 100644 desktop/src/cef/context/builder.rs delete mode 100644 desktop/src/cef/context/multithreaded.rs delete mode 100644 desktop/src/cef/context/singlethreaded.rs delete mode 100644 desktop/src/cef/input.rs delete mode 100644 desktop/src/cef/view.rs create mode 100644 desktop/ui/Cargo.toml rename desktop/{src/cef => ui/src}/consts.rs (52%) create mode 100644 desktop/ui/src/context.rs create mode 100644 desktop/ui/src/delegate.rs create mode 100644 desktop/ui/src/dirs.rs create mode 100644 desktop/ui/src/events.rs create mode 100644 desktop/ui/src/frames.rs create mode 100644 desktop/ui/src/frames/import.rs create mode 100644 desktop/ui/src/frames/import/d3d11.rs create mode 100644 desktop/ui/src/frames/import/dmabuf.rs create mode 100644 desktop/ui/src/frames/import/iosurface.rs create mode 100644 desktop/ui/src/frames/plane.rs create mode 100644 desktop/ui/src/frames/plane/linux.rs create mode 100644 desktop/ui/src/frames/plane/mac.rs create mode 100644 desktop/ui/src/frames/plane/win.rs create mode 100644 desktop/ui/src/frames/receive.rs create mode 100644 desktop/ui/src/frames/sequence.rs create mode 100644 desktop/ui/src/frames/sink.rs create mode 100644 desktop/ui/src/frames/streamer.rs create mode 100644 desktop/ui/src/frames/surface.rs create mode 100644 desktop/ui/src/input.rs rename desktop/{src/cef => ui/src}/input/keymap.rs (100%) rename desktop/{src/cef => ui/src}/input/state.rs (90%) rename desktop/{src/cef => ui/src}/internal.rs (95%) rename desktop/{src/cef => ui/src}/internal/browser_process_app.rs (86%) rename desktop/{src/cef => ui/src}/internal/browser_process_client.rs (72%) rename desktop/{src/cef => ui/src}/internal/browser_process_handler.rs (52%) rename desktop/{src/cef => ui/src}/internal/context_menu_handler.rs (100%) rename desktop/{src/cef => ui/src}/internal/display_handler.rs (85%) rename desktop/{src/cef => ui/src}/internal/life_span_handler.rs (100%) rename desktop/{src/cef => ui/src}/internal/load_handler.rs (65%) rename desktop/{src/cef => ui/src}/internal/render_handler.rs (58%) rename desktop/{src/cef => ui/src}/internal/render_process_app.rs (70%) rename desktop/{src/cef => ui/src}/internal/render_process_handler.rs (98%) rename desktop/{src/cef => ui/src}/internal/render_process_v8_handler.rs (97%) rename desktop/{src/cef => ui/src}/internal/request_handler.rs (97%) rename desktop/{src/cef => ui/src}/internal/resource_handler.rs (98%) rename desktop/{src/cef => ui/src}/internal/resource_request_handler.rs (96%) rename desktop/{src/cef => ui/src}/internal/scheme_handler_factory.rs (54%) rename desktop/{src/cef => ui/src}/internal/task.rs (100%) rename desktop/{src/cef => ui/src}/ipc.rs (100%) create mode 100644 desktop/ui/src/lib.rs rename desktop/{src/cef => ui/src}/platform.rs (67%) create mode 100644 desktop/ui/src/platform/linux.rs create mode 100644 desktop/ui/src/platform/mac.rs create mode 100644 desktop/ui/src/platform/win.rs create mode 100644 desktop/ui/src/remote.rs create mode 100644 desktop/ui/src/remote/host.rs create mode 100644 desktop/ui/src/remote/messages.rs create mode 100644 desktop/ui/src/remote/spawn.rs create mode 100644 desktop/ui/src/resources.rs rename desktop/{src/cef => ui/src}/utility.rs (100%) create mode 100644 desktop/ui/src/view.rs diff --git a/Cargo.lock b/Cargo.lock index 5a62bc852c..bd05098735 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -598,24 +598,16 @@ version = "149.2.0+149.0.5" source = "git+https://github.com/timon-schelling/cef-rs.git?branch=graphite-149#877c9cd8f7776e6c52e365d501ad44c73e17117a" dependencies = [ "anyhow", - "ash", "cargo_metadata 0.23.1", "cef-dll-sys", "clap", - "libc", "libloading 0.9.0", "objc2 0.6.3", - "objc2-foundation 0.3.2", - "objc2-io-surface", - "objc2-metal 0.3.2", "plist", "semver", "serde", "serde_json", "thiserror 2.0.18", - "tracing", - "wgpu", - "windows", "windows-sys 0.61.2", ] @@ -777,6 +769,15 @@ dependencies = [ "cc", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "codespan-reporting" version = "0.13.1" @@ -1223,7 +1224,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1413,6 +1414,18 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -1474,7 +1487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1850,11 +1863,22 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasi 0.14.3+wasi-0.2.4", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + [[package]] name = "gif" version = "0.13.3" @@ -1931,7 +1955,7 @@ dependencies = [ "log", "presser", "thiserror 2.0.18", - "windows", + "windows 0.62.2", ] [[package]] @@ -2182,8 +2206,6 @@ name = "graphite-desktop" version = "0.1.0" dependencies = [ "bytemuck", - "cef", - "cef-dll-sys", "clap", "ctrlc", "derivative", @@ -2191,7 +2213,7 @@ dependencies = [ "fd-lock", "futures", "glam", - "graphite-desktop-embedded-resources", + "graphite-desktop-ui", "graphite-desktop-wrapper", "interprocess", "lzma-rust2", @@ -2211,7 +2233,7 @@ dependencies = [ "vello", "wgpu", "window_clipboard", - "windows", + "windows 0.62.2", "winit", ] @@ -2243,6 +2265,7 @@ name = "graphite-desktop-platform-mac" version = "0.0.0" dependencies = [ "graphite-desktop", + "graphite-desktop-ui", ] [[package]] @@ -2253,6 +2276,33 @@ dependencies = [ "winres", ] +[[package]] +name = "graphite-desktop-ui" +version = "0.0.0" +dependencies = [ + "ash", + "bytemuck", + "cef", + "graphite-desktop-embedded-resources", + "ipc-channel", + "libc", + "mach2", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "objc2-io-surface", + "objc2-metal 0.3.2", + "rand", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", + "wgpu", + "wgpu-sync", + "windows 0.62.2", + "winit", +] + [[package]] name = "graphite-desktop-wrapper" version = "0.1.0" @@ -2916,6 +2966,25 @@ dependencies = [ "libc", ] +[[package]] +name = "ipc-channel" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "347ef783e380784658248bdb0b87ca1293e3e3df3fc7dee061e129aa5f013d61" +dependencies = [ + "crossbeam-channel", + "libc", + "mio", + "postcard", + "rand", + "rustc-hash 2.1.1", + "serde_core", + "tempfile", + "thiserror 2.0.18", + "uuid", + "windows 0.61.3", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -3249,6 +3318,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "markup5ever" version = "0.36.1" @@ -3783,7 +3861,6 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.11.0", "block2 0.6.2", - "libc", "objc2 0.6.3", "objc2-core-foundation", ] @@ -3950,7 +4027,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.61.2", ] [[package]] @@ -4330,6 +4407,18 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.3" @@ -4510,7 +4599,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4528,6 +4617,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.9.2" @@ -4986,7 +5081,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5054,7 +5149,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5787,7 +5882,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6457,6 +6552,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -7003,7 +7109,7 @@ dependencies = [ "web-sys", "wgpu-naga-bridge", "wgpu-types", - "windows", + "windows 0.62.2", "windows-core 0.62.2", "windows-result 0.4.1", ] @@ -7067,7 +7173,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -7090,16 +7196,38 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + [[package]] name = "windows" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-collections", + "windows-collections 0.3.2", "windows-core 0.62.2", - "windows-future", - "windows-numerics", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", ] [[package]] @@ -7137,6 +7265,17 @@ dependencies = [ "windows-strings 0.5.1", ] +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + [[package]] name = "windows-future" version = "0.3.2" @@ -7145,7 +7284,7 @@ checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ "windows-core 0.62.2", "windows-link 0.2.1", - "windows-threading", + "windows-threading 0.2.1", ] [[package]] @@ -7182,6 +7321,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-numerics" version = "0.3.1" @@ -7332,6 +7481,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-threading" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 98ef2d9eee..d433170e12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "desktop/platform/linux", "desktop/platform/mac", "desktop/platform/win", + "desktop/ui", "document/graph-storage", "document/container", "document/document-format", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index d7f3112b87..b68f76ac76 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -15,14 +15,14 @@ path = "src/main.rs" [features] default = ["recommended", "embedded_resources"] recommended = ["gpu", "accelerated_paint"] -embedded_resources = ["dep:graphite-desktop-embedded-resources"] +embedded_resources = ["graphite-desktop-ui/embedded_resources"] gpu = ["graphite-desktop-wrapper/gpu"] -accelerated_paint = ["cef/accelerated_osr"] +accelerated_paint = ["graphite-desktop-ui/accelerated_paint"] [dependencies] # Local dependencies graphite-desktop-wrapper = { path = "wrapper" } -graphite-desktop-embedded-resources = { path = "embedded-resources", optional = true } +graphite-desktop-ui = { path = "ui" } wgpu = { workspace = true } winit = { workspace = true, features = [ @@ -32,8 +32,6 @@ winit = { workspace = true, features = [ thiserror = { workspace = true } futures = { workspace = true } tokio = { workspace = true } -cef = { workspace = true } -cef-dll-sys = { workspace = true } tracing-subscriber = { workspace = true } tracing = { workspace = true } dirs = { workspace = true } diff --git a/desktop/bundle/src/common.rs b/desktop/bundle/src/common.rs index 88e16ff2a7..0cafa8be9e 100644 --- a/desktop/bundle/src/common.rs +++ b/desktop/bundle/src/common.rs @@ -28,12 +28,16 @@ pub(crate) fn cef_path() -> PathBuf { PathBuf::from(env!("CEF_PATH")) } -pub(crate) fn build_bin(package: &str, bin: Option<&str>) -> Result> { +pub(crate) fn build_bin(package: &str, bin: Option<&str>, features: Option<&str>) -> Result> { let mut args = vec!["build", "--package", package, "--profile", profile_name()]; if let Some(bin) = bin { args.push("--bin"); args.push(bin); } + if let Some(features) = features { + args.push("--features"); + args.push(features); + } run_command("cargo", &args)?; let profile_path = profile_path(); let mut bin_path = if let Some(bin) = bin { profile_path.join(bin) } else { profile_path.join(APP_BIN) }; diff --git a/desktop/bundle/src/linux.rs b/desktop/bundle/src/linux.rs index f1511d8b02..badb7d7ec9 100644 --- a/desktop/bundle/src/linux.rs +++ b/desktop/bundle/src/linux.rs @@ -1,7 +1,7 @@ use crate::common::*; pub fn main() -> Result<(), Box> { - let app_bin = build_bin("graphite-desktop-platform-linux", None)?; + let app_bin = build_bin("graphite-desktop-platform-linux", None, None)?; // TODO: Implement bundling for linux diff --git a/desktop/bundle/src/mac.rs b/desktop/bundle/src/mac.rs index 38f8e866eb..4d7a096e09 100644 --- a/desktop/bundle/src/mac.rs +++ b/desktop/bundle/src/mac.rs @@ -18,8 +18,8 @@ const GRAPHITE_FILE_EXTENSION: &str = "graphite"; const GRAPHITE_MIME_TYPE: &str = "application/graphite+json"; pub fn main() -> Result<(), Box> { - let app_bin = build_bin("graphite-desktop-platform-mac", None)?; - let helper_bin = build_bin("graphite-desktop-platform-mac", Some("helper"))?; + let app_bin = build_bin("graphite-desktop-platform-mac", None, Some("main"))?; + let helper_bin = build_bin("graphite-desktop-platform-mac", Some("helper"), Some("helper"))?; let profile_path = profile_path(); let app_dir = bundle(&profile_path, &app_bin, &helper_bin); diff --git a/desktop/bundle/src/win.rs b/desktop/bundle/src/win.rs index f673124cf8..f3274636f3 100644 --- a/desktop/bundle/src/win.rs +++ b/desktop/bundle/src/win.rs @@ -7,7 +7,7 @@ use crate::common::*; const EXECUTABLE: &str = "Graphite.exe"; pub fn main() -> Result<(), Box> { - let app_bin = build_bin("graphite-desktop-platform-win", None)?; + let app_bin = build_bin("graphite-desktop-platform-win", None, None)?; let executable = bundle(&profile_path(), &app_bin); diff --git a/desktop/platform/linux/src/main.rs b/desktop/platform/linux/src/main.rs index 5420d97bbe..6c5edc1a0c 100644 --- a/desktop/platform/linux/src/main.rs +++ b/desktop/platform/linux/src/main.rs @@ -1,3 +1,3 @@ -fn main() { - graphite_desktop::start(); +fn main() -> std::process::ExitCode { + graphite_desktop::start() } diff --git a/desktop/platform/mac/Cargo.toml b/desktop/platform/mac/Cargo.toml index 06ea413129..9566c9ba75 100644 --- a/desktop/platform/mac/Cargo.toml +++ b/desktop/platform/mac/Cargo.toml @@ -8,13 +8,20 @@ repository = "" edition = "2024" rust-version = "1.87" +[features] +main = ["dep:graphite-desktop"] +helper = ["dep:graphite-desktop-ui"] + [[bin]] name = "graphite" path = "src/main.rs" +required-features = ["main"] [[bin]] name = "helper" path = "src/helper.rs" +required-features = ["helper"] [dependencies] -graphite-desktop = { path = "../.." } +graphite-desktop = { path = "../..", optional = true } +graphite-desktop-ui = { path = "../../ui", optional = true } diff --git a/desktop/platform/mac/src/helper.rs b/desktop/platform/mac/src/helper.rs index 149bb79e84..ff80dd540d 100644 --- a/desktop/platform/mac/src/helper.rs +++ b/desktop/platform/mac/src/helper.rs @@ -1,3 +1,3 @@ -fn main() { - graphite_desktop::start_helper(); +fn main() -> std::process::ExitCode { + graphite_desktop_ui::run_helper() } diff --git a/desktop/platform/mac/src/main.rs b/desktop/platform/mac/src/main.rs index 5420d97bbe..6c5edc1a0c 100644 --- a/desktop/platform/mac/src/main.rs +++ b/desktop/platform/mac/src/main.rs @@ -1,3 +1,3 @@ -fn main() { - graphite_desktop::start(); +fn main() -> std::process::ExitCode { + graphite_desktop::start() } diff --git a/desktop/platform/win/src/main.rs b/desktop/platform/win/src/main.rs index 2864480316..693c81eecd 100644 --- a/desktop/platform/win/src/main.rs +++ b/desktop/platform/win/src/main.rs @@ -1,4 +1,4 @@ #![windows_subsystem = "windows"] -fn main() { - graphite_desktop::start(); +fn main() -> std::process::ExitCode { + graphite_desktop::start() } diff --git a/desktop/src/app.rs b/desktop/src/app.rs index ddd16dfa92..37e66ecdb8 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -5,7 +5,7 @@ use std::io::Read; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc::{Receiver, Sender, SyncSender}; +use std::sync::mpsc::{Receiver, SyncSender}; use std::thread; use std::time::{Duration, Instant}; use winit::application::ApplicationHandler; @@ -14,8 +14,8 @@ use winit::event::{ButtonSource, ElementState, MouseButton, StartCause, WindowEv use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::window::WindowId; -use crate::cef; -use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS; +use graphite_desktop_ui::{UiCommand, UiInstance}; + use crate::dirs; use crate::event::{AppEvent, AppEventScheduler}; use crate::persist; @@ -40,10 +40,8 @@ pub(crate) struct App { app_event_receiver: Receiver, app_event_scheduler: AppEventScheduler, desktop_wrapper: DesktopWrapper, - cef_context: Box, - cef_schedule: Option, - cef_view_info_sender: Sender, - cef_init_successful: bool, + ui: UiInstance, + ui_frame_received: bool, start_render_sender: SyncSender<()>, web_communication_initialized: bool, web_communication_startup_buffer: Vec>, @@ -59,10 +57,8 @@ impl App { Window::init(); } - #[allow(clippy::too_many_arguments)] pub(crate) fn new( - cef_context: Box, - cef_view_info_sender: Sender, + ui: UiInstance, wgpu_context: WgpuContext, app_event_receiver: Receiver, app_event_scheduler: AppEventScheduler, @@ -117,10 +113,8 @@ impl App { app_event_receiver, app_event_scheduler, desktop_wrapper, - cef_context, - cef_schedule: Some(Instant::now()), - cef_view_info_sender, - cef_init_successful: false, + ui, + ui_frame_received: false, start_render_sender, web_communication_initialized: false, web_communication_startup_buffer: Vec::new(), @@ -177,16 +171,16 @@ impl App { } if is_new_size { - let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Size { + self.ui.send(UiCommand::Resized { width: size.width, height: size.height, }); } if is_new_scale { - let _ = self.cef_view_info_sender.send(cef::ViewInfoUpdate::Scale(scale)); + self.ui.send(UiCommand::ScaleChanged(scale)); } - self.cef_context.notify_view_info_changed(); + self.ui.send(UiCommand::Refresh); if let Some(render_state) = &mut self.render_state { render_state.resize(size.width, size.height); @@ -430,7 +424,7 @@ impl App { fn send_or_queue_web_message(&mut self, message: Vec) { if self.web_communication_initialized { - self.cef_context.send_web_message(message); + self.ui.send(UiCommand::Message(message)); } else { self.web_communication_startup_buffer.push(message); } @@ -441,7 +435,7 @@ impl App { AppEvent::WebCommunicationInitialized => { self.web_communication_initialized = true; for message in self.web_communication_startup_buffer.drain(..) { - self.cef_context.send_web_message(message); + self.ui.send(UiCommand::Message(message)); } } AppEvent::DesktopWrapperMessage(message) => self.dispatch_desktop_wrapper_message(message), @@ -465,15 +459,8 @@ impl App { if let Some(window) = &self.window { window.request_redraw(); } - if !self.cef_init_successful { - self.cef_init_successful = true; - } - } - AppEvent::ScheduleBrowserWork(instant) => { - if instant <= Instant::now() { - self.cef_context.work(); - } else { - self.cef_schedule = Some(instant); + if !self.ui_frame_received { + self.ui_frame_received = true; } } AppEvent::CursorChange(cursor) => { @@ -485,6 +472,10 @@ impl App { tracing::info!("Exiting main event loop"); event_loop.exit(); } + AppEvent::UiCrashed => { + tracing::error!("UI process crashed, exiting."); + self.exit(Some(ExitReason::Shutdown)); + } AppEvent::OpenFiles(paths) => { // Accumulate launch documents until OpenLaunchDocuments message is received if let Some(launch_documents) = &mut self.launch_documents { @@ -556,15 +547,15 @@ impl ApplicationHandler for App { if let Some(window) = &self.window { window.end_pointer_lock(); } - self.cef_context.handle_window_event(&WindowEvent::PointerMoved { + self.ui.send(UiCommand::Input(WindowEvent::PointerMoved { device_id: None, position: pointer_lock_position, primary: true, source: winit::event::PointerSource::Mouse, - }); + })); } - self.cef_context.handle_window_event(&event); + self.ui.send(UiCommand::Input(event.clone())); match event { WindowEvent::CloseRequested => { @@ -586,7 +577,7 @@ impl ApplicationHandler for App { match render_state.render(window) { Ok(_) => {} Err(RenderError::OutdatedUITextureError) => { - self.cef_context.notify_view_info_changed(); + self.ui.send(UiCommand::Refresh); } Err(RenderError::SurfaceLost) => { tracing::warn!("lost surface"); @@ -596,7 +587,7 @@ impl ApplicationHandler for App { let _ = self.start_render_sender.try_send(()); } - if !self.cef_init_successful + if !self.ui_frame_received && !self.preferences.disable_ui_acceleration && self.web_communication_initialized && let Some(startup_time) = self.startup_time @@ -670,9 +661,6 @@ impl ApplicationHandler for App { _ => {} } - - // Notify cef of possible input events - self.cef_context.work(); } fn device_event(&mut self, _event_loop: &dyn ActiveEventLoop, _device_id: Option, event: winit::event::DeviceEvent) { @@ -693,20 +681,7 @@ impl ApplicationHandler for App { } fn about_to_wait(&mut self, event_loop: &dyn ActiveEventLoop) { - // Set a timeout in case we miss any cef schedule requests - let mut wait_until = Instant::now() + Duration::from_millis(10); - if let Some(schedule) = self.cef_schedule - && schedule < Instant::now() - { - self.cef_schedule = None; - // Poll cef message loop multiple times to avoid message loop starvation - for _ in 0..CEF_MESSAGE_LOOP_MAX_ITERATIONS { - self.cef_context.work(); - } - } else if let Some(cef_schedule) = self.cef_schedule { - wait_until = wait_until.min(cef_schedule); - } - event_loop.set_control_flow(ControlFlow::WaitUntil(wait_until)); + event_loop.set_control_flow(ControlFlow::WaitUntil(Instant::now() + Duration::from_millis(10))); } } diff --git a/desktop/src/cef.rs b/desktop/src/cef.rs deleted file mode 100644 index c96711fd1d..0000000000 --- a/desktop/src/cef.rs +++ /dev/null @@ -1,245 +0,0 @@ -//! CEF (Chromium Embedded Framework) integration for Graphite Desktop -//! -//! This module provides CEF browser integration with hardware-accelerated texture sharing. -//! -//! # Hardware Acceleration -//! -//! The texture import system supports platform-specific hardware acceleration: -//! -//! - **Linux**: DMA-BUF via Vulkan external memory (`accelerated_paint_dmabuf` feature) -//! - **Windows**: D3D11 shared textures via either Vulkan or D3D12 interop (`accelerated_paint_d3d11` feature) -//! - **macOS**: IOSurface via Metal/Vulkan interop (`accelerated_paint_iosurface` feature) -//! -//! The system gracefully falls back to CPU textures when hardware acceleration is unavailable. - -use std::fs::File; -use std::io; -use std::io::Read; -use std::path::PathBuf; -use std::sync::mpsc::Receiver; -use std::sync::{Arc, Mutex}; -use std::time::Instant; - -use crate::event::{AppEvent, AppEventScheduler}; -use crate::window::Cursor; -use crate::wrapper::deserialize_editor_message; - -mod consts; -mod context; -mod input; -mod internal; -mod ipc; -mod platform; -mod utility; -mod view; - -pub(crate) use context::{CefContext, CefContextBuilder, InitError}; -pub(crate) use view::View; - -pub(crate) trait CefEventHandler: Send + Sync + 'static { - fn view_info(&self) -> ViewInfo; - fn draw(&self, view: &View); - fn load_resource(&self, path: PathBuf) -> Option; - fn cursor_change(&self, cursor: Cursor); - /// Schedule the main event loop to run the CEF event loop after the timeout. - /// See [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation. - fn schedule_cef_message_loop_work(&self, scheduled_time: Instant); - fn initialized_web_communication(&self); - fn receive_web_message(&self, message: &[u8]); - fn duplicate(&self) -> Self - where - Self: Sized; -} - -#[derive(Clone, Copy)] -pub(crate) struct ViewInfo { - width: u32, - height: u32, - scale: f64, -} -impl ViewInfo { - pub(crate) fn new() -> Self { - Self { width: 1, height: 1, scale: 1. } - } - pub(crate) fn apply_update(&mut self, update: ViewInfoUpdate) { - match update { - ViewInfoUpdate::Size { width, height } if width > 0 && height > 0 => { - self.width = width; - self.height = height; - } - ViewInfoUpdate::Scale(scale) if scale > 0. => { - self.scale = scale; - } - _ => {} - } - } - pub(crate) fn zoom(&self) -> f64 { - self.scale.ln() / 1.2_f64.ln() - } - pub(crate) fn width(&self) -> u32 { - self.width - } - pub(crate) fn height(&self) -> u32 { - self.height - } -} -impl Default for ViewInfo { - fn default() -> Self { - Self::new() - } -} - -pub(crate) enum ViewInfoUpdate { - Size { width: u32, height: u32 }, - Scale(f64), -} - -#[derive(Clone)] -pub(crate) struct Resource { - pub(crate) reader: ResourceReader, - pub(crate) mimetype: Option, -} - -#[expect(dead_code)] -#[derive(Clone)] -pub(crate) enum ResourceReader { - Embedded(io::Cursor<&'static [u8]>), - File(Arc), -} -impl Read for ResourceReader { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - match self { - ResourceReader::Embedded(cursor) => cursor.read(buf), - ResourceReader::File(file) => file.as_ref().read(buf), - } - } -} - -pub(crate) struct CefHandler { - app_event_scheduler: AppEventScheduler, - view_info_receiver: Arc>, -} - -impl CefHandler { - pub(crate) fn new(app_event_scheduler: AppEventScheduler, view_info_receiver: Receiver) -> Self { - Self { - app_event_scheduler, - view_info_receiver: Arc::new(Mutex::new(ViewInfoReceiver::new(view_info_receiver))), - } - } -} - -impl CefEventHandler for CefHandler { - fn view_info(&self) -> ViewInfo { - let Ok(mut guard) = self.view_info_receiver.lock() else { - tracing::error!("Failed to lock view_info_receiver"); - return ViewInfo::new(); - }; - let ViewInfoReceiver { receiver, view_info } = &mut *guard; - for update in receiver.try_iter() { - view_info.apply_update(update); - } - *view_info - } - - fn draw(&self, view: &View) { - if let Some(texture) = view.texture() { - self.app_event_scheduler.schedule(AppEvent::UiUpdate(texture)); - } - } - - fn load_resource(&self, path: PathBuf) -> Option { - let path = if path.as_os_str().is_empty() { PathBuf::from("index.html") } else { path }; - - let mimetype = match path.extension().and_then(|s| s.to_str()).unwrap_or("") { - "html" => Some("text/html".to_string()), - "css" => Some("text/css".to_string()), - "txt" => Some("text/plain".to_string()), - "wasm" => Some("application/wasm".to_string()), - "js" => Some("application/javascript".to_string()), - "png" => Some("image/png".to_string()), - "jpg" | "jpeg" => Some("image/jpeg".to_string()), - "svg" => Some("image/svg+xml".to_string()), - "xml" => Some("application/xml".to_string()), - "json" => Some("application/json".to_string()), - "ico" => Some("image/x-icon".to_string()), - "woff" => Some("font/woff".to_string()), - "woff2" => Some("font/woff2".to_string()), - "ttf" => Some("font/ttf".to_string()), - "otf" => Some("font/otf".to_string()), - "webmanifest" => Some("application/manifest+json".to_string()), - "graphite" => Some("application/graphite+json".to_string()), - _ => None, - }; - - #[cfg(feature = "embedded_resources")] - { - if let Some(resources) = &graphite_desktop_embedded_resources::EMBEDDED_RESOURCES - && let Some(file) = resources.get_file(&path) - { - return Some(Resource { - reader: ResourceReader::Embedded(io::Cursor::new(file.contents())), - mimetype, - }); - } - } - - #[cfg(not(feature = "embedded_resources"))] - { - use std::path::Path; - let asset_path_env = std::env::var("GRAPHITE_RESOURCES").ok()?; - let asset_path = Path::new(&asset_path_env); - let file_path = asset_path.join(path.strip_prefix("/").unwrap_or(&path)); - if file_path.exists() && file_path.is_file() { - if let Ok(file) = std::fs::File::open(file_path) { - return Some(Resource { - reader: ResourceReader::File(file.into()), - mimetype, - }); - } - } - } - - None - } - - fn cursor_change(&self, cursor: Cursor) { - self.app_event_scheduler.schedule(AppEvent::CursorChange(cursor)); - } - - fn schedule_cef_message_loop_work(&self, scheduled_time: std::time::Instant) { - self.app_event_scheduler.schedule(AppEvent::ScheduleBrowserWork(scheduled_time)); - } - - fn initialized_web_communication(&self) { - self.app_event_scheduler.schedule(AppEvent::WebCommunicationInitialized); - } - - fn receive_web_message(&self, message: &[u8]) { - let Some(desktop_wrapper_message) = deserialize_editor_message(message) else { - tracing::error!("Failed to deserialize web message"); - return; - }; - self.app_event_scheduler.schedule(AppEvent::DesktopWrapperMessage(desktop_wrapper_message)); - } - - fn duplicate(&self) -> Self - where - Self: Sized, - { - Self { - app_event_scheduler: self.app_event_scheduler.clone(), - view_info_receiver: self.view_info_receiver.clone(), - } - } -} - -struct ViewInfoReceiver { - view_info: ViewInfo, - receiver: Receiver, -} -impl ViewInfoReceiver { - fn new(receiver: Receiver) -> Self { - Self { view_info: ViewInfo::new(), receiver } - } -} diff --git a/desktop/src/cef/context.rs b/desktop/src/cef/context.rs deleted file mode 100644 index 53eac0aeef..0000000000 --- a/desktop/src/cef/context.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[cfg(not(target_os = "macos"))] -mod multithreaded; -mod singlethreaded; - -mod builder; -pub(crate) use builder::{CefContextBuilder, InitError}; - -pub(crate) trait CefContext { - fn work(&mut self); - - fn handle_window_event(&mut self, event: &winit::event::WindowEvent); - - fn notify_view_info_changed(&self); - - fn send_web_message(&self, message: Vec); -} diff --git a/desktop/src/cef/context/builder.rs b/desktop/src/cef/context/builder.rs deleted file mode 100644 index 50cb1e0a21..0000000000 --- a/desktop/src/cef/context/builder.rs +++ /dev/null @@ -1,221 +0,0 @@ -use cef::args::Args; -use cef::sys::{CEF_API_VERSION_LAST, cef_log_severity_t}; -use cef::{ - App, BrowserSettings, CefString, Client, DictionaryValue, ImplCommandLine, ImplRequestContext, LogSeverity, RequestContextSettings, SchemeHandlerFactory, Settings, WindowInfo, api_hash, - browser_host_create_browser_sync, execute_process, -}; -use std::path::Path; - -use super::CefContext; -use super::singlethreaded::SingleThreadedCefContext; -use crate::cef::CefEventHandler; -use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME}; -use crate::cef::input::InputState; -use crate::cef::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderProcessAppImpl, SchemeHandlerFactoryImpl}; -use crate::dirs::TempDir; -use crate::wrapper::WgpuContext; - -pub(crate) struct CefContextBuilder { - pub(crate) args: Args, - pub(crate) is_sub_process: bool, - _marker: std::marker::PhantomData, -} - -unsafe impl Send for CefContextBuilder {} - -impl CefContextBuilder { - pub(crate) fn new() -> Self { - Self::new_impl(false) - } - pub(crate) fn new_helper() -> Self { - Self::new_impl(true) - } - - fn new_impl(helper: bool) -> Self { - #[cfg(target_os = "macos")] - let _loader = { - let loader = cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), helper); - assert!(loader.load()); - loader - }; - #[cfg(not(target_os = "macos"))] - let _ = helper; - - let _ = api_hash(CEF_API_VERSION_LAST, 0); - let args = Args::new(); - let is_sub_process = args.as_cmd_line().unwrap().has_switch(Some(&"type".into())) == 1; - Self { - args, - is_sub_process, - _marker: std::marker::PhantomData, - } - } - - pub(crate) fn is_sub_process(&self) -> bool { - self.is_sub_process - } - - pub(crate) fn execute_sub_process(&self) -> SetupError { - let cmd = self.args.as_cmd_line().unwrap(); - let process_type = CefString::from(&cmd.switch_value(Some(&"type".into()))); - let mut app = RenderProcessAppImpl::::app(); - let ret = execute_process(Some(self.args.as_main_args()), Some(&mut app), std::ptr::null_mut()); - if ret >= 0 { - SetupError::SubprocessFailed(process_type.to_string()) - } else { - SetupError::Subprocess - } - } - - #[cfg(target_os = "macos")] - pub(crate) fn create(self, event_handler: H, wgpu_context: WgpuContext, disable_gpu_acceleration: bool) -> Result { - let instance_dir = TempDir::new().expect("Failed to create temporary directory for CEF instance"); - let accelerated_paint = accelerated_paint(disable_gpu_acceleration); - self.build_inner(&event_handler, instance_dir.as_ref(), accelerated_paint)?; - create_browser(event_handler, wgpu_context, instance_dir, accelerated_paint) - } - - #[cfg(not(target_os = "macos"))] - pub(crate) fn create(self, event_handler: H, wgpu_context: WgpuContext, disable_gpu_acceleration: bool) -> Result { - let instance_dir = TempDir::new().expect("Failed to create temporary directory for CEF instance"); - let accelerated_paint = accelerated_paint(disable_gpu_acceleration); - self.build_inner(&event_handler, instance_dir.as_ref(), accelerated_paint)?; - super::multithreaded::run_on_ui_thread(move || match create_browser(event_handler, wgpu_context, instance_dir, accelerated_paint) { - Ok(context) => super::multithreaded::CONTEXT.with(|b| *b.borrow_mut() = Some(context)), - Err(e) => panic!("Failed to initialize CEF context: {:?}", e), - }); - Ok(super::multithreaded::MultiThreadedCefContextProxy) - } - - fn build_inner(self, event_handler: &H, instance_dir: &Path, accelerated_paint: bool) -> Result<(), InitError> { - let mut cef_app = App::new(BrowserProcessAppImpl::new(event_handler.duplicate(), accelerated_paint)); - let result = cef::initialize(Some(self.args.as_main_args()), Some(&platform_settings(instance_dir)), Some(&mut cef_app), std::ptr::null_mut()); - if result != 1 { - return Err(InitError::InitializationFailureCode(cef::get_exit_code() as u32)); - } - Ok(()) - } -} - -fn accelerated_paint(disable_gpu_acceleration: bool) -> bool { - #[cfg(feature = "accelerated_paint")] - { - !disable_gpu_acceleration && crate::cef::platform::should_enable_hardware_acceleration() - } - #[cfg(not(feature = "accelerated_paint"))] - { - let _ = disable_gpu_acceleration; - false - } -} - -fn platform_settings(instance_dir: &Path) -> Settings { - let log_severity = match std::env::var("GRAPHITE_BROWSER_LOG").unwrap_or_default().to_lowercase().as_str() { - "debug" => cef_log_severity_t::LOGSEVERITY_VERBOSE, - "info" => cef_log_severity_t::LOGSEVERITY_INFO, - "warn" => cef_log_severity_t::LOGSEVERITY_WARNING, - "error" => cef_log_severity_t::LOGSEVERITY_ERROR, - "none" => cef_log_severity_t::LOGSEVERITY_DISABLE, - _ => cef_log_severity_t::LOGSEVERITY_FATAL, - }; - - let base = Settings { - windowless_rendering_enabled: 1, - root_cache_path: instance_dir.to_str().map(CefString::from).unwrap(), - cache_path: "".into(), - disable_signal_handlers: 1, - log_severity: LogSeverity::from(log_severity), - ..Default::default() - }; - - #[cfg(target_os = "macos")] - { - let exe = std::env::current_exe().expect("cannot get current exe path"); - let app_root = exe.parent().and_then(|p| p.parent()).expect("bad path structure").parent().expect("bad path structure"); - Settings { - main_bundle_path: app_root.to_str().map(CefString::from).unwrap(), - multi_threaded_message_loop: 0, - external_message_pump: 1, - no_sandbox: 1, // GPU helper crashes when running with sandbox - ..base - } - } - - #[cfg(not(target_os = "macos"))] - Settings { - multi_threaded_message_loop: 1, - #[cfg(target_os = "linux")] - no_sandbox: 1, - ..base - } -} - -fn create_browser(event_handler: H, wgpu_context: WgpuContext, instance_dir: TempDir, accelerated_paint: bool) -> Result { - let mut client = Client::new(BrowserProcessClientImpl::new(&event_handler, wgpu_context)); - - let window_info = WindowInfo { - windowless_rendering_enabled: 1, - #[cfg(feature = "accelerated_paint")] - shared_texture_enabled: accelerated_paint as i32, - ..Default::default() - }; - - let settings = BrowserSettings { - windowless_frame_rate: crate::consts::CEF_WINDOWLESS_FRAME_RATE, - background_color: 0x0, - ..Default::default() - }; - - let Some(mut incognito_request_context) = cef::request_context_create_context( - Some(&RequestContextSettings { - persist_session_cookies: 0, - cache_path: "".into(), - ..Default::default() - }), - Option::<&mut cef::RequestContextHandler>::None, - ) else { - return Err(InitError::RequestContextCreationFailed); - }; - - let mut scheme_handler_factory = SchemeHandlerFactory::new(SchemeHandlerFactoryImpl::new(event_handler.duplicate())); - incognito_request_context.clear_scheme_handler_factories(); - incognito_request_context.register_scheme_handler_factory(Some(&RESOURCE_SCHEME.into()), Some(&RESOURCE_DOMAIN.into()), Some(&mut scheme_handler_factory)); - - let url = format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/"); - browser_host_create_browser_sync( - Some(&window_info), - Some(&mut client), - Some(&url.as_str().into()), - Some(&settings), - Option::<&mut DictionaryValue>::None, - Some(&mut incognito_request_context), - ) - .map(|browser| SingleThreadedCefContext { - event_handler: Box::new(event_handler), - browser, - input_state: InputState::default(), - _instance_dir: instance_dir, - }) - .ok_or_else(|| { - tracing::error!("Failed to create browser"); - InitError::BrowserCreationFailed - }) -} - -#[derive(thiserror::Error, Debug)] -pub(crate) enum SetupError { - #[error("This is the sub process should exit immediately")] - Subprocess, - #[error("Subprocess returned non zero exit code: {0}")] - SubprocessFailed(String), -} - -#[derive(thiserror::Error, Debug)] -pub(crate) enum InitError { - #[error("Initialization failed with code: {0}")] - InitializationFailureCode(u32), - #[error("Browser creation failed")] - BrowserCreationFailed, - #[error("Request context creation failed")] - RequestContextCreationFailed, -} diff --git a/desktop/src/cef/context/multithreaded.rs b/desktop/src/cef/context/multithreaded.rs deleted file mode 100644 index 832cfd2f2f..0000000000 --- a/desktop/src/cef/context/multithreaded.rs +++ /dev/null @@ -1,73 +0,0 @@ -use cef::sys::cef_thread_id_t; -use cef::{Task, ThreadId, post_task}; -use std::cell::RefCell; -use winit::event::WindowEvent; - -use crate::cef::internal::task::ClosureTask; - -use super::CefContext; -use super::singlethreaded::SingleThreadedCefContext; - -thread_local! { - pub(super) static CONTEXT: RefCell> = const { RefCell::new(None) }; -} - -pub(super) struct MultiThreadedCefContextProxy; - -impl CefContext for MultiThreadedCefContextProxy { - fn work(&mut self) { - // CEF handles its own message loop in multi-threaded mode - } - - fn handle_window_event(&mut self, event: &WindowEvent) { - let event_clone = event.clone(); - run_on_ui_thread(move || { - CONTEXT.with(|b| { - if let Some(context) = b.borrow_mut().as_mut() { - context.handle_window_event(&event_clone); - } - }); - }); - } - - fn notify_view_info_changed(&self) { - run_on_ui_thread(move || { - CONTEXT.with(|b| { - if let Some(context) = b.borrow_mut().as_mut() { - context.notify_view_info_changed(); - } - }); - }); - } - - fn send_web_message(&self, message: Vec) { - run_on_ui_thread(move || { - CONTEXT.with(|b| { - if let Some(context) = b.borrow_mut().as_mut() { - context.send_web_message(message); - } - }); - }); - } -} - -impl Drop for MultiThreadedCefContextProxy { - fn drop(&mut self) { - // Force dropping underlying context on the UI thread - let (sync_drop_tx, sync_drop_rx) = std::sync::mpsc::channel(); - run_on_ui_thread(move || { - drop(CONTEXT.take()); - let _ = sync_drop_tx.send(()); - }); - let _ = sync_drop_rx.recv(); - } -} - -pub(super) fn run_on_ui_thread(closure: F) -where - F: FnOnce() + Send + 'static, -{ - let closure_task = ClosureTask::new(closure); - let mut task = Task::new(closure_task); - post_task(ThreadId::from(cef_thread_id_t::TID_UI), Some(&mut task)); -} diff --git a/desktop/src/cef/context/singlethreaded.rs b/desktop/src/cef/context/singlethreaded.rs deleted file mode 100644 index fe4e01a8d2..0000000000 --- a/desktop/src/cef/context/singlethreaded.rs +++ /dev/null @@ -1,61 +0,0 @@ -use cef::{Browser, ImplBrowser, ImplBrowserHost}; -use winit::event::WindowEvent; - -use crate::cef::input::InputState; -use crate::cef::ipc::{MessageType, SendMessage}; -use crate::cef::{CefEventHandler, input}; -use crate::dirs::TempDir; - -use super::CefContext; - -pub(super) struct SingleThreadedCefContext { - pub(super) event_handler: Box, - pub(super) browser: Browser, - pub(super) input_state: InputState, - pub(super) _instance_dir: TempDir, -} - -impl CefContext for SingleThreadedCefContext { - fn work(&mut self) { - cef::do_message_loop_work(); - } - - fn handle_window_event(&mut self, event: &WindowEvent) { - input::handle_window_event(&self.browser, &mut self.input_state, event); - } - - fn notify_view_info_changed(&self) { - let view_info = self.event_handler.view_info(); - let host = self.browser.host().unwrap(); - host.set_zoom_level(view_info.zoom()); - host.was_resized(); - - // Fix for CEF not updating the view after resize - // TODO: remove once https://github.com/chromiumembedded/cef/issues/3822 is fixed - host.invalidate(cef::PaintElementType::default()); - } - - fn send_web_message(&self, message: Vec) { - self.send_message(MessageType::SendToJS, &message); - } -} - -impl Drop for SingleThreadedCefContext { - fn drop(&mut self) { - tracing::debug!("Shutting down CEF"); - - // CEF wants us to close the browser before shutting down, otherwise it may run longer that necessary. - self.browser.host().unwrap().close_browser(1); - cef::shutdown(); - } -} - -impl SendMessage for SingleThreadedCefContext { - fn send_message(&self, message_type: MessageType, message: &[u8]) { - let Some(frame) = self.browser.main_frame() else { - tracing::error!("Main frame is not available, cannot send message"); - return; - }; - frame.send_message(message_type, message); - } -} diff --git a/desktop/src/cef/input.rs b/desktop/src/cef/input.rs deleted file mode 100644 index a13d8fa23e..0000000000 --- a/desktop/src/cef/input.rs +++ /dev/null @@ -1,149 +0,0 @@ -use cef::sys::{cef_key_event_type_t, cef_mouse_button_type_t}; -use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, MouseEvent}; -use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, WindowEvent}; - -mod keymap; -use keymap::{ToCharRepresentation, ToNativeKeycode, ToVKBits}; - -mod state; -pub(crate) use state::{CefModifiers, InputState}; - -use super::consts::{PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y}; - -pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputState, event: &WindowEvent) { - match event { - WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerEntered { position, .. } => { - if !input_state.cursor_move(position) { - return; - } - - let Some(host) = browser.host() else { return }; - host.send_mouse_move_event(Some(&input_state.into()), 0); - } - WindowEvent::PointerLeft { position, .. } => { - if let Some(position) = position { - let _ = input_state.cursor_move(position); - } - - let Some(host) = browser.host() else { return }; - host.send_mouse_move_event(Some(&(input_state.into())), 1); - } - WindowEvent::PointerButton { state, button, .. } => { - let mouse_button = match button { - ButtonSource::Mouse(mouse_button) => mouse_button, - _ => { - return; // TODO: Handle touch input - } - }; - - let cef_click_count = input_state.mouse_input(mouse_button, state).into(); - let cef_mouse_up = match state { - ElementState::Pressed => 0, - ElementState::Released => 1, - }; - let cef_button = match mouse_button { - MouseButton::Left => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_LEFT), - MouseButton::Right => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_RIGHT), - MouseButton::Middle => cef::MouseButtonType::from(cef_mouse_button_type_t::MBT_MIDDLE), - _ => return, - }; - - let Some(host) = browser.host() else { return }; - host.send_mouse_click_event(Some(&input_state.into()), cef_button, cef_mouse_up, cef_click_count); - } - WindowEvent::MouseWheel { delta, phase: _, device_id: _, .. } => { - let mouse_event = input_state.into(); - let (mut delta_x, mut delta_y) = match delta { - MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_LINE_WIDTH as f32, y * SCROLL_LINE_HEIGHT as f32), - MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32), - }; - delta_x *= SCROLL_SPEED_X; - delta_y *= SCROLL_SPEED_Y; - - let Some(host) = browser.host() else { return }; - host.send_mouse_wheel_event(Some(&mouse_event), delta_x as i32, delta_y as i32); - } - WindowEvent::ModifiersChanged(modifiers) => { - input_state.modifiers_changed(&modifiers.state()); - } - WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => { - let Some(host) = browser.host() else { return }; - - input_state.modifiers_apply_key_event(&event.logical_key, &event.state); - - let mut key_event = KeyEvent { - type_: match (event.state, &event.logical_key) { - (ElementState::Pressed, winit::keyboard::Key::Character(_)) => cef_key_event_type_t::KEYEVENT_CHAR, - (ElementState::Pressed, _) => cef_key_event_type_t::KEYEVENT_RAWKEYDOWN, - (ElementState::Released, _) => cef_key_event_type_t::KEYEVENT_KEYUP, - } - .into(), - ..Default::default() - }; - - key_event.modifiers = input_state.cef_modifiers(&event.location, event.repeat).into(); - - key_event.windows_key_code = match &event.logical_key { - winit::keyboard::Key::Named(named) => named.to_vk_bits(), - winit::keyboard::Key::Character(char) => char.chars().next().unwrap_or_default().to_vk_bits(), - _ => 0, - }; - - key_event.native_key_code = event.physical_key.to_native_keycode(); - - key_event.character = event.logical_key.to_char_representation() as u16; - - if event.state == ElementState::Pressed && key_event.character != 0 { - key_event.type_ = cef_key_event_type_t::KEYEVENT_CHAR.into(); - } - - // Mitigation for CEF on Mac bug to prevent NSMenu being triggered by this key event. - // - // CEF converts the key event into an `NSEvent` internally and passes that to Chromium. - // In some cases the `NSEvent` gets to the native Cocoa application, is considered "unhandled" and can trigger menus. - // - // Why mitigation works: - // Leaving `key_event.unmodified_character = 0` still leads to CEF forwarding a "unhandled" event to the native application - // but that event is discarded because `key_event.unmodified_character = 0` is considered non-printable and not used for shortcut matching. - // - // See https://github.com/chromiumembedded/cef/issues/3857 - // - // TODO: Remove mitigation once bug is fixed or a better solution is found. - #[cfg(not(target_os = "macos"))] - { - key_event.unmodified_character = event.key_without_modifiers.to_char_representation() as u16; - } - - #[cfg(target_os = "macos")] // See https://www.magpcss.org/ceforum/viewtopic.php?start=10&t=11650 - if key_event.character == 0 && key_event.unmodified_character == 0 && event.text_with_all_modifiers.is_some() { - key_event.character = 1; - } - - if key_event.type_ == cef_key_event_type_t::KEYEVENT_CHAR.into() { - let mut key_down_event = key_event.clone(); - key_down_event.type_ = cef_key_event_type_t::KEYEVENT_RAWKEYDOWN.into(); - host.send_key_event(Some(&key_down_event)); - - key_event.windows_key_code = event.logical_key.to_char_representation() as i32; - } - - host.send_key_event(Some(&key_event)); - } - WindowEvent::PinchGesture { delta, .. } => { - if !delta.is_normal() { - return; - } - let Some(host) = browser.host() else { return }; - - let mouse_event = MouseEvent { - modifiers: CefModifiers::PINCH_MODIFIERS.into(), - ..input_state.into() - }; - - let delta = (delta * PINCH_ZOOM_SPEED).round() as i32; - - host.send_mouse_wheel_event(Some(&mouse_event), 0, delta); - } - _ => {} - } -} diff --git a/desktop/src/cef/view.rs b/desktop/src/cef/view.rs deleted file mode 100644 index fa265a75a7..0000000000 --- a/desktop/src/cef/view.rs +++ /dev/null @@ -1,108 +0,0 @@ -use cef::Rect; -use std::sync::{Arc, Mutex}; - -use crate::wrapper::WgpuContext; - -#[derive(Clone)] -pub(crate) struct View { - context: WgpuContext, - texture: Arc>>, -} - -impl View { - pub(crate) fn new(context: WgpuContext) -> Self { - Self { - context, - texture: Arc::new(Mutex::new(None)), - } - } - - pub(crate) fn texture(&self) -> Option { - let Ok(texture) = self.texture.lock() else { - tracing::error!("Failed to lock view texture"); - return None; - }; - texture.clone() - } - - pub(super) fn upload_frame_buffer(&self, buffer: &[u8], width: u32, height: u32, dirty_rects: &[Rect]) { - debug_assert_eq!(buffer.len(), width as usize * height as usize * 4); - - let Ok(mut slot) = self.texture.lock() else { - tracing::error!("Failed to lock view texture"); - return; - }; - - let needs_new_texture = slot.as_ref().is_none_or(|texture| texture.width() != width || texture.height() != height); - if needs_new_texture { - *slot = Some(self.context.device.create_texture(&wgpu::TextureDescriptor { - label: Some("CEF Texture"), - size: wgpu::Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Bgra8Unorm, - usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, - view_formats: &[], - })); - } - let texture = slot.as_ref().expect("Texture was just created"); - - let full_frame = [Rect { - x: 0, - y: 0, - width: width as i32, - height: height as i32, - }]; - let rects = if needs_new_texture || dirty_rects.is_empty() { &full_frame } else { dirty_rects }; - - for rect in rects { - let x = (rect.x.max(0) as u32).min(width); - let y = (rect.y.max(0) as u32).min(height); - let rect_width = (rect.width.max(0) as u32).min(width - x); - let rect_height = (rect.height.max(0) as u32).min(height - y); - if rect_width == 0 || rect_height == 0 { - continue; - } - self.context.queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture, - mip_level: 0, - origin: wgpu::Origin3d { x, y, z: 0 }, - aspect: wgpu::TextureAspect::All, - }, - buffer, - wgpu::TexelCopyBufferLayout { - offset: 4 * (y as u64 * width as u64 + x as u64), - bytes_per_row: Some(4 * width), - rows_per_image: None, - }, - wgpu::Extent3d { - width: rect_width, - height: rect_height, - depth_or_array_layers: 1, - }, - ); - } - } - - #[cfg(feature = "accelerated_paint")] - pub(super) fn import_shared_texture(&self, info: &cef::AcceleratedPaintInfo) { - let texture = match cef::osr_texture_import::SharedTextureHandle::new(info).import_texture(&self.context.device) { - Ok(texture) => texture, - Err(e) => { - tracing::error!("Failed to import shared texture: {e}"); - return; - } - }; - let Ok(mut slot) = self.texture.lock() else { - tracing::error!("Failed to lock view texture"); - return; - }; - *slot = Some(texture); - } -} diff --git a/desktop/src/consts.rs b/desktop/src/consts.rs index e1326402f4..b1bb55d798 100644 --- a/desktop/src/consts.rs +++ b/desktop/src/consts.rs @@ -12,7 +12,3 @@ pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron"; pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron"; pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents"; pub(crate) const APP_RESOURCES_DIRECTORY_NAME: &str = "resources"; - -// CEF configuration constants -pub(crate) const CEF_WINDOWLESS_FRAME_RATE: i32 = 60; -pub(crate) const CEF_MESSAGE_LOOP_MAX_ITERATIONS: usize = 10; diff --git a/desktop/src/dirs.rs b/desktop/src/dirs.rs index 2b8fe849d9..c6f7c80aeb 100644 --- a/desktop/src/dirs.rs +++ b/desktop/src/dirs.rs @@ -1,6 +1,5 @@ use std::fs; -use std::io; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use crate::consts::{APP_DIRECTORY_NAME, APP_DOCUMENTS_DIRECTORY_NAME, APP_RESOURCES_DIRECTORY_NAME}; @@ -10,7 +9,7 @@ pub(crate) fn ensure_dir_exists(path: &PathBuf) { } } -fn clear_dir(path: &PathBuf) { +pub(crate) fn clear_dir(path: &PathBuf) { let Ok(entries) = fs::read_dir(path) else { tracing::error!("Failed to read directory at {path:?}"); return; @@ -35,16 +34,6 @@ pub(crate) fn app_data_dir() -> PathBuf { path } -fn app_tmp_dir() -> PathBuf { - let path = std::env::temp_dir().join(APP_DIRECTORY_NAME); - ensure_dir_exists(&path); - path -} - -pub(crate) fn app_tmp_dir_cleanup() { - clear_dir(&app_tmp_dir()); -} - pub(crate) fn app_autosave_documents_dir() -> PathBuf { let path = app_data_dir().join(APP_DOCUMENTS_DIRECTORY_NAME); ensure_dir_exists(&path); @@ -57,40 +46,6 @@ pub(crate) fn app_resources_dir() -> PathBuf { path } -/// Temporary directory that is automatically deleted when dropped. -pub struct TempDir { - path: PathBuf, -} - -impl TempDir { - pub fn new() -> io::Result { - Self::new_with_parent(app_tmp_dir()) - } - - pub fn new_with_parent(parent: impl AsRef) -> io::Result { - let random_suffix = format!("{:032x}", rand::random::()); - let name = format!("{}_{}", std::process::id(), random_suffix); - let path = parent.as_ref().join(name); - fs::create_dir_all(&path)?; - Ok(Self { path }) - } -} - -impl Drop for TempDir { - fn drop(&mut self) { - let result = fs::remove_dir_all(&self.path); - if let Err(e) = result { - tracing::error!("Failed to remove temporary directory at {:?}: {}", self.path, e); - } - } -} - -impl AsRef for TempDir { - fn as_ref(&self) -> &Path { - &self.path - } -} - // TODO: Eventually remove this cleanup code for the old "browser" CEF directory pub(crate) fn delete_old_cef_browser_directory() { let old_browser_dir = crate::dirs::app_data_dir().join("browser"); diff --git a/desktop/src/event.rs b/desktop/src/event.rs index d46debfcd3..6420a76765 100644 --- a/desktop/src/event.rs +++ b/desktop/src/event.rs @@ -3,12 +3,12 @@ use crate::wrapper::messages::DesktopWrapperMessage; pub(crate) enum AppEvent { UiUpdate(wgpu::Texture), - CursorChange(crate::window::Cursor), - ScheduleBrowserWork(std::time::Instant), + CursorChange(graphite_desktop_ui::Cursor), WebCommunicationInitialized, DesktopWrapperMessage(DesktopWrapperMessage), NodeGraphExecutionResult(NodeGraphExecutionResult), Exit, + UiCrashed, OpenFiles(Vec), #[cfg(target_os = "macos")] MenuEvent { diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index 008adb4b5b..7292fa85d9 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -1,17 +1,17 @@ use crate::app::App; -use crate::cef::CefHandler; use crate::cli::Cli; use crate::consts::APP_LOCK_FILE_NAME; -use crate::event::CreateAppEventSchedulerEventLoopExt; +use crate::event::{AppEvent, CreateAppEventSchedulerEventLoopExt}; use clap::Parser; +use graphite_desktop_ui::{Acceleration, UiConfig, UiContext, UiEvent, UiSetupResult}; use std::io::Write; +use std::process::ExitCode; use tracing_subscriber::EnvFilter; use winit::event_loop::EventLoop; pub(crate) use graphite_desktop_wrapper as wrapper; mod app; -mod cef; mod cli; mod dirs; mod event; @@ -24,18 +24,17 @@ mod window; pub(crate) mod consts; -pub fn start() { +pub fn start() -> ExitCode { tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init(); - let cef_context_builder = cef::CefContextBuilder::::new(); - - if cef_context_builder.is_sub_process() { - // We are in a CEF subprocess - // This will block until the CEF subprocess quits - let error = cef_context_builder.execute_sub_process(); - tracing::warn!("Cef subprocess failed with error: {error}"); - return; - } + let ui_context = match UiContext::setup() { + UiSetupResult::Ready(context) => context, + UiSetupResult::Helper(code) => return code, + UiSetupResult::Failed => { + eprintln!("Failed to set up the UI runtime"); + return ExitCode::FAILURE; + } + }; let cli = Cli::parse(); @@ -63,13 +62,13 @@ pub fn start() { && let Err(error) = socket::send(socket::Message::OpenFiles(cli.files)) { tracing::error!("Failed to send socket message to running instance: {}", error); - std::process::exit(1); + return ExitCode::FAILURE; } - return; + return ExitCode::SUCCESS; } }; - dirs::app_tmp_dir_cleanup(); + dirs::clear_dir(&graphite_desktop_ui::temp_dir_root()); // TODO: Eventually remove this cleanup code for the old "browser" CEF directory dirs::delete_old_cef_browser_directory(); @@ -87,8 +86,6 @@ pub fn start() { let _socket_handle = socket::start(app_event_scheduler.clone()); - let (cef_view_info_sender, cef_view_info_receiver) = std::sync::mpsc::channel(); - if cli.disable_ui_acceleration { prefs.disable_ui_acceleration = true; } @@ -96,27 +93,46 @@ pub fn start() { println!("UI acceleration is disabled"); } - let cef_handler = cef::CefHandler::new(app_event_scheduler.clone(), cef_view_info_receiver); - let cef_context = match cef_context_builder.create(cef_handler, wgpu_context.clone(), prefs.disable_ui_acceleration) { - Ok(context) => { - tracing::info!("CEF initialized successfully"); - context - } - Err(cef::InitError::InitializationFailureCode(code)) => { - panic!("CEF initialization failed with code: {code}"); - } - Err(cef::InitError::BrowserCreationFailed) => { - panic!("Failed to create CEF browser"); - } - Err(cef::InitError::RequestContextCreationFailed) => { - panic!("Failed to create CEF request context"); - } - }; + let acceleration = if prefs.disable_ui_acceleration { Acceleration::Disabled } else { Acceleration::Auto }; + let ui_context = ui_context.start(UiConfig { acceleration }).unwrap_or_else(|error| panic!("Failed to start the UI runtime: {error}")); + let ui = ui_context + .instance(&wgpu_context.device, &wgpu_context.queue) + .unwrap_or_else(|error| panic!("Failed to start the UI: {error}")); + tracing::info!("UI runtime started successfully"); + + { + let ui = ui.clone(); + let scheduler = app_event_scheduler.clone(); + std::thread::Builder::new() + .name("ui-events".to_string()) + .spawn(move || { + while let Some(event) = ui.recv() { + match event { + UiEvent::Ready => scheduler.schedule(AppEvent::WebCommunicationInitialized), + UiEvent::Frame(texture) => scheduler.schedule(AppEvent::UiUpdate(texture)), + UiEvent::Cursor(cursor) => scheduler.schedule(AppEvent::CursorChange(cursor)), + UiEvent::Message(message) => match wrapper::deserialize_editor_message(&message) { + Some(message) => scheduler.schedule(AppEvent::DesktopWrapperMessage(message)), + None => tracing::error!("Failed to deserialize web message"), + }, + UiEvent::InitFailed(error) => { + tracing::error!("UI initialization failed: {error}"); + scheduler.schedule(AppEvent::UiCrashed); + } + UiEvent::Crashed => scheduler.schedule(AppEvent::UiCrashed), + } + } + }) + .expect("Failed to spawn the UI event bridge thread"); + } - let app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli.files); + let app = App::new(ui.clone(), wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli.files); let exit_reason = app.run(event_loop); + // ui needs to be shutdown before restarting + ui.shutdown(); + // If exiting due to a UI acceleration failure, update preferences to disable it for next launch if matches!(exit_reason, app::ExitReason::UiAccelerationFailure) { tracing::error!("Disabling UI acceleration"); @@ -149,10 +165,7 @@ pub fn start() { // TODO: Identify and fix the underlying CEF shutdown issue so this workaround can be removed. #[cfg(target_os = "windows")] std::process::exit(0); -} -pub fn start_helper() { - let cef_context_builder = cef::CefContextBuilder::::new_helper(); - assert!(cef_context_builder.is_sub_process()); - cef_context_builder.execute_sub_process(); + #[cfg(not(target_os = "windows"))] + ExitCode::SUCCESS } diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 5420d97bbe..6c5edc1a0c 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -1,3 +1,3 @@ -fn main() { - graphite_desktop::start(); +fn main() -> std::process::ExitCode { + graphite_desktop::start() } diff --git a/desktop/src/window.rs b/desktop/src/window.rs index 4783db16c5..703980244c 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -2,9 +2,10 @@ use crate::consts::APP_NAME; use crate::event::AppEventScheduler; use crate::wrapper::messages::MenuItem; use crate::wrapper::{WgpuInstance, WgpuSurface}; +use graphite_desktop_ui::Cursor; use std::collections::HashMap; use std::sync::Arc; -use winit::cursor::{CursorIcon, CustomCursor, CustomCursorSource}; +use winit::cursor::{CustomCursor, CustomCursorSource}; use winit::event_loop::ActiveEventLoop; use winit::monitor::Fullscreen; use winit::window::{Window as WinitWindow, WindowAttributes}; @@ -161,7 +162,17 @@ impl Window { pub(crate) fn set_cursor(&mut self, event_loop: &dyn ActiveEventLoop, cursor: Cursor) { let cursor = match cursor { Cursor::Icon(cursor_icon) => cursor_icon.into(), - Cursor::Custom(custom_cursor_source) => { + Cursor::Custom { + rgba, + width, + height, + hotspot_x, + hotspot_y, + } => { + let Ok(custom_cursor_source) = CustomCursorSource::from_rgba(rgba, width, height, hotspot_x, hotspot_y) else { + tracing::error!("Invalid custom cursor image"); + return; + }; let custom_cursor = match self.custom_cursors.get(&custom_cursor_source).cloned() { Some(cursor) => cursor, None => { @@ -222,19 +233,3 @@ impl Window { } } } - -pub(crate) enum Cursor { - Icon(CursorIcon), - Custom(CustomCursorSource), - None, -} -impl From for Cursor { - fn from(icon: CursorIcon) -> Self { - Cursor::Icon(icon) - } -} -impl From for Cursor { - fn from(custom: CustomCursorSource) -> Self { - Cursor::Custom(custom) - } -} diff --git a/desktop/ui/Cargo.toml b/desktop/ui/Cargo.toml new file mode 100644 index 0000000000..30f6805448 --- /dev/null +++ b/desktop/ui/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "graphite-desktop-ui" +description = "Renders the Graphite editor frontend UI into wgpu textures" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + + +[features] +default = [] +embedded_resources = ["dep:graphite-desktop-embedded-resources"] +accelerated_paint = ["dep:ash", "dep:bytemuck", "dep:objc2-io-surface", "dep:objc2-metal", "dep:mach2"] + +[dependencies] +# Local dependencies +graphite-desktop-embedded-resources = { path = "../embedded-resources", optional = true } + +wgpu = { workspace = true } +wgpu-sync = { workspace = true } +winit = { workspace = true, features = ["serde"] } +thiserror = { workspace = true } +tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +rand = { workspace = true, features = ["thread_rng"] } +cef = { workspace = true } +ipc-channel = "0.22" + +# Linux-specific dependencies +[target.'cfg(target_os = "linux")'.dependencies] +libc = "0.2" +ash = { version = "0.38", optional = true } +bytemuck = { workspace = true, optional = true } + +# Windows-specific dependencies +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.62.2", features = [ + "Win32_Foundation", + "Win32_Graphics_Direct3D12", + "Win32_Security", + "Win32_System_JobObjects", + "Win32_System_Threading", +] } + +# macOS-specific dependencies +[target.'cfg(target_os = "macos")'.dependencies] +libc = "0.2" +mach2 = { version = "0.4", optional = true } +objc2 = { version = "0.6.1", default-features = false } +objc2-foundation = { version = "0.3.2", default-features = false } +objc2-app-kit = { version = "0.3.2", default-features = false } +objc2-io-surface = { version = "0.3.2", optional = true } +objc2-metal = { version = "0.3", features = ["objc2-io-surface"], optional = true } diff --git a/desktop/src/cef/consts.rs b/desktop/ui/src/consts.rs similarity index 52% rename from desktop/src/cef/consts.rs rename to desktop/ui/src/consts.rs index 62b5aad288..f9500db92f 100644 --- a/desktop/src/cef/consts.rs +++ b/desktop/ui/src/consts.rs @@ -1,9 +1,21 @@ -use graphite_desktop_wrapper::DOUBLE_CLICK_MILLISECONDS; use std::time::Duration; pub(crate) const RESOURCE_SCHEME: &str = "resources"; pub(crate) const RESOURCE_DOMAIN: &str = "resources"; +pub(crate) const BROWSER_HOST_CONFIG_FLAG: &str = "--graphite-browser-host="; + +pub(crate) const WINDOWLESS_FRAME_RATE: i32 = 60; +pub(crate) const FRAMES_IN_FLIGHT_LIMIT: u64 = 3; +pub(crate) const FRAME_SEGMENT_POOL_SIZE: u64 = FRAMES_IN_FLIGHT_LIMIT + 1; // allow one extra staged frame +pub(crate) const FRAME_SEGMENT_GRANULARITY: usize = 2 * 1024 * 1024; // 2 MiB + +pub(crate) const HOST_HELLO_TIMEOUT: Duration = Duration::from_secs(5); +pub(crate) const HOST_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); + +#[cfg(target_os = "macos")] +pub(crate) const IPC_BOOTSTRAP_PREFIX: &str = "art.graphite.Graphite.ipc."; + pub(crate) const SCROLL_LINE_HEIGHT: usize = 40; pub(crate) const SCROLL_LINE_WIDTH: usize = 40; @@ -19,5 +31,5 @@ pub(crate) const SCROLL_SPEED_Y: f32 = 1.; pub(crate) const PINCH_ZOOM_SPEED: f64 = 300.; -pub(crate) const MULTICLICK_TIMEOUT: Duration = Duration::from_millis(DOUBLE_CLICK_MILLISECONDS); +pub(crate) const MULTICLICK_TIMEOUT: Duration = Duration::from_millis(500); pub(crate) const MULTICLICK_ALLOWED_TRAVEL: usize = 4; diff --git a/desktop/ui/src/context.rs b/desktop/ui/src/context.rs new file mode 100644 index 0000000000..b98085814e --- /dev/null +++ b/desktop/ui/src/context.rs @@ -0,0 +1,315 @@ +use cef::args::Args; +use cef::sys::{CEF_API_VERSION_LAST, cef_log_severity_t, cef_thread_id_t}; +use cef::{ + App, Browser, BrowserSettings, CefString, Client, DictionaryValue, ImplBrowser, ImplBrowserHost, ImplCommandLine, ImplRequestContext, LogSeverity, RequestContextSettings, SchemeHandlerFactory, + Settings, Task, ThreadId, WindowInfo, api_hash, browser_host_create_browser_sync, execute_process, post_task, +}; +use std::cell::RefCell; +use std::marker::PhantomData; +use std::path::Path; +use std::sync::mpsc::Sender; + +use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME, WINDOWLESS_FRAME_RATE}; +use crate::delegate::BrowserDelegate; +use crate::dirs::TempDir; +use crate::frames::FrameStreamer; +use crate::input::{self, InputEvent}; +use crate::internal::task::ClosureTask; +use crate::internal::{BrowserProcessAppImpl, BrowserProcessClientImpl, RenderProcessAppImpl, SchemeHandlerFactoryImpl}; +use crate::ipc::{MessageType, SendMessage}; +use crate::view::ViewInfoUpdate; + +thread_local! { + static CONTEXT: RefCell> = const { RefCell::new(None) }; +} + +pub(crate) struct CefContext { + _not_send: PhantomData<*const ()>, // impl !Send for CefContext +} + +impl CefContext { + pub(crate) fn create(delegate: BrowserDelegate, frames: FrameStreamer, view_info_sender: Sender, accelerated_paint: bool) -> Result { + let args = bootstrap(false); + #[cfg(target_os = "macos")] + crate::platform::mac::install_application(); + + let instance_dir = TempDir::new().map_err(|e| InitError::InstanceDirectoryCreationFailed(e.to_string()))?; + initialize(&args, instance_dir.as_ref(), accelerated_paint)?; + + let (created_tx, created_rx) = std::sync::mpsc::channel(); + let install_browser = move || { + let result = create_browser(delegate, frames, view_info_sender, instance_dir, accelerated_paint).map(|context| CONTEXT.with(|b| *b.borrow_mut() = Some(context))); + let _ = created_tx.send(result); + }; + #[cfg(target_os = "macos")] + install_browser(); + #[cfg(not(target_os = "macos"))] + run_on_ui_thread(install_browser); + + created_rx.recv().unwrap_or(Err(InitError::BrowserCreationFailed))?; + Ok(Self { _not_send: PhantomData }) + } + + #[cfg(not(target_os = "macos"))] + pub(crate) fn run(self, control: impl FnOnce(CefContextHandle) -> R + Send + 'static) -> R { + let result = control(CefContextHandle); + let (dropped_sender, dropped_receiver) = std::sync::mpsc::channel(); + run_on_ui_thread(move || { + drop(CONTEXT.take()); + let _ = dropped_sender.send(()); + }); + let _ = dropped_receiver.recv(); + cef::shutdown(); + result + } + + #[cfg(target_os = "macos")] + pub(crate) fn run(self, control: impl FnOnce(CefContextHandle) -> R + Send + 'static) -> R { + let (result_sender, result_receiver) = std::sync::mpsc::channel(); + let control_thread = std::thread::Builder::new() + .name("cef-host-control".to_string()) + .spawn(move || { + let result = control(CefContextHandle); + with_context(|context| { + context.browser.host().unwrap().close_browser(1); + }); + run_on_ui_thread(cef::quit_message_loop); + let _ = result_sender.send(result); + }) + .expect("Failed to spawn the CEF control thread"); + cef::run_message_loop(); + drop(CONTEXT.take()); + cef::shutdown(); + let _ = control_thread.join(); + result_receiver.recv().expect("The CEF control thread ended without a result") + } +} + +pub(crate) fn execute_helper_process() -> std::process::ExitCode { + let args = bootstrap(true); + assert_eq!(args.as_cmd_line().unwrap().has_switch(Some(&"type".into())), 1, "Not a CEF helper process"); + let mut app = RenderProcessAppImpl::app(); + let code = execute_process(Some(args.as_main_args()), Some(&mut app), std::ptr::null_mut()); + std::process::ExitCode::from(code as u8) +} + +fn bootstrap(helper: bool) -> Args { + #[cfg(target_os = "macos")] + let _loader = { + let loader = cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), helper); + assert!(loader.load()); + loader + }; + #[cfg(not(target_os = "macos"))] + let _ = helper; + + let _ = api_hash(CEF_API_VERSION_LAST, 0); + Args::new() +} + +fn initialize(args: &Args, instance_dir: &Path, accelerated_paint: bool) -> Result<(), InitError> { + let mut app = App::new(BrowserProcessAppImpl::new(accelerated_paint)); + if cef::initialize(Some(args.as_main_args()), Some(&platform_settings(instance_dir)), Some(&mut app), std::ptr::null_mut()) != 1 { + return Err(InitError::InitializationFailureCode(cef::get_exit_code() as u32)); + } + Ok(()) +} + +fn platform_settings(instance_dir: &Path) -> Settings { + let log_severity = match std::env::var("GRAPHITE_BROWSER_LOG").unwrap_or_default().to_lowercase().as_str() { + "debug" => cef_log_severity_t::LOGSEVERITY_VERBOSE, + "info" => cef_log_severity_t::LOGSEVERITY_INFO, + "warn" => cef_log_severity_t::LOGSEVERITY_WARNING, + "error" => cef_log_severity_t::LOGSEVERITY_ERROR, + "none" => cef_log_severity_t::LOGSEVERITY_DISABLE, + _ => cef_log_severity_t::LOGSEVERITY_FATAL, + }; + + let base = Settings { + windowless_rendering_enabled: 1, + root_cache_path: instance_dir.to_str().map(CefString::from).unwrap(), + cache_path: "".into(), + disable_signal_handlers: 1, + log_severity: LogSeverity::from(log_severity), + ..Default::default() + }; + + #[cfg(target_os = "macos")] + { + let exe = std::env::current_exe().expect("cannot get current exe path"); + let app_root = exe.parent().and_then(|p| p.parent()).expect("bad path structure").parent().expect("bad path structure"); + Settings { + main_bundle_path: app_root.to_str().map(CefString::from).unwrap(), + multi_threaded_message_loop: 0, + external_message_pump: 0, + no_sandbox: 1, // GPU helper crashes when running with sandbox + ..base + } + } + + #[cfg(not(target_os = "macos"))] + Settings { + multi_threaded_message_loop: 1, + #[cfg(target_os = "linux")] + no_sandbox: 1, + ..base + } +} + +fn create_browser(delegate: BrowserDelegate, frames: FrameStreamer, view_info_sender: Sender, instance_dir: TempDir, accelerated_paint: bool) -> Result { + #[cfg(not(feature = "accelerated_paint"))] + let _ = accelerated_paint; + let mut client = Client::new(BrowserProcessClientImpl::new(&delegate, frames)); + + let window_info = WindowInfo { + windowless_rendering_enabled: 1, + #[cfg(feature = "accelerated_paint")] + shared_texture_enabled: accelerated_paint as i32, + ..Default::default() + }; + + let settings = BrowserSettings { + windowless_frame_rate: WINDOWLESS_FRAME_RATE, + background_color: 0x0, + ..Default::default() + }; + + let Some(mut incognito_request_context) = cef::request_context_create_context( + Some(&RequestContextSettings { + persist_session_cookies: 0, + cache_path: "".into(), + ..Default::default() + }), + Option::<&mut cef::RequestContextHandler>::None, + ) else { + return Err(InitError::RequestContextCreationFailed); + }; + + let mut scheme_handler_factory = SchemeHandlerFactory::new(SchemeHandlerFactoryImpl::new(delegate.clone())); + incognito_request_context.clear_scheme_handler_factories(); + incognito_request_context.register_scheme_handler_factory(Some(&RESOURCE_SCHEME.into()), Some(&RESOURCE_DOMAIN.into()), Some(&mut scheme_handler_factory)); + + let url = format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/"); + browser_host_create_browser_sync( + Some(&window_info), + Some(&mut client), + Some(&url.as_str().into()), + Some(&settings), + Option::<&mut DictionaryValue>::None, + Some(&mut incognito_request_context), + ) + .map(|browser| BrowserContext { + delegate, + browser, + view_info_sender, + _instance_dir: instance_dir, + }) + .ok_or_else(|| { + tracing::error!("Failed to create browser"); + InitError::BrowserCreationFailed + }) +} + +#[derive(thiserror::Error, Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) enum InitError { + #[error("Failed to create the instance directory: {0}")] + InstanceDirectoryCreationFailed(String), + #[error("Initialization failed with code: {0}")] + InitializationFailureCode(u32), + #[error("Browser creation failed")] + BrowserCreationFailed, + #[error("Request context creation failed")] + RequestContextCreationFailed, +} + +#[derive(Clone)] +pub(crate) struct CefContextHandle; + +impl CefContextHandle { + pub(crate) fn apply_input(&self, events: Vec) { + with_context(move |context| { + for event in &events { + input::apply(&context.browser, event); + } + }); + } + + pub(crate) fn update_view_info(&self, update: ViewInfoUpdate) { + with_context(move |context| context.update_view_info(update)); + } + + pub(crate) fn refresh_view_info(&self) { + with_context(|context| context.refresh_view_info()); + } + + pub(crate) fn send_web_message(&self, message: Vec) { + with_context(move |context| context.send_web_message(message)); + } +} + +struct BrowserContext { + delegate: BrowserDelegate, + browser: Browser, + view_info_sender: Sender, + _instance_dir: TempDir, +} + +impl BrowserContext { + fn update_view_info(&self, update: ViewInfoUpdate) { + let _ = self.view_info_sender.send(update); + } + + fn refresh_view_info(&self) { + let view_info = self.delegate.view_info(); + let host = self.browser.host().unwrap(); + host.set_zoom_level(view_info.zoom()); + host.was_resized(); + + // Fix for CEF not updating the view after resize + // TODO: remove once https://github.com/chromiumembedded/cef/issues/3822 is fixed + host.invalidate(cef::PaintElementType::default()); + } + + fn send_web_message(&self, message: Vec) { + self.send_message(MessageType::SendToJS, &message); + } +} + +impl Drop for BrowserContext { + fn drop(&mut self) { + tracing::debug!("Shutting down CEF"); + self.browser.host().unwrap().close_browser(1); + } +} + +impl SendMessage for BrowserContext { + fn send_message(&self, message_type: MessageType, message: &[u8]) { + let Some(frame) = self.browser.main_frame() else { + tracing::error!("Main frame is not available, cannot send message"); + return; + }; + frame.send_message(message_type, message); + } +} + +fn run_on_ui_thread(closure: F) +where + F: FnOnce() + Send + 'static, +{ + let closure_task = ClosureTask::new(closure); + let mut task = Task::new(closure_task); + post_task(ThreadId::from(cef_thread_id_t::TID_UI), Some(&mut task)); +} + +fn with_context(closure: F) +where + F: FnOnce(&mut BrowserContext) + Send + 'static, +{ + run_on_ui_thread(move || { + CONTEXT.with(|b| { + if let Some(context) = b.borrow_mut().as_mut() { + closure(context); + } + }); + }); +} diff --git a/desktop/ui/src/delegate.rs b/desktop/ui/src/delegate.rs new file mode 100644 index 0000000000..373a15c4cf --- /dev/null +++ b/desktop/ui/src/delegate.rs @@ -0,0 +1,59 @@ +use ipc_channel::ipc::IpcSender; +use std::path::PathBuf; +use std::sync::mpsc::Receiver; +use std::sync::{Arc, Mutex}; + +use super::remote::messages::EventMessage; +use super::view::{ViewInfo, ViewInfoReceiver, ViewInfoUpdate}; +use crate::Cursor; + +#[derive(Clone)] +pub(crate) struct BrowserDelegate(Arc); + +struct Inner { + sender: Arc>>, + view_info: Mutex, +} + +impl BrowserDelegate { + pub(crate) fn new(sender: Arc>>, view_info_receiver: Receiver) -> Self { + Self(Arc::new(Inner { + sender, + view_info: Mutex::new(ViewInfoReceiver::new(view_info_receiver)), + })) + } + + fn send(&self, message: EventMessage) { + let Ok(sender) = self.0.sender.lock() else { + tracing::error!("Failed to lock host message sender"); + return; + }; + if let Err(e) = sender.send(message) { + tracing::debug!("Failed to send message to main process: {e}"); + } + } + + pub(crate) fn view_info(&self) -> ViewInfo { + let Ok(mut guard) = self.0.view_info.lock() else { + tracing::error!("Failed to lock the view info mirror"); + return ViewInfo::new(); + }; + guard.current() + } + + pub(crate) fn load_resource(&self, path: PathBuf) -> Option { + crate::resources::load(path) + } + + pub(crate) fn cursor_change(&self, cursor: Cursor) { + self.send(EventMessage::CursorChange(cursor)); + } + + pub(crate) fn initialized_web_communication(&self) { + self.send(EventMessage::WebCommunicationInitialized); + } + + pub(crate) fn receive_web_message(&self, message: &[u8]) { + self.send(EventMessage::WebMessage(message.to_vec())); + } +} diff --git a/desktop/ui/src/dirs.rs b/desktop/ui/src/dirs.rs new file mode 100644 index 0000000000..1bef4d549f --- /dev/null +++ b/desktop/ui/src/dirs.rs @@ -0,0 +1,50 @@ +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +#[cfg(target_os = "linux")] +const APP_DIRECTORY_NAME: &str = "graphite"; +#[cfg(not(target_os = "linux"))] +const APP_DIRECTORY_NAME: &str = "Graphite"; + +pub(crate) fn app_tmp_dir() -> PathBuf { + let path = std::env::temp_dir().join(APP_DIRECTORY_NAME); + if !path.exists() { + fs::create_dir_all(&path).unwrap_or_else(|_| panic!("Failed to create directory at {path:?}")); + } + path +} + +/// Temporary directory that is automatically deleted when dropped. +pub struct TempDir { + path: PathBuf, +} + +impl TempDir { + pub fn new() -> io::Result { + Self::new_with_parent(app_tmp_dir()) + } + + pub fn new_with_parent(parent: impl AsRef) -> io::Result { + let random_suffix = format!("{:032x}", rand::random::()); + let name = format!("{}_{}", std::process::id(), random_suffix); + let path = parent.as_ref().join(name); + fs::create_dir_all(&path)?; + Ok(Self { path }) + } +} + +impl Drop for TempDir { + fn drop(&mut self) { + let result = fs::remove_dir_all(&self.path); + if let Err(e) = result { + tracing::error!("Failed to remove temporary directory at {:?}: {}", self.path, e); + } + } +} + +impl AsRef for TempDir { + fn as_ref(&self) -> &Path { + &self.path + } +} diff --git a/desktop/ui/src/events.rs b/desktop/ui/src/events.rs new file mode 100644 index 0000000000..c71f320cca --- /dev/null +++ b/desktop/ui/src/events.rs @@ -0,0 +1,41 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::Receiver; + +use crate::UiEvent; + +#[derive(Clone)] +pub(crate) struct EventQueue { + sender: std::sync::mpsc::Sender, + terminated: Arc, +} + +impl EventQueue { + pub(crate) fn new() -> (Self, Receiver) { + let (sender, receiver) = std::sync::mpsc::channel(); + ( + Self { + sender, + terminated: Arc::new(AtomicBool::new(false)), + }, + receiver, + ) + } + + pub(crate) fn send(&self, event: UiEvent) { + let _ = self.sender.send(event); + } + + pub(crate) fn terminate(&self, event: UiEvent) { + let _ = self.sender.send(event); + self.terminated.store(true, Ordering::SeqCst); + } + + pub(crate) fn mark_terminated(&self) { + self.terminated.store(true, Ordering::SeqCst); + } + + pub(crate) fn is_terminated(&self) -> bool { + self.terminated.load(Ordering::SeqCst) + } +} diff --git a/desktop/ui/src/frames.rs b/desktop/ui/src/frames.rs new file mode 100644 index 0000000000..1cba4d9c85 --- /dev/null +++ b/desktop/ui/src/frames.rs @@ -0,0 +1,12 @@ +#[cfg(feature = "accelerated_paint")] +pub(crate) mod import; +#[cfg(feature = "accelerated_paint")] +pub(crate) mod plane; +pub(crate) mod receive; +pub(crate) mod sequence; +pub(crate) mod sink; +mod streamer; +mod surface; + +pub(crate) use streamer::FrameStreamer; +pub(crate) use surface::FrameSurface; diff --git a/desktop/ui/src/frames/import.rs b/desktop/ui/src/frames/import.rs new file mode 100644 index 0000000000..856b87feb8 --- /dev/null +++ b/desktop/ui/src/frames/import.rs @@ -0,0 +1,61 @@ +use cef::sys::cef_color_type_t; + +#[cfg(target_os = "windows")] +pub(crate) mod d3d11; +#[cfg(target_os = "linux")] +pub(crate) mod dmabuf; +#[cfg(target_os = "macos")] +pub(crate) mod iosurface; + +pub(crate) type TextureImportResult = Result; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum TextureImportError { + #[error("Invalid texture handle: {0}")] + InvalidHandle(String), + #[error("Unsupported texture format: {format:?}")] + UnsupportedFormat { format: cef_color_type_t }, + #[error("Hardware acceleration not available: {reason}")] + HardwareUnavailable { reason: String }, + #[error("Vulkan operation failed: {operation}")] + VulkanError { operation: String }, + #[error("Platform-specific error: {message}")] + PlatformError { message: String }, +} + +impl From for TextureImportError { + fn from(e: wgpu::hal::DeviceError) -> Self { + TextureImportError::PlatformError { + message: format!("wgpu-hal DeviceError: {:?}", e), + } + } +} + +pub(crate) trait TextureImporter { + fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult; +} + +fn wgpu_format(format: cef_color_type_t) -> Result { + match format { + cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(wgpu::TextureFormat::Bgra8Unorm), + cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(wgpu::TextureFormat::Rgba8Unorm), + _ => Err(TextureImportError::UnsupportedFormat { format }), + } +} + +fn texture_descriptor(width: u32, height: u32, format: cef_color_type_t, label: &'static str) -> Result, TextureImportError> { + Ok(wgpu::TextureDescriptor { + label: Some(label), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu_format(format)?, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }) +} diff --git a/desktop/ui/src/frames/import/d3d11.rs b/desktop/ui/src/frames/import/d3d11.rs new file mode 100644 index 0000000000..1702470e52 --- /dev/null +++ b/desktop/ui/src/frames/import/d3d11.rs @@ -0,0 +1,145 @@ +use super::{TextureImportError, TextureImportResult, TextureImporter, texture_descriptor, wgpu_format}; +use cef::sys::cef_color_type_t; +use std::os::raw::c_void; + +pub struct D3D11Importer { + pub handle: *mut c_void, + pub format: cef_color_type_t, + pub width: u32, + pub height: u32, +} + +impl TextureImporter for D3D11Importer { + fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult { + if self.handle.is_null() { + return Err(TextureImportError::InvalidHandle("Null D3D11 shared texture handle".to_string())); + } + + if is_d3d12_backend(device) { + match self.import_via_d3d12(device) { + Ok(texture) => { + tracing::trace!("Successfully imported D3D11 shared texture via D3D12"); + return Ok(texture); + } + Err(e) => { + tracing::warn!("Failed to import D3D11 via D3D12: {}, trying Vulkan fallback", e); + } + } + } + + let texture = self.import_via_vulkan(device)?; + tracing::trace!("Successfully imported D3D11 shared texture via Vulkan"); + Ok(texture) + } +} + +impl D3D11Importer { + pub fn from_parts(handle: u64, width: u32, height: u32, format: cef_color_type_t) -> Self { + Self { + handle: handle as *mut c_void, + format, + width, + height, + } + } + + fn import_via_d3d12(&self, device: &wgpu::Device) -> TextureImportResult { + use wgpu::hal::api; + let hal_texture = unsafe { + let hal_device_guard = device.as_hal::(); + let Some(hal_device) = hal_device_guard else { + return Err(TextureImportError::HardwareUnavailable { + reason: "Device is not using D3D12 backend".to_string(), + }); + }; + + let d3d12_resource = self.import_d3d11_handle_to_d3d12(&hal_device)?; + + let hal_texture = ::Device::texture_from_raw( + d3d12_resource, + wgpu_format(self.format)?, + wgpu::TextureDimension::D2, + wgpu::Extent3d { + width: self.width, + height: self.height, + depth_or_array_layers: 1, + }, + 1, // mip_level_count + 1, // sample_count + ); + + Ok::<_, TextureImportError>(hal_texture) + }?; + + let texture = unsafe { device.create_texture_from_hal::(hal_texture, &texture_descriptor(self.width, self.height, self.format, "CEF D3D11→D3D12 Shared Texture")?) }; + + Ok(texture) + } + + fn import_via_vulkan(&self, device: &wgpu::Device) -> TextureImportResult { + use wgpu::{TextureUses, wgc::api::Vulkan}; + let hal_texture = unsafe { + let hal_device_guard = device.as_hal::(); + let Some(hal_device) = hal_device_guard else { + return Err(TextureImportError::HardwareUnavailable { + reason: "Device is not using Vulkan backend".to_string(), + }); + }; + + let hal_texture = ::Device::texture_from_d3d11_shared_handle( + &hal_device, + windows::Win32::Foundation::HANDLE(self.handle), + &wgpu::hal::TextureDescriptor { + label: Some("CEF D3D11 Shared Texture"), + size: wgpu::Extent3d { + width: self.width, + height: self.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu_format(self.format)?, + usage: TextureUses::COPY_DST | TextureUses::COPY_SRC | TextureUses::RESOURCE, + memory_flags: wgpu::hal::MemoryFlags::empty(), + view_formats: vec![], + }, + ) + .map_err(|e| TextureImportError::PlatformError { + message: format!("Failed to import D3D11 shared handle into Vulkan: {:?}", e), + })?; + + Ok::<_, TextureImportError>(hal_texture) + }?; + + let texture = unsafe { device.create_texture_from_hal::(hal_texture, &texture_descriptor(self.width, self.height, self.format, "CEF D3D11 Shared Texture")?) }; + + Ok(texture) + } + + fn import_d3d11_handle_to_d3d12(&self, hal_device: &::Device) -> Result { + use windows::Win32::Graphics::Direct3D12::*; + + let d3d12_device = hal_device.raw_device(); + + if self.width == 0 || self.height == 0 { + return Err(TextureImportError::InvalidHandle("Invalid D3D11 texture dimensions".to_string())); + } + + unsafe { + let mut shared_resource: Option = None; + d3d12_device + .OpenSharedHandle(windows::Win32::Foundation::HANDLE(self.handle), &mut shared_resource) + .map_err(|e| TextureImportError::PlatformError { + message: format!("Failed to open D3D11 shared handle on D3D12: {:?}", e), + })?; + + shared_resource.ok_or_else(|| TextureImportError::InvalidHandle("Failed to get D3D12 resource from shared handle".to_string())) + } + } +} + +fn is_d3d12_backend(device: &wgpu::Device) -> bool { + use wgpu::hal::api; + unsafe { device.as_hal::().is_some() } +} diff --git a/desktop/ui/src/frames/import/dmabuf.rs b/desktop/ui/src/frames/import/dmabuf.rs new file mode 100644 index 0000000000..a2cd5e5f33 --- /dev/null +++ b/desktop/ui/src/frames/import/dmabuf.rs @@ -0,0 +1,202 @@ +use super::{TextureImportError, TextureImportResult, TextureImporter, texture_descriptor, wgpu_format}; +use ash::vk; +use cef::sys::cef_color_type_t; +use wgpu::hal::api; + +pub struct DmaBufImporter { + fds: Vec, + format: cef_color_type_t, + modifier: u64, + width: u32, + height: u32, + strides: Vec, + offsets: Vec, +} + +impl TextureImporter for DmaBufImporter { + fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult { + if self.fds.is_empty() { + return Err(TextureImportError::InvalidHandle("No DMA-BUF plane fds".to_string())); + } + let texture = self.import_via_vulkan(device)?; + tracing::trace!("Successfully imported DMA-BUF texture via Vulkan"); + Ok(texture) + } +} + +impl DmaBufImporter { + pub fn from_parts(fds: Vec, strides: Vec, offsets: Vec, modifier: u64, width: u32, height: u32, format: cef_color_type_t) -> Self { + Self { + fds, + format, + modifier, + width, + height, + strides, + offsets, + } + } + + fn import_via_vulkan(&self, device: &wgpu::Device) -> TextureImportResult { + use wgpu::{TextureUses, wgc::api::Vulkan}; + let hal_texture = unsafe { + let hal_device_guard = device.as_hal::(); + let Some(hal_device) = hal_device_guard else { + return Err(TextureImportError::HardwareUnavailable { + reason: "Device is not using Vulkan backend".to_string(), + }); + }; + + let (vk_image, device_memory) = self.create_vulkan_image_from_dmabuf(&hal_device)?; + + let hal_texture = ::Device::texture_from_raw( + &hal_device, + vk_image, + &wgpu::hal::TextureDescriptor { + label: Some("CEF DMA-BUF Texture"), + size: wgpu::Extent3d { + width: self.width, + height: self.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu_format(self.format)?, + usage: TextureUses::COPY_DST | TextureUses::COPY_SRC | TextureUses::RESOURCE, + memory_flags: wgpu::hal::MemoryFlags::empty(), + view_formats: vec![], + }, + None, + wgpu::hal::vulkan::TextureMemory::Dedicated(device_memory), + ); + + Ok::<_, TextureImportError>(hal_texture) + }?; + + let texture = unsafe { device.create_texture_from_hal::(hal_texture, &texture_descriptor(self.width, self.height, self.format, "CEF DMA-BUF Texture")?) }; + + Ok(texture) + } + + fn create_vulkan_image_from_dmabuf(&self, hal_device: &::Device) -> Result<(vk::Image, vk::DeviceMemory), TextureImportError> { + let device = hal_device.raw_device(); + let _instance = hal_device.shared_instance().raw_instance(); + + if self.width == 0 || self.height == 0 { + return Err(TextureImportError::InvalidHandle("Invalid DMA-BUF dimensions".to_string())); + } + + let image_create_info = vk::ImageCreateInfo::default() + .image_type(vk::ImageType::TYPE_2D) + .format(vulkan_format(self.format)?) + .extent(vk::Extent3D { + width: self.width, + height: self.height, + depth: 1, + }) + .mip_levels(1) + .array_layers(1) + .samples(vk::SampleCountFlags::TYPE_1) + .tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT) + .usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_SRC) + .sharing_mode(vk::SharingMode::EXCLUSIVE); + + // Set up DRM format modifier + let plane_layouts = self.create_subresource_layouts(); + let mut drm_format_modifier = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default() + .drm_format_modifier(self.modifier) + .plane_layouts(&plane_layouts); + + let image_create_info = image_create_info.push_next(&mut drm_format_modifier); + + let image = unsafe { + device.create_image(&image_create_info, None).map_err(|e| TextureImportError::VulkanError { + operation: format!("Failed to create Vulkan image: {e:?}"), + })? + }; + + let memory_requirements = unsafe { device.get_image_memory_requirements(image) }; + + // Duplicate the file descriptor + let dup_fd = unsafe { libc::dup(std::os::fd::AsRawFd::as_raw_fd(&self.fds[0])) }; + if dup_fd == -1 { + // SAFETY: the image was created above and never bound or returned. + unsafe { device.destroy_image(image, None) }; + return Err(TextureImportError::PlatformError { + message: "Failed to duplicate DMA-BUF file descriptor".to_string(), + }); + } + + let mut import_memory_fd = vk::ImportMemoryFdInfoKHR::default().handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT).fd(dup_fd); + + let memory_properties = unsafe { hal_device.shared_instance().raw_instance().get_physical_device_memory_properties(hal_device.raw_physical_device()) }; + + let Some(memory_type_index) = find_memory_type_index(memory_requirements.memory_type_bits, vk::MemoryPropertyFlags::empty(), &memory_properties) else { + // SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd + unsafe { + device.destroy_image(image, None); + libc::close(dup_fd); + } + return Err(TextureImportError::VulkanError { + operation: "Failed to find suitable memory type for DMA-BUF".to_string(), + }); + }; + + let allocate_info = vk::MemoryAllocateInfo::default() + .allocation_size(memory_requirements.size) + .memory_type_index(memory_type_index) + .push_next(&mut import_memory_fd); + + let device_memory = match unsafe { device.allocate_memory(&allocate_info, None) } { + Ok(memory) => memory, + Err(e) => { + // SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd + unsafe { + device.destroy_image(image, None); + libc::close(dup_fd); + } + return Err(TextureImportError::VulkanError { + operation: format!("Failed to allocate memory for DMA-BUF: {e:?}"), + }); + } + }; + + if let Err(e) = unsafe { device.bind_image_memory(image, device_memory, 0) } { + // SAFETY: import failed, need to clean up the image and free the memory + unsafe { + device.destroy_image(image, None); + device.free_memory(device_memory, None); + } + return Err(TextureImportError::VulkanError { + operation: format!("Failed to bind memory to image: {e:?}"), + }); + } + + Ok((image, device_memory)) + } + + fn create_subresource_layouts(&self) -> Vec { + (0..self.fds.len()) + .map(|i| vk::SubresourceLayout { + offset: self.offsets.get(i).copied().unwrap_or(0) as u64, + size: 0, // Will be calculated by driver + row_pitch: self.strides.get(i).copied().unwrap_or(0) as u64, + array_pitch: 0, + depth_pitch: 0, + }) + .collect() + } +} + +fn vulkan_format(format: cef_color_type_t) -> Result { + match format { + cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888 => Ok(vk::Format::B8G8R8A8_UNORM), + cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888 => Ok(vk::Format::R8G8B8A8_UNORM), + _ => Err(TextureImportError::UnsupportedFormat { format }), + } +} + +fn find_memory_type_index(type_filter: u32, properties: vk::MemoryPropertyFlags, mem_properties: &vk::PhysicalDeviceMemoryProperties) -> Option { + (0..mem_properties.memory_type_count).find(|&i| (type_filter & (1 << i)) != 0 && mem_properties.memory_types[i as usize].property_flags.contains(properties)) +} diff --git a/desktop/ui/src/frames/import/iosurface.rs b/desktop/ui/src/frames/import/iosurface.rs new file mode 100644 index 0000000000..a315871e2f --- /dev/null +++ b/desktop/ui/src/frames/import/iosurface.rs @@ -0,0 +1,93 @@ +use super::{TextureImportError, TextureImportResult, TextureImporter, texture_descriptor}; +use cef::sys::cef_color_type_t; +use objc2::rc::Retained; +use objc2_io_surface::IOSurfaceRef; +use objc2_metal::{MTLDevice, MTLPixelFormat, MTLStorageMode, MTLTextureDescriptor, MTLTextureType, MTLTextureUsage}; +use wgpu::TextureDescriptor; + +use std::os::raw::c_void; + +pub struct IOSurfaceImporter { + pub handle: *mut c_void, + pub format: cef_color_type_t, + pub width: u32, + pub height: u32, +} + +impl TextureImporter for IOSurfaceImporter { + fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult { + let texture = self.import_via_metal(device)?; + tracing::trace!("Successfully imported IOSurface texture via Metal"); + Ok(texture) + } +} + +impl IOSurfaceImporter { + pub fn from_parts(handle: *mut c_void, width: u32, height: u32, format: cef_color_type_t) -> Self { + Self { handle, format, width, height } + } + + fn get_metal_desc(&self, texture_desc: &TextureDescriptor) -> Result, TextureImportError> { + if self.width == 0 || self.height == 0 { + return Err(TextureImportError::InvalidHandle("Invalid IOSurface texture dimensions".to_string())); + } + + let metal_desc = MTLTextureDescriptor::new(); + unsafe { + metal_desc.setWidth(texture_desc.size.width as _); + metal_desc.setHeight(texture_desc.size.height as _); + metal_desc.setArrayLength(texture_desc.array_layer_count() as _); + metal_desc.setMipmapLevelCount(texture_desc.mip_level_count as _); + metal_desc.setSampleCount(texture_desc.sample_count as _); + metal_desc.setTextureType(MTLTextureType::Type2D); + metal_desc.setPixelFormat(match texture_desc.format { + wgpu::TextureFormat::Rgba8Unorm => MTLPixelFormat::RGBA8Unorm, + wgpu::TextureFormat::Bgra8Unorm => MTLPixelFormat::BGRA8Unorm, + _ => unimplemented!(), + }); + metal_desc.setUsage(MTLTextureUsage::ShaderRead); + metal_desc.setStorageMode(MTLStorageMode::Managed); + } + + Ok(metal_desc) + } + + fn import_via_metal(&self, device: &wgpu::Device) -> TextureImportResult { + let io_surface = std::ptr::NonNull::new(self.handle.cast::()).ok_or(TextureImportError::InvalidHandle("Invalid IOSurface handle".to_string()))?; + + let texture_desc = texture_descriptor(self.width, self.height, self.format, "Cef Texture")?; + let hal_tex = { + let metal_desc = self.get_metal_desc(&texture_desc)?; + + let texture = unsafe { + let hal_device_guard = device.as_hal::(); + let Some(hal_device) = hal_device_guard else { + return Err(TextureImportError::InvalidHandle("Failed to get Metal device from wgpu".to_string())); + }; + + let texture = hal_device + .raw_device() + .newTextureWithDescriptor_iosurface_plane(metal_desc.as_ref(), io_surface.as_ref(), 0) + .ok_or(TextureImportError::InvalidHandle("Invalid IOSurface handle".to_string()))?; + + let hal_tex = ::Device::texture_from_raw( + texture, + texture_desc.format, + MTLTextureType::Type2D, + texture_desc.array_layer_count(), + texture_desc.mip_level_count, + wgpu::hal::CopyExtent { + width: texture_desc.size.width, + height: texture_desc.size.height, + depth: texture_desc.array_layer_count(), + }, + ); + + Ok::<_, TextureImportError>(hal_tex) + }?; + texture + }; + + Ok(unsafe { device.create_texture_from_hal::(hal_tex, &texture_desc) }) + } +} diff --git a/desktop/ui/src/frames/plane.rs b/desktop/ui/src/frames/plane.rs new file mode 100644 index 0000000000..6d35454762 --- /dev/null +++ b/desktop/ui/src/frames/plane.rs @@ -0,0 +1,35 @@ +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "linux")] +pub(crate) use linux::*; + +#[cfg(target_os = "windows")] +mod win; +#[cfg(target_os = "windows")] +pub(crate) use win::*; + +#[cfg(target_os = "macos")] +mod mac; +#[cfg(target_os = "macos")] +pub(crate) use mac::*; + +#[cfg(any(target_os = "linux", target_os = "macos"))] +pub(crate) enum RecvResult { + Frame(WireFrame), + WouldBlock, + #[cfg_attr(target_os = "macos", allow(dead_code))] + Closed, +} + +/// Decode the wire representation of `cef_color_type_t` (its `u32` discriminant), +/// logging unknown discriminants. +fn wire_color_type(format: u32) -> Option { + match format { + 0 => Some(cef::sys::cef_color_type_t::CEF_COLOR_TYPE_RGBA_8888), + 1 => Some(cef::sys::cef_color_type_t::CEF_COLOR_TYPE_BGRA_8888), + _ => { + tracing::error!("Unknown color type {format} in accelerated frame"); + None + } + } +} diff --git a/desktop/ui/src/frames/plane/linux.rs b/desktop/ui/src/frames/plane/linux.rs new file mode 100644 index 0000000000..0d4b730142 --- /dev/null +++ b/desktop/ui/src/frames/plane/linux.rs @@ -0,0 +1,238 @@ +use ipc_channel::ipc::IpcSender; +use std::os::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd}; +use std::sync::{Arc, Mutex}; + +use super::RecvResult; +use crate::frames::surface::FrameSurface; +use crate::remote::HostConfig; +use crate::remote::messages::EventMessage; +pub(crate) const FRAME_SOCKET_CHILD_FD: RawFd = 3; + +#[repr(C)] +#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] +struct FrameDescriptor { + seq: u64, + modifier: u64, + width: u32, + height: u32, + format: u32, + plane_count: u32, + strides: [u32; 4], + offsets: [u32; 4], +} + +const DESCRIPTOR_BYTES: usize = std::mem::size_of::(); +const MAX_PLANES: usize = 4; + +pub(crate) fn socketpair() -> std::io::Result<(OwnedFd, OwnedFd)> { + let mut fds = [0 as RawFd; 2]; + // SAFETY: socketpair call; on success fds are owned by us. + let result = unsafe { libc::socketpair(libc::AF_UNIX, libc::SOCK_SEQPACKET | libc::SOCK_CLOEXEC, 0, fds.as_mut_ptr()) }; + if result != 0 { + return Err(std::io::Error::last_os_error()); + } + // SAFETY: socketpair succeeded, so both fds are valid and not owned elsewhere. + Ok(unsafe { (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])) }) +} + +pub(crate) struct PlaneSender { + socket: OwnedFd, +} + +impl PlaneSender { + pub(crate) fn from_config(config: &HostConfig, _events: Arc>>) -> Option { + let fd = config.frame_socket_fd?; + // SAFETY: the spawner dup2'd this fd for us; nothing else owns it. + let socket = unsafe { OwnedFd::from_raw_fd(fd) }; + + // Restore CLOEXEC so subprocesses don't inherit the socket. + // SAFETY: plain fcntl on an fd we own. + if unsafe { libc::fcntl(socket.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC) } != 0 { + tracing::warn!("Failed to set CLOEXEC on the frame socket: {}", std::io::Error::last_os_error()); + } + Some(Self { socket }) + } + + pub(crate) fn stage(&self, info: &cef::AcceleratedPaintInfo) -> Option { + let plane_count = (info.plane_count.max(0) as usize).min(info.planes.len()); + let mut fds = Vec::with_capacity(plane_count); + let mut strides = [0u32; 4]; + let mut offsets = [0u32; 4]; + for (i, plane) in info.planes[..plane_count].iter().enumerate() { + // SAFETY: CEF keeps the plane fds valid for the `on_accelerated_paint` callback. + let fd = unsafe { BorrowedFd::borrow_raw(plane.fd) }; + match fd.try_clone_to_owned() { + Ok(owned) => fds.push(owned), + Err(e) => { + tracing::error!("Failed to duplicate DMA-BUF plane fd: {e}"); + return None; + } + } + strides[i] = plane.stride; + offsets[i] = plane.offset as u32; + } + + Some(StagedFrame { + descriptor: FrameDescriptor { + seq: 0, + modifier: info.modifier, + width: info.extra.coded_size.width as u32, + height: info.extra.coded_size.height as u32, + format: *info.format.as_ref() as u32, + plane_count: plane_count as u32, + strides, + offsets, + }, + fds, + }) + } + + pub(crate) fn send(&self, seq: u64, frame: StagedFrame) -> std::io::Result<()> { + let mut descriptor = frame.descriptor; + descriptor.seq = seq; + + debug_assert!(frame.fds.len() <= MAX_PLANES); + let fd_bytes = frame.fds.len() * std::mem::size_of::(); + let mut iov = libc::iovec { + iov_base: &descriptor as *const FrameDescriptor as *mut libc::c_void, + iov_len: DESCRIPTOR_BYTES, + }; + let mut cmsg_buffer = [0u8; unsafe { libc::CMSG_SPACE((MAX_PLANES * std::mem::size_of::()) as u32) } as usize]; + let mut msg: libc::msghdr = unsafe { std::mem::zeroed() }; + msg.msg_iov = &mut iov; + msg.msg_iovlen = 1; + msg.msg_control = cmsg_buffer.as_mut_ptr().cast(); + msg.msg_controllen = unsafe { libc::CMSG_SPACE(fd_bytes as u32) } as _; + unsafe { + let cmsg = libc::CMSG_FIRSTHDR(&msg); + (*cmsg).cmsg_level = libc::SOL_SOCKET; + (*cmsg).cmsg_type = libc::SCM_RIGHTS; + (*cmsg).cmsg_len = libc::CMSG_LEN(fd_bytes as u32) as _; + let data = libc::CMSG_DATA(cmsg) as *mut RawFd; + for (i, fd) in frame.fds.iter().enumerate() { + data.add(i).write_unaligned(fd.as_raw_fd()); + } + } + loop { + // SAFETY: msg and everything it points to are valid for the duration of the call. + let sent = unsafe { libc::sendmsg(self.socket.as_raw_fd(), &msg, libc::MSG_NOSIGNAL) }; + if sent >= 0 { + return Ok(()); + } + let error = std::io::Error::last_os_error(); + if error.kind() != std::io::ErrorKind::Interrupted { + return Err(error); + } + } + } +} + +pub(crate) struct StagedFrame { + descriptor: FrameDescriptor, + fds: Vec, +} + +pub(crate) struct PlaneReceiver { + socket: OwnedFd, +} + +impl PlaneReceiver { + pub(crate) fn new(socket: OwnedFd) -> Self { + Self { socket } + } + + pub(crate) fn recv_blocking(&self) -> std::io::Result { + self.recv(false) + } + + pub(crate) fn try_recv(&self) -> std::io::Result { + self.recv(true) + } + + fn recv(&self, nonblocking: bool) -> std::io::Result { + let mut descriptor: FrameDescriptor = bytemuck::Zeroable::zeroed(); + let mut iov = libc::iovec { + iov_base: (&mut descriptor as *mut FrameDescriptor).cast(), + iov_len: DESCRIPTOR_BYTES, + }; + // SAFETY: pure size computation. + let mut cmsg_buffer = [0u8; unsafe { libc::CMSG_SPACE((MAX_PLANES * std::mem::size_of::()) as u32) } as usize]; + + let mut msg: libc::msghdr = unsafe { std::mem::zeroed() }; + msg.msg_iov = &mut iov; + msg.msg_iovlen = 1; + msg.msg_control = cmsg_buffer.as_mut_ptr().cast(); + msg.msg_controllen = cmsg_buffer.len() as _; + + let flags = libc::MSG_CMSG_CLOEXEC | if nonblocking { libc::MSG_DONTWAIT } else { 0 }; + let received = loop { + // SAFETY: msg and everything it points to are valid for the duration of the call. + let received = unsafe { libc::recvmsg(self.socket.as_raw_fd(), &mut msg, flags) }; + if received >= 0 { + break received; + } + let error = std::io::Error::last_os_error(); + match error.kind() { + std::io::ErrorKind::Interrupted => continue, + std::io::ErrorKind::WouldBlock => return Ok(RecvResult::WouldBlock), + _ => return Err(error), + } + }; + + let mut fds = Vec::new(); + // SAFETY: traversing the cmsgs recvmsg just filled; SCM_RIGHTS payload is fds now owned by us. + unsafe { + let mut cmsg = libc::CMSG_FIRSTHDR(&msg); + while !cmsg.is_null() { + if (*cmsg).cmsg_level == libc::SOL_SOCKET && (*cmsg).cmsg_type == libc::SCM_RIGHTS { + let data = libc::CMSG_DATA(cmsg) as *const RawFd; + let count = ((*cmsg).cmsg_len as usize - libc::CMSG_LEN(0) as usize) / std::mem::size_of::(); + for i in 0..count { + fds.push(OwnedFd::from_raw_fd(data.add(i).read_unaligned())); + } + } + cmsg = libc::CMSG_NXTHDR(&msg, cmsg); + } + } + + if received == 0 { + return Ok(RecvResult::Closed); + } + if received as usize != DESCRIPTOR_BYTES || (msg.msg_flags & libc::MSG_CTRUNC) != 0 { + return Err(std::io::Error::other(format!( + "malformed frame message: {received} bytes, flags {:#x} ({} fds)", + msg.msg_flags, + fds.len() + ))); + } + + Ok(RecvResult::Frame(WireFrame { descriptor, fds })) + } +} + +pub(crate) struct WireFrame { + descriptor: FrameDescriptor, + fds: Vec, +} + +impl WireFrame { + pub(crate) fn seq(&self) -> u64 { + self.descriptor.seq + } + + pub(crate) fn import(self, surface: &FrameSurface) -> Option { + let descriptor = self.descriptor; + let format = super::wire_color_type(descriptor.format)?; + let plane_count = (descriptor.plane_count as usize).min(self.fds.len()); + let importer = crate::frames::import::dmabuf::DmaBufImporter::from_parts( + self.fds, + descriptor.strides[..plane_count].to_vec(), + descriptor.offsets[..plane_count].to_vec(), + descriptor.modifier, + descriptor.width, + descriptor.height, + format, + ); + surface.import_texture(importer) + } +} diff --git a/desktop/ui/src/frames/plane/mac.rs b/desktop/ui/src/frames/plane/mac.rs new file mode 100644 index 0000000000..26126f3d47 --- /dev/null +++ b/desktop/ui/src/frames/plane/mac.rs @@ -0,0 +1,275 @@ +use ipc_channel::ipc::IpcSender; +use std::ffi::CString; +use std::sync::{Arc, Mutex}; + +use mach2::kern_return::KERN_SUCCESS; +use mach2::message::{ + MACH_MSG_PORT_DESCRIPTOR, MACH_MSG_SUCCESS, MACH_MSG_TIMEOUT_NONE, MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MOVE_SEND, MACH_MSGH_BITS_COMPLEX, MACH_RCV_MSG, MACH_RCV_TIMED_OUT, MACH_RCV_TIMEOUT, + MACH_SEND_MSG, mach_msg, mach_msg_body_t, mach_msg_header_t, +}; +use mach2::port::{MACH_PORT_NULL, mach_port_t}; +use mach2::traps::mach_task_self; +use objc2_io_surface::IOSurfaceRef; + +use super::RecvResult; +use crate::frames::surface::FrameSurface; +use crate::remote::HostConfig; +use crate::remote::messages::EventMessage; + +// From libSystem, stable since 10.0, not coverd by mach2 +unsafe extern "C" { + static bootstrap_port: mach_port_t; + fn bootstrap_check_in(bp: mach_port_t, service_name: *const std::ffi::c_char, sp: *mut mach_port_t) -> mach2::kern_return::kern_return_t; + fn bootstrap_look_up(bp: mach_port_t, service_name: *const std::ffi::c_char, sp: *mut mach_port_t) -> mach2::kern_return::kern_return_t; +} + +// `mach_msg_port_descriptor_t` kernel ABI +#[repr(C)] +#[derive(Clone, Copy)] +struct PortDescriptor { + name: mach_port_t, + pad1: u32, + pad2: u16, + disposition: u8, + descriptor_type: u8, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct FrameDescriptor { + seq: u64, + width: u32, + height: u32, + format: u32, + _pad: u32, +} + +#[repr(C)] +struct FrameMessage { + header: mach_msg_header_t, + body: mach_msg_body_t, + surface: PortDescriptor, + descriptor: FrameDescriptor, +} + +#[repr(C)] +struct FrameMessageBuffer { + message: FrameMessage, + trailer: [u8; 64], +} + +struct SendRight(mach_port_t); + +// SAFETY: mach port names are task-wide; rights may be used from any thread. +unsafe impl Send for SendRight {} +unsafe impl Sync for SendRight {} + +impl Drop for SendRight { + fn drop(&mut self) { + // SAFETY: we own one reference on this send right. + unsafe { mach2::mach_port::mach_port_deallocate(mach_task_self(), self.0) }; + } +} + +pub(crate) fn create_service(name: &str) -> std::io::Result { + let c_name = CString::new(name).map_err(std::io::Error::other)?; + let mut port: mach_port_t = MACH_PORT_NULL; + // SAFETY: plain bootstrap call; on success we own the service's receive right. + let result = unsafe { bootstrap_check_in(bootstrap_port, c_name.as_ptr(), &mut port) }; + if result != KERN_SUCCESS { + return Err(std::io::Error::other(format!("bootstrap_check_in failed: {result:#x}"))); + } + Ok(port) +} + +fn look_up_service(name: &str) -> std::io::Result { + let c_name = CString::new(name).map_err(std::io::Error::other)?; + let mut port: mach_port_t = MACH_PORT_NULL; + // SAFETY: plain bootstrap call; on success we own a send right. + let result = unsafe { bootstrap_look_up(bootstrap_port, c_name.as_ptr(), &mut port) }; + if result != KERN_SUCCESS { + return Err(std::io::Error::other(format!("bootstrap_look_up failed: {result:#x}"))); + } + Ok(SendRight(port)) +} + +pub(crate) struct PlaneSender { + service: SendRight, +} + +impl PlaneSender { + pub(crate) fn from_config(config: &HostConfig, _events: Arc>>) -> Option { + let name = config.frame_service.as_deref()?; + match look_up_service(name) { + Ok(service) => Some(Self { service }), + Err(e) => { + tracing::error!("Failed to look up the accelerated frame service, falling back to software frames: {e}"); + None + } + } + } + + pub(crate) fn stage(&self, info: &cef::AcceleratedPaintInfo) -> Option { + let Some(surface) = std::ptr::NonNull::new(info.shared_texture_io_surface.cast::()) else { + tracing::error!("Accelerated paint delivered a null IOSurface"); + return None; + }; + // SAFETY: CEF keeps the surface valid for the `on_accelerated_paint` callback. + let port = unsafe { surface.as_ref() }.create_mach_port(); + if port == MACH_PORT_NULL { + tracing::error!("Failed to wrap the IOSurface in a mach port"); + return None; + } + Some(StagedFrame { + descriptor: FrameDescriptor { + seq: 0, + width: info.extra.coded_size.width as u32, + height: info.extra.coded_size.height as u32, + format: *info.format.as_ref() as u32, + _pad: 0, + }, + surface: SendRight(port), + }) + } + + pub(crate) fn send(&self, seq: u64, frame: StagedFrame) -> std::io::Result<()> { + let mut descriptor = frame.descriptor; + descriptor.seq = seq; + let mut message = FrameMessage { + header: mach_msg_header_t { + msgh_bits: MACH_MSG_TYPE_COPY_SEND | MACH_MSGH_BITS_COMPLEX, + msgh_size: std::mem::size_of::() as u32, + msgh_remote_port: self.service.0, + msgh_local_port: MACH_PORT_NULL, + msgh_voucher_port: MACH_PORT_NULL, + msgh_id: 0, + }, + body: mach_msg_body_t { msgh_descriptor_count: 1 }, + surface: PortDescriptor { + name: frame.surface.0, + pad1: 0, + pad2: 0, + disposition: MACH_MSG_TYPE_MOVE_SEND as u8, + descriptor_type: MACH_MSG_PORT_DESCRIPTOR as u8, + }, + descriptor, + }; + + // Kernel takes ownership of the surface and moves it to the receiver. We must not drop. + std::mem::forget(frame.surface); + + // SAFETY: message is a well-formed complex message of the declared size. + let result = unsafe { + mach_msg( + &mut message.header, + MACH_SEND_MSG, + std::mem::size_of::() as u32, + 0, + MACH_PORT_NULL, + MACH_MSG_TIMEOUT_NONE, + MACH_PORT_NULL, + ) + }; + if result != MACH_MSG_SUCCESS { + return Err(std::io::Error::other(format!("mach_msg send failed: {result:#x}"))); + } + Ok(()) + } +} + +pub(crate) struct StagedFrame { + descriptor: FrameDescriptor, + surface: SendRight, +} + +pub(crate) struct PlaneReceiver { + port: mach_port_t, +} + +impl PlaneReceiver { + pub(crate) fn new(port: mach_port_t) -> Self { + Self { port } + } + + pub(crate) fn recv_blocking(&self) -> std::io::Result { + self.recv(false) + } + + pub(crate) fn try_recv(&self) -> std::io::Result { + self.recv(true) + } + + fn recv(&self, nonblocking: bool) -> std::io::Result { + // SAFETY: zeroed is a valid representation for these plain-data structs. + let mut buffer: FrameMessageBuffer = unsafe { std::mem::zeroed() }; + let (options, timeout) = if nonblocking { + (MACH_RCV_MSG | MACH_RCV_TIMEOUT, 0) + } else { + (MACH_RCV_MSG, MACH_MSG_TIMEOUT_NONE) + }; + + // SAFETY: the buffer is large enough for the message plus the basic trailer. + let result = unsafe { + mach_msg( + &mut buffer.message.header, + options, + 0, + std::mem::size_of::() as u32, + self.port, + timeout, + MACH_PORT_NULL, + ) + }; + if result == MACH_RCV_TIMED_OUT { + return Ok(RecvResult::WouldBlock); + } + if result != MACH_MSG_SUCCESS { + return Err(std::io::Error::other(format!("mach_msg receive failed: {result:#x}"))); + } + + let received_complex = buffer.message.header.msgh_bits & MACH_MSGH_BITS_COMPLEX != 0; + let descriptor_count = if received_complex { buffer.message.body.msgh_descriptor_count } else { 0 }; + let surface = (descriptor_count == 1 && buffer.message.surface.descriptor_type == MACH_MSG_PORT_DESCRIPTOR as u8).then(|| SendRight(buffer.message.surface.name)); + + if buffer.message.header.msgh_size as usize != std::mem::size_of::() { + return Err(std::io::Error::other(format!("malformed frame message: {} bytes", buffer.message.header.msgh_size))); + } + let Some(surface) = surface else { + return Err(std::io::Error::other("frame message carried no surface port")); + }; + + Ok(RecvResult::Frame(WireFrame { + descriptor: buffer.message.descriptor, + surface, + })) + } +} + +pub(crate) struct WireFrame { + descriptor: FrameDescriptor, + surface: SendRight, +} + +impl WireFrame { + pub(crate) fn seq(&self) -> u64 { + self.descriptor.seq + } + + pub(crate) fn import(self, surface: &FrameSurface) -> Option { + let WireFrame { descriptor, surface: port } = self; + let format = super::wire_color_type(descriptor.format)?; + + // Lookup takes its own reference on the surface, port can be dropped. + let io_surface = IOSurfaceRef::lookup_from_mach_port(port.0); + drop(port); + + let Some(io_surface) = io_surface else { + tracing::error!("Failed to look up the IOSurface for frame {}", descriptor.seq); + return None; + }; + let io_surface_ref: &IOSurfaceRef = &io_surface; + + let importer = crate::frames::import::iosurface::IOSurfaceImporter::from_parts(io_surface_ref as *const _ as *mut std::os::raw::c_void, descriptor.width, descriptor.height, format); + surface.import_texture(importer) + } +} diff --git a/desktop/ui/src/frames/plane/win.rs b/desktop/ui/src/frames/plane/win.rs new file mode 100644 index 0000000000..e52e9e8a61 --- /dev/null +++ b/desktop/ui/src/frames/plane/win.rs @@ -0,0 +1,161 @@ +use ipc_channel::ipc::IpcSender; +use std::sync::{Arc, Mutex}; + +use windows::Win32::Foundation::{CloseHandle, DUPLICATE_CLOSE_SOURCE, DUPLICATE_SAME_ACCESS, DuplicateHandle, HANDLE}; +use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcess, PROCESS_DUP_HANDLE}; + +use crate::frames::surface::FrameSurface; +use crate::remote::HostConfig; +use crate::remote::messages::EventMessage; + +struct ParentProcess(HANDLE); + +// SAFETY: process handles may be used and closed from any thread. +unsafe impl Send for ParentProcess {} +unsafe impl Sync for ParentProcess {} + +impl ParentProcess { + fn open(pid: u32) -> windows::core::Result { + // SAFETY: plain OpenProcess call; on success the handle is ours to close. + unsafe { OpenProcess(PROCESS_DUP_HANDLE, false, pid).map(Self) } + } + + fn duplicate_into(&self, handle: HANDLE) -> windows::core::Result { + let mut target = HANDLE::default(); + // SAFETY: both process handles are valid; `target` receives the duplicate. + unsafe { DuplicateHandle(GetCurrentProcess(), handle, self.0, &mut target, 0, false, DUPLICATE_SAME_ACCESS)? }; + Ok(target.0 as u64) + } + + fn close_in_parent(&self, handle: u64) { + let mut reclaimed = HANDLE::default(); + // SAFETY: `handle` came from `duplicate_into` and is valid. + unsafe { + if let Err(e) = DuplicateHandle(self.0, HANDLE(handle as _), GetCurrentProcess(), &mut reclaimed, 0, false, DUPLICATE_CLOSE_SOURCE) { + tracing::warn!("Failed to reclaim a frame handle from the main process: {e}"); + } + if !reclaimed.is_invalid() { + let _ = CloseHandle(reclaimed); + } + } + } +} + +impl Drop for ParentProcess { + fn drop(&mut self) { + // SAFETY: we own the process handle. + unsafe { + let _ = CloseHandle(self.0); + } + } +} + +pub(crate) struct PlaneSender { + parent: Arc, + events: Arc>>, +} + +impl PlaneSender { + pub(crate) fn from_config(config: &HostConfig, events: Arc>>) -> Option { + match ParentProcess::open(config.main_pid) { + Ok(parent) => Some(Self { parent: Arc::new(parent), events }), + Err(e) => { + tracing::error!("Failed to open the main process for handle duplication, falling back to software frames: {e}"); + None + } + } + } + + pub(crate) fn stage(&self, info: &cef::AcceleratedPaintInfo) -> Option { + let handle = match self.parent.duplicate_into(HANDLE(info.shared_texture_handle)) { + Ok(handle) => handle, + Err(e) => { + tracing::error!("Failed to duplicate the shared texture handle into the main process: {e}"); + return None; + } + }; + Some(StagedFrame { + handle: HandleInParent { handle, parent: self.parent.clone() }, + width: info.extra.coded_size.width as u32, + height: info.extra.coded_size.height as u32, + format: *info.format.as_ref() as u32, + }) + } + + pub(crate) fn send(&self, seq: u64, frame: StagedFrame) -> std::io::Result<()> { + let message = EventMessage::AcceleratedFrame { + seq, + handle: frame.handle.handle, + width: frame.width, + height: frame.height, + format: frame.format, + }; + let sender = self.events.lock().map_err(|_| std::io::Error::other("the host message sender lock is poisoned"))?; + match sender.send(message) { + Ok(()) => { + // Dropping the handle would reclaim it. We must not drop. + std::mem::forget(frame.handle); + Ok(()) + } + Err(e) => Err(std::io::Error::other(e.to_string())), + } + } +} + +pub(crate) struct StagedFrame { + handle: HandleInParent, + width: u32, + height: u32, + format: u32, +} + +struct HandleInParent { + handle: u64, + parent: Arc, +} + +impl Drop for HandleInParent { + fn drop(&mut self) { + self.parent.close_in_parent(self.handle); + } +} + +pub(crate) struct WireFrame { + seq: u64, + handle: ReceivedHandle, + width: u32, + height: u32, + format: u32, +} + +impl WireFrame { + pub(crate) fn new(seq: u64, handle: u64, width: u32, height: u32, format: u32) -> Self { + Self { + seq, + handle: ReceivedHandle(handle), + width, + height, + format, + } + } + + pub(crate) fn seq(&self) -> u64 { + self.seq + } + + pub(crate) fn import(self, surface: &FrameSurface) -> Option { + let format = super::wire_color_type(self.format)?; + surface.import_texture(crate::frames::import::d3d11::D3D11Importer::from_parts(self.handle.0, self.width, self.height, format)) + } +} + +struct ReceivedHandle(u64); + +impl Drop for ReceivedHandle { + fn drop(&mut self) { + // SAFETY: the host duplicated this handle into our process for us to own. + if let Err(e) = unsafe { CloseHandle(HANDLE(self.0 as _)) } { + tracing::warn!("Failed to close a remote frame handle: {e}"); + } + } +} diff --git a/desktop/ui/src/frames/receive.rs b/desktop/ui/src/frames/receive.rs new file mode 100644 index 0000000000..562804a8b4 --- /dev/null +++ b/desktop/ui/src/frames/receive.rs @@ -0,0 +1,131 @@ +use ipc_channel::ipc::{IpcSender, IpcSharedMemory}; +use std::sync::Arc; + +use super::FrameSurface; +#[cfg(feature = "accelerated_paint")] +use super::plane; +use super::sink::FrameSink; +use crate::UiEvent; +use crate::events::EventQueue; +use crate::remote::messages::HostControlMessage; + +pub(crate) enum PendingFrame { + Software { + seq: u64, + segment: u32, + width: u32, + height: u32, + }, + #[cfg(all(target_os = "windows", feature = "accelerated_paint"))] + Accelerated(plane::WireFrame), +} + +impl PendingFrame { + pub(crate) fn seq(&self) -> u64 { + match self { + PendingFrame::Software { seq, .. } => *seq, + #[cfg(all(target_os = "windows", feature = "accelerated_paint"))] + PendingFrame::Accelerated(frame) => frame.seq(), + } + } +} + +pub(crate) struct SegmentTable(Vec>); + +impl SegmentTable { + pub(crate) fn new() -> Self { + Self(Vec::new()) + } + + pub(crate) fn advertise(&mut self, index: u32, shm: IpcSharedMemory) { + let index = index as usize; + if self.0.len() <= index { + self.0.resize_with(index + 1, || None); + } + self.0[index] = Some(shm); + } + + fn frame(&self, seq: u64, segment: u32, width: u32, height: u32) -> Option<&[u8]> { + let frame_bytes = width as usize * height as usize * 4; + match self.0.get(segment as usize).and_then(Option::as_ref) { + Some(shm) if shm.len() >= frame_bytes => Some(&shm[..frame_bytes]), + Some(shm) => { + tracing::error!("Frame {seq} needs {frame_bytes} bytes but segment {segment} holds {}", shm.len()); + None + } + None => { + tracing::error!("Frame {seq} references unadvertised segment {segment}"); + None + } + } + } +} + +#[derive(Clone)] +pub(crate) struct FrameConsumer { + surface: FrameSurface, + events: EventQueue, + sender: IpcSender, + sink: Arc, +} + +impl FrameConsumer { + pub(crate) fn new(surface: FrameSurface, events: EventQueue, sender: IpcSender) -> Self { + Self { + surface, + events, + sender, + sink: Arc::new(FrameSink::new()), + } + } + + fn deliver(&self, seq: u64, install: impl FnOnce(&FrameSurface) -> Option) { + self.sink.deliver(&self.sender, seq, || match install(&self.surface) { + Some(texture) => { + self.events.send(UiEvent::Frame(texture)); + true + } + None => false, + }); + } + + pub(crate) fn deliver_pending(&self, frame: PendingFrame, segments: &SegmentTable) { + match frame { + PendingFrame::Software { seq, segment, width, height } => { + self.deliver(seq, |surface| { + segments.frame(seq, segment, width, height).and_then(|pixels| surface.upload_buffer(pixels, width, height)) + }); + } + #[cfg(all(target_os = "windows", feature = "accelerated_paint"))] + PendingFrame::Accelerated(frame) => self.deliver_accelerated(frame), + } + } + + #[cfg(feature = "accelerated_paint")] + pub(crate) fn deliver_accelerated(&self, frame: plane::WireFrame) { + let seq = frame.seq(); + self.deliver(seq, |surface| frame.import(surface)); + } +} + +#[cfg(all(any(target_os = "linux", target_os = "macos"), feature = "accelerated_paint"))] +pub(crate) fn plane_receiver_loop(receiver: plane::PlaneReceiver, consumer: FrameConsumer) { + loop { + let mut frame = loop { + match receiver.recv_blocking() { + Ok(plane::RecvResult::Frame(frame)) => break frame, + Ok(plane::RecvResult::WouldBlock) => continue, + Ok(plane::RecvResult::Closed) => return, + Err(e) => { + tracing::error!("Accelerated frame plane failed: {e}"); + return; + } + } + }; + // Drain any newer frames that have arrived since the blocking receive + while let Ok(plane::RecvResult::Frame(newer)) = receiver.try_recv() { + frame = newer; + } + consumer.deliver_accelerated(frame); + } +} diff --git a/desktop/ui/src/frames/sequence.rs b/desktop/ui/src/frames/sequence.rs new file mode 100644 index 0000000000..dbb922ecad --- /dev/null +++ b/desktop/ui/src/frames/sequence.rs @@ -0,0 +1,62 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +use crate::consts::FRAMES_IN_FLIGHT_LIMIT; + +pub(crate) struct SequenceState { + last_sent: AtomicU64, + last_acked: AtomicU64, +} + +impl SequenceState { + pub(crate) fn new() -> Self { + Self { + last_sent: AtomicU64::new(0), + last_acked: AtomicU64::new(0), + } + } + + pub(crate) fn claim(self: &Arc) -> Option { + let last_sent = self.last_sent.load(Ordering::Relaxed); + let last_acked = self.last_acked.load(Ordering::Relaxed); + if last_sent.saturating_sub(last_acked) >= FRAMES_IN_FLIGHT_LIMIT { + return None; + } + let seq = last_sent + 1; + self.last_sent.store(seq, Ordering::Relaxed); + Some(FrameSequenceClaim { + seq, + sequence: self.clone(), + commited: false, + }) + } + + pub(crate) fn ack(&self, seq: u64) { + self.last_acked.fetch_max(seq, Ordering::Relaxed); + } +} + +pub(crate) struct FrameSequenceClaim { + seq: u64, + commited: bool, + sequence: Arc, +} + +impl FrameSequenceClaim { + pub(crate) fn seq(&self) -> u64 { + self.seq + } + + pub(crate) fn commit(mut self) { + self.commited = true; + } +} + +impl Drop for FrameSequenceClaim { + fn drop(&mut self) { + // Roll back the claim if it was never committed to free the sequence number + if !self.commited { + let _ = self.sequence.last_sent.compare_exchange(self.seq, self.seq - 1, Ordering::Relaxed, Ordering::Relaxed); + } + } +} diff --git a/desktop/ui/src/frames/sink.rs b/desktop/ui/src/frames/sink.rs new file mode 100644 index 0000000000..f1bfcd22b5 --- /dev/null +++ b/desktop/ui/src/frames/sink.rs @@ -0,0 +1,45 @@ +use ipc_channel::ipc::IpcSender; +use std::sync::Mutex; + +use crate::remote::messages::HostControlMessage; + +pub(super) struct FrameSink { + state: Mutex, +} + +#[derive(Default)] +struct FrameSinkState { + newest_installed: u64, + last_acked: u64, +} + +impl FrameSink { + pub(super) fn new() -> Self { + Self { + state: Mutex::new(FrameSinkState::default()), + } + } + + pub(super) fn deliver(&self, sender: &IpcSender, seq: u64, install: impl FnOnce() -> bool) { + let Ok(mut state) = self.state.lock() else { + tracing::error!("Failed to lock the frame sink"); + return; + }; + + if seq > 1 && seq - 1 > state.last_acked { + if let Err(e) = sender.send(HostControlMessage::FrameAck { seq: seq - 1 }) { + tracing::debug!("Failed to ack superseded frames to CEF host: {e}"); + } + state.last_acked = seq - 1; + } + if seq > state.newest_installed && install() { + state.newest_installed = seq; + } + if seq > state.last_acked { + if let Err(e) = sender.send(HostControlMessage::FrameAck { seq }) { + tracing::debug!("Failed to ack frame to CEF host: {e}"); + } + state.last_acked = seq; + } + } +} diff --git a/desktop/ui/src/frames/streamer.rs b/desktop/ui/src/frames/streamer.rs new file mode 100644 index 0000000000..38034068d4 --- /dev/null +++ b/desktop/ui/src/frames/streamer.rs @@ -0,0 +1,155 @@ +use ipc_channel::ipc::{IpcSender, IpcSharedMemory}; +use std::sync::{Arc, Mutex}; + +#[cfg(feature = "accelerated_paint")] +use super::plane; +use super::sequence::{FrameSequenceClaim, SequenceState}; +use crate::consts::{FRAME_SEGMENT_GRANULARITY, FRAME_SEGMENT_POOL_SIZE}; +use crate::remote::messages::EventMessage; + +#[derive(Clone)] +pub(crate) struct FrameStreamer(Arc); + +struct StreamerInner { + events: Arc>>, + sequence: Arc, + #[cfg(feature = "accelerated_paint")] + plane: Option, + staged: Mutex, +} + +#[derive(Default)] +struct Staged { + segments: Vec, + pending_adverts: Vec<(u32, IpcSharedMemory)>, + buffer: Option, + #[cfg(feature = "accelerated_paint")] + accelerated: Option<(FrameSequenceClaim, plane::StagedFrame)>, +} + +struct StagedBuffer { + claim: FrameSequenceClaim, + segment: u32, + width: u32, + height: u32, +} + +impl FrameStreamer { + pub(crate) fn new(events: Arc>>, sequence: Arc, #[cfg(feature = "accelerated_paint")] plane: Option) -> Self { + Self(Arc::new(StreamerInner { + events, + sequence, + #[cfg(feature = "accelerated_paint")] + plane, + staged: Mutex::new(Staged::default()), + })) + } + + pub(crate) fn stage_buffer(&self, buffer: &[u8], width: u32, height: u32) { + debug_assert_eq!(buffer.len(), width as usize * height as usize * 4); + if buffer.is_empty() { + return; + } + + if buffer.chunks_exact(4).all(|pixel| pixel[3] == 0) { + tracing::debug!("Skipping fully transparent {width}x{height} frame from CEF"); + return; + } + + let Some(claim) = self.0.sequence.claim() else { + return; + }; + let segment = (claim.seq() % FRAME_SEGMENT_POOL_SIZE) as u32; + + let Ok(mut staged) = self.0.staged.lock() else { + tracing::error!("Failed to lock the frame staging state"); + return; + }; + let staged = &mut *staged; + if staged.segments.len() < FRAME_SEGMENT_POOL_SIZE as usize { + staged.segments.resize_with(FRAME_SEGMENT_POOL_SIZE as usize, || IpcSharedMemory::from_bytes(&[])); + } + + let backing = &mut staged.segments[segment as usize]; + if backing.len() < buffer.len() { + let capacity = buffer.len().next_multiple_of(FRAME_SEGMENT_GRANULARITY); + *backing = IpcSharedMemory::from_byte(0, capacity); + staged.pending_adverts.push((segment, backing.clone())); + } + + unsafe { backing.deref_mut()[..buffer.len()].copy_from_slice(buffer) }; + + #[cfg(target_os = "macos")] + if !staged.pending_adverts.iter().any(|(index, _)| *index == segment) { + staged.pending_adverts.push((segment, backing.clone())); + } + + staged.buffer = Some(StagedBuffer { claim, segment, width, height }); + } + + #[cfg(feature = "accelerated_paint")] + pub(crate) fn stage_texture(&self, info: &cef::AcceleratedPaintInfo) { + let Some(plane) = &self.0.plane else { + tracing::error!("Accelerated paint delivered without a frame plane"); + return; + }; + + let Some(claim) = self.0.sequence.claim() else { + return; + }; + let Some(frame) = plane.stage(info) else { + return; + }; + let Ok(mut staged) = self.0.staged.lock() else { + tracing::error!("Failed to lock the frame staging state"); + return; + }; + staged.accelerated = Some((claim, frame)); + } + + pub(crate) fn publish(&self) { + let Ok(mut staged) = self.0.staged.lock() else { + tracing::error!("Failed to lock the frame staging state"); + return; + }; + let adverts = std::mem::take(&mut staged.pending_adverts); + let software = staged.buffer.take(); + #[cfg(feature = "accelerated_paint")] + let accelerated = staged.accelerated.take(); + drop(staged); + + #[cfg(feature = "accelerated_paint")] + if let Some((claim, frame)) = accelerated + && let Some(plane) = &self.0.plane + { + match plane.send(claim.seq(), frame) { + Ok(()) => claim.commit(), + Err(e) => tracing::debug!("Failed to send accelerated frame to main process: {e}"), + } + } + + if adverts.is_empty() && software.is_none() { + return; + } + let Ok(sender) = self.0.events.lock() else { + tracing::error!("Failed to lock host message sender"); + return; + }; + for (index, shm) in adverts { + if let Err(e) = sender.send(EventMessage::AdvertiseFrameSegment { index, shm }) { + tracing::debug!("Failed to send frame segment to main process: {e}"); + } + } + if let Some(StagedBuffer { claim, segment, width, height }) = software { + match sender.send(EventMessage::SoftwareFrame { + seq: claim.seq(), + segment, + width, + height, + }) { + Ok(()) => claim.commit(), + Err(e) => tracing::debug!("Failed to send frame to main process: {e}"), + } + } + } +} diff --git a/desktop/ui/src/frames/surface.rs b/desktop/ui/src/frames/surface.rs new file mode 100644 index 0000000000..4c148df16f --- /dev/null +++ b/desktop/ui/src/frames/surface.rs @@ -0,0 +1,129 @@ +use std::sync::{Arc, Mutex}; + +#[derive(Clone)] +pub(crate) struct FrameSurface { + device: wgpu::Device, + queue: wgpu_sync::Queue, + slot: Arc>>, +} + +impl FrameSurface { + pub(crate) fn new(device: wgpu::Device, queue: wgpu_sync::Queue) -> Self { + Self { + device, + queue, + slot: Arc::new(Mutex::new(None)), + } + } + + pub(crate) fn upload_buffer(&self, buffer: &[u8], width: u32, height: u32) -> Option { + debug_assert_eq!(buffer.len(), width as usize * height as usize * 4); + + let Ok(mut slot) = self.slot.lock() else { + tracing::error!("Failed to lock the frame surface"); + return None; + }; + + if slot.as_ref().is_none_or(|texture| texture.width() != width || texture.height() != height) { + *slot = Some(self.device.create_texture(&wgpu::TextureDescriptor { + label: Some("CEF Texture"), + size: wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Bgra8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + })); + } + let texture = slot.as_ref().expect("Texture was just created"); + + self.queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + buffer, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(4 * width), + rows_per_image: None, + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + + Some(texture.clone()) + } + + #[cfg(feature = "accelerated_paint")] + pub(crate) fn import_texture(&self, importer: impl crate::frames::import::TextureImporter) -> Option { + let imported = match importer.import_to_wgpu(&self.device) { + Ok(texture) => texture, + Err(e) => { + tracing::error!("Failed to import remote accelerated frame: {e}"); + return None; + } + }; + + let size = wgpu::Extent3d { + width: imported.width(), + height: imported.height(), + depth_or_array_layers: 1, + }; + let owned = self.device.create_texture(&wgpu::TextureDescriptor { + label: Some("CEF Imported Frame Copy"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: imported.format(), + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + + let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("CEF Frame Copy Encoder"), + }); + encoder.copy_texture_to_texture( + wgpu::TexelCopyTextureInfo { + texture: &imported, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyTextureInfo { + texture: &owned, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + size, + ); + + let submission = self.queue.submit(std::iter::once(encoder.finish())); + + // Block until the copy is done, so the caller's ack cannot free CEF to + // overwrite the shared texture while the copy is still reading it. + let _ = self.device.poll(wgpu::PollType::Wait { + submission_index: Some(submission), + timeout: None, + }); + + let Ok(mut slot) = self.slot.lock() else { + tracing::error!("Failed to lock the frame surface"); + return None; + }; + *slot = Some(owned.clone()); + Some(owned) + } +} diff --git a/desktop/ui/src/input.rs b/desktop/ui/src/input.rs new file mode 100644 index 0000000000..43f8709fbc --- /dev/null +++ b/desktop/ui/src/input.rs @@ -0,0 +1,258 @@ +use cef::sys::{cef_key_event_type_t, cef_mouse_button_type_t}; +use cef::{Browser, ImplBrowser, ImplBrowserHost, KeyEvent, MouseEvent}; +use winit::event::{ButtonSource, ElementState, MouseButton, MouseScrollDelta, WindowEvent}; + +mod keymap; +use keymap::{ToCharRepresentation, ToNativeKeycode, ToVKBits}; + +mod state; +pub(crate) use state::{CefModifiers, InputState}; + +use super::consts::{PINCH_ZOOM_SPEED, SCROLL_LINE_HEIGHT, SCROLL_LINE_WIDTH, SCROLL_SPEED_X, SCROLL_SPEED_Y}; + +/// A window input translated into the plain data CEF consumes — no winit types, so it can +/// be applied to a browser living in another process. +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub(crate) enum InputEvent { + MouseMove { data: MouseData, leave: bool }, + MouseClick { data: MouseData, button: MouseButtonKind, up: bool, click_count: i32 }, + MouseWheel { data: MouseData, delta_x: i32, delta_y: i32 }, + Key(KeyData), +} + +#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] +pub(crate) struct MouseData { + pub(crate) x: i32, + pub(crate) y: i32, + pub(crate) modifiers: u32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum MouseButtonKind { + Left, + Right, + Middle, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub(crate) enum KeyEventKind { + RawKeyDown, + KeyUp, + Char, +} + +#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] +pub(crate) struct KeyData { + pub(crate) kind: KeyEventKind, + pub(crate) modifiers: u32, + pub(crate) windows_key_code: i32, + pub(crate) native_key_code: i32, + pub(crate) character: u16, + pub(crate) unmodified_character: u16, +} + +/// Turns a winit event into zero or more [`InputEvent`]s, updating the tracked input state +/// (cursor position, click counting, modifiers) along the way. +pub(crate) fn translate(input_state: &mut InputState, event: &WindowEvent) -> Vec { + match event { + WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerEntered { position, .. } => { + if !input_state.cursor_move(position) { + return Vec::new(); + } + vec![InputEvent::MouseMove { + data: input_state.mouse_data(), + leave: false, + }] + } + WindowEvent::PointerLeft { position, .. } => { + if let Some(position) = position { + let _ = input_state.cursor_move(position); + } + vec![InputEvent::MouseMove { + data: input_state.mouse_data(), + leave: true, + }] + } + WindowEvent::PointerButton { state, button, .. } => { + let mouse_button = match button { + ButtonSource::Mouse(mouse_button) => mouse_button, + _ => { + return Vec::new(); // TODO: Handle touch input + } + }; + + let click_count = input_state.mouse_input(mouse_button, state).into(); + let up = matches!(state, ElementState::Released); + let button = match mouse_button { + MouseButton::Left => MouseButtonKind::Left, + MouseButton::Right => MouseButtonKind::Right, + MouseButton::Middle => MouseButtonKind::Middle, + _ => return Vec::new(), + }; + + vec![InputEvent::MouseClick { + data: input_state.mouse_data(), + button, + up, + click_count, + }] + } + WindowEvent::MouseWheel { delta, phase: _, device_id: _, .. } => { + let (mut delta_x, mut delta_y) = match delta { + MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_LINE_WIDTH as f32, y * SCROLL_LINE_HEIGHT as f32), + MouseScrollDelta::PixelDelta(physical_position) => (physical_position.x as f32, physical_position.y as f32), + }; + delta_x *= SCROLL_SPEED_X; + delta_y *= SCROLL_SPEED_Y; + + vec![InputEvent::MouseWheel { + data: input_state.mouse_data(), + delta_x: delta_x as i32, + delta_y: delta_y as i32, + }] + } + WindowEvent::ModifiersChanged(modifiers) => { + input_state.modifiers_changed(&modifiers.state()); + Vec::new() + } + WindowEvent::KeyboardInput { device_id: _, event, is_synthetic: _ } => { + input_state.modifiers_apply_key_event(&event.logical_key, &event.state); + + let mut kind = match (event.state, &event.logical_key) { + (ElementState::Pressed, winit::keyboard::Key::Character(_)) => KeyEventKind::Char, + (ElementState::Pressed, _) => KeyEventKind::RawKeyDown, + (ElementState::Released, _) => KeyEventKind::KeyUp, + }; + + let modifiers = input_state.cef_modifiers(&event.location, event.repeat).into(); + + let windows_key_code = match &event.logical_key { + winit::keyboard::Key::Named(named) => named.to_vk_bits(), + winit::keyboard::Key::Character(char) => char.chars().next().unwrap_or_default().to_vk_bits(), + _ => 0, + }; + + let native_key_code = event.physical_key.to_native_keycode(); + + let char_representation = event.logical_key.to_char_representation(); + #[allow(unused_mut)] + let mut character = char_representation as u16; + + if event.state == ElementState::Pressed && character != 0 { + kind = KeyEventKind::Char; + } + + // Mitigation for CEF on Mac bug to prevent NSMenu being triggered by this key event. + // + // CEF converts the key event into an `NSEvent` internally and passes that to Chromium. + // In some cases the `NSEvent` gets to the native Cocoa application, is considered "unhandled" and can trigger menus. + // + // Why mitigation works: + // Leaving `key_event.unmodified_character = 0` still leads to CEF forwarding a "unhandled" event to the native application + // but that event is discarded because `key_event.unmodified_character = 0` is considered non-printable and not used for shortcut matching. + // + // See https://github.com/chromiumembedded/cef/issues/3857 + // + // TODO: Remove mitigation once bug is fixed or a better solution is found. + #[cfg(not(target_os = "macos"))] + let unmodified_character = event.key_without_modifiers.to_char_representation() as u16; + #[cfg(target_os = "macos")] + let unmodified_character = 0; + + #[cfg(target_os = "macos")] // See https://www.magpcss.org/ceforum/viewtopic.php?start=10&t=11650 + if character == 0 && unmodified_character == 0 && event.text_with_all_modifiers.is_some() { + character = 1; + } + + let key = KeyData { + kind, + modifiers, + windows_key_code, + native_key_code, + character, + unmodified_character, + }; + + if kind == KeyEventKind::Char { + // CEF expects a raw key-down before the character event it produces. + vec![ + InputEvent::Key(KeyData { + kind: KeyEventKind::RawKeyDown, + ..key + }), + InputEvent::Key(KeyData { + windows_key_code: char_representation as i32, + ..key + }), + ] + } else { + vec![InputEvent::Key(key)] + } + } + WindowEvent::PinchGesture { delta, .. } => { + if !delta.is_normal() { + return Vec::new(); + } + + let data = MouseData { + modifiers: CefModifiers::PINCH_MODIFIERS.into(), + ..input_state.mouse_data() + }; + + vec![InputEvent::MouseWheel { + data, + delta_x: 0, + delta_y: (delta * PINCH_ZOOM_SPEED).round() as i32, + }] + } + _ => Vec::new(), + } +} + +/// Sends a translated [`InputEvent`] to the browser. Must run on the thread owning the browser. +pub(crate) fn apply(browser: &Browser, event: &InputEvent) { + let Some(host) = browser.host() else { return }; + match event { + InputEvent::MouseMove { data, leave } => { + host.send_mouse_move_event(Some(&data.into()), *leave as i32); + } + InputEvent::MouseClick { data, button, up, click_count } => { + let cef_button = cef::MouseButtonType::from(match button { + MouseButtonKind::Left => cef_mouse_button_type_t::MBT_LEFT, + MouseButtonKind::Right => cef_mouse_button_type_t::MBT_RIGHT, + MouseButtonKind::Middle => cef_mouse_button_type_t::MBT_MIDDLE, + }); + host.send_mouse_click_event(Some(&data.into()), cef_button, *up as i32, *click_count); + } + InputEvent::MouseWheel { data, delta_x, delta_y } => { + host.send_mouse_wheel_event(Some(&data.into()), *delta_x, *delta_y); + } + InputEvent::Key(key) => { + let key_event = KeyEvent { + type_: match key.kind { + KeyEventKind::RawKeyDown => cef_key_event_type_t::KEYEVENT_RAWKEYDOWN, + KeyEventKind::KeyUp => cef_key_event_type_t::KEYEVENT_KEYUP, + KeyEventKind::Char => cef_key_event_type_t::KEYEVENT_CHAR, + } + .into(), + modifiers: key.modifiers, + windows_key_code: key.windows_key_code, + native_key_code: key.native_key_code, + character: key.character, + unmodified_character: key.unmodified_character, + ..Default::default() + }; + host.send_key_event(Some(&key_event)); + } + } +} + +impl From<&MouseData> for MouseEvent { + fn from(data: &MouseData) -> Self { + MouseEvent { + x: data.x, + y: data.y, + modifiers: data.modifiers, + } + } +} diff --git a/desktop/src/cef/input/keymap.rs b/desktop/ui/src/input/keymap.rs similarity index 100% rename from desktop/src/cef/input/keymap.rs rename to desktop/ui/src/input/keymap.rs diff --git a/desktop/src/cef/input/state.rs b/desktop/ui/src/input/state.rs similarity index 90% rename from desktop/src/cef/input/state.rs rename to desktop/ui/src/input/state.rs index 45d3c9c7ac..8d6de8d167 100644 --- a/desktop/src/cef/input/state.rs +++ b/desktop/ui/src/input/state.rs @@ -1,11 +1,11 @@ -use cef::MouseEvent; use cef::sys::cef_event_flags_t; use std::time::Instant; use winit::dpi::PhysicalPosition; use winit::event::{ElementState, MouseButton}; use winit::keyboard::{Key, KeyLocation, ModifiersState, NamedKey}; -use crate::cef::consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT}; +use super::MouseData; +use crate::consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT}; #[derive(Default)] pub(crate) struct InputState { @@ -52,28 +52,12 @@ impl InputState { pub(crate) fn cef_mouse_modifiers(&self) -> CefModifiers { self.cef_modifiers(&KeyLocation::Standard, false) } -} -impl From for CefModifiers { - fn from(val: InputState) -> Self { - CefModifiers::new(&val, &KeyLocation::Standard, false) - } -} -impl From<&InputState> for MouseEvent { - fn from(val: &InputState) -> Self { - MouseEvent { - x: val.mouse_position.x as i32, - y: val.mouse_position.y as i32, - modifiers: val.cef_mouse_modifiers().into(), - } - } -} -impl From<&mut InputState> for MouseEvent { - fn from(val: &mut InputState) -> Self { - MouseEvent { - x: val.mouse_position.x as i32, - y: val.mouse_position.y as i32, - modifiers: val.cef_mouse_modifiers().into(), + pub(crate) fn mouse_data(&self) -> MouseData { + MouseData { + x: self.mouse_position.x as i32, + y: self.mouse_position.y as i32, + modifiers: self.cef_mouse_modifiers().into(), } } } diff --git a/desktop/src/cef/internal.rs b/desktop/ui/src/internal.rs similarity index 95% rename from desktop/src/cef/internal.rs rename to desktop/ui/src/internal.rs index bb8a8ec37f..c2194ef3eb 100644 --- a/desktop/src/cef/internal.rs +++ b/desktop/ui/src/internal.rs @@ -17,7 +17,6 @@ mod scheme_handler_factory; pub(super) mod render_handler; -#[cfg(not(target_os = "macos"))] pub(super) mod task; pub(super) use browser_process_app::BrowserProcessAppImpl; diff --git a/desktop/src/cef/internal/browser_process_app.rs b/desktop/ui/src/internal/browser_process_app.rs similarity index 86% rename from desktop/src/cef/internal/browser_process_app.rs rename to desktop/ui/src/internal/browser_process_app.rs index 7d98068236..d15f9cc681 100644 --- a/desktop/src/cef/internal/browser_process_app.rs +++ b/desktop/ui/src/internal/browser_process_app.rs @@ -3,31 +3,28 @@ use cef::sys::{_cef_app_t, cef_base_ref_counted_t}; use cef::{BrowserProcessHandler, CefString, ImplApp, ImplCommandLine, SchemeRegistrar, WrapApp}; use super::browser_process_handler::BrowserProcessHandlerImpl; -use super::scheme_handler_factory::SchemeHandlerFactoryImpl; -use crate::cef::CefEventHandler; +use super::scheme_handler_factory::register_schemes; -pub(crate) struct BrowserProcessAppImpl { +pub(crate) struct BrowserProcessAppImpl { object: *mut RcImpl<_cef_app_t, Self>, - event_handler: H, accelerated_paint: bool, } -impl BrowserProcessAppImpl { - pub(crate) fn new(event_handler: H, accelerated_paint: bool) -> Self { +impl BrowserProcessAppImpl { + pub(crate) fn new(accelerated_paint: bool) -> Self { Self { object: std::ptr::null_mut(), - event_handler, accelerated_paint, } } } -impl ImplApp for BrowserProcessAppImpl { +impl ImplApp for BrowserProcessAppImpl { fn browser_process_handler(&self) -> Option { - Some(BrowserProcessHandler::new(BrowserProcessHandlerImpl::new(self.event_handler.duplicate()))) + Some(BrowserProcessHandler::new(BrowserProcessHandlerImpl::new())) } fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { - SchemeHandlerFactoryImpl::::register_schemes(registrar); + register_schemes(registrar); } fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) { @@ -110,7 +107,7 @@ impl ImplApp for BrowserProcessAppImpl { } } -impl Clone for BrowserProcessAppImpl { +impl Clone for BrowserProcessAppImpl { fn clone(&self) -> Self { unsafe { let rc_impl = &mut *self.object; @@ -118,12 +115,11 @@ impl Clone for BrowserProcessAppImpl { } Self { object: self.object, - event_handler: self.event_handler.duplicate(), accelerated_paint: self.accelerated_paint, } } } -impl Rc for BrowserProcessAppImpl { +impl Rc for BrowserProcessAppImpl { fn as_base(&self) -> &cef_base_ref_counted_t { unsafe { let base = &*self.object; @@ -131,7 +127,7 @@ impl Rc for BrowserProcessAppImpl { } } } -impl WrapApp for BrowserProcessAppImpl { +impl WrapApp for BrowserProcessAppImpl { fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) { self.object = object; } diff --git a/desktop/src/cef/internal/browser_process_client.rs b/desktop/ui/src/internal/browser_process_client.rs similarity index 72% rename from desktop/src/cef/internal/browser_process_client.rs rename to desktop/ui/src/internal/browser_process_client.rs index f94aa58bf2..76475487b8 100644 --- a/desktop/src/cef/internal/browser_process_client.rs +++ b/desktop/ui/src/internal/browser_process_client.rs @@ -2,9 +2,9 @@ use cef::rc::{Rc, RcImpl}; use cef::sys::{_cef_client_t, cef_base_ref_counted_t}; use cef::{ContextMenuHandler, DisplayHandler, ImplClient, LifeSpanHandler, LoadHandler, RenderHandler, RequestHandler, WrapClient}; -use crate::cef::CefEventHandler; -use crate::cef::ipc::{MessageType, UnpackMessage, UnpackedMessage}; -use crate::wrapper::WgpuContext; +use crate::delegate::BrowserDelegate; +use crate::frames::FrameStreamer; +use crate::ipc::{MessageType, UnpackMessage, UnpackedMessage}; use super::context_menu_handler::ContextMenuHandlerImpl; use super::display_handler::DisplayHandlerImpl; @@ -13,28 +13,28 @@ use super::load_handler::LoadHandlerImpl; use super::render_handler::RenderHandlerImpl; use super::request_handler::RequestHandlerImpl; -pub(crate) struct BrowserProcessClientImpl { +pub(crate) struct BrowserProcessClientImpl { object: *mut RcImpl<_cef_client_t, Self>, - event_handler: H, + delegate: BrowserDelegate, load_handler: LoadHandler, render_handler: RenderHandler, display_handler: DisplayHandler, request_handler: RequestHandler, } -impl BrowserProcessClientImpl { - pub(crate) fn new(event_handler: &H, wgpu_context: WgpuContext) -> Self { +impl BrowserProcessClientImpl { + pub(crate) fn new(delegate: &BrowserDelegate, frames: FrameStreamer) -> Self { Self { object: std::ptr::null_mut(), - event_handler: event_handler.duplicate(), - load_handler: LoadHandler::new(LoadHandlerImpl::new(event_handler.duplicate())), - render_handler: RenderHandler::new(RenderHandlerImpl::new(event_handler.duplicate(), wgpu_context)), - display_handler: DisplayHandler::new(DisplayHandlerImpl::new(event_handler.duplicate())), + delegate: delegate.clone(), + load_handler: LoadHandler::new(LoadHandlerImpl::new(delegate.clone())), + render_handler: RenderHandler::new(RenderHandlerImpl::new(delegate.clone(), frames)), + display_handler: DisplayHandler::new(DisplayHandlerImpl::new(delegate.clone())), request_handler: RequestHandler::new(RequestHandlerImpl::new()), } } } -impl ImplClient for BrowserProcessClientImpl { +impl ImplClient for BrowserProcessClientImpl { fn on_process_message_received( &self, _browser: Option<&mut cef::Browser>, @@ -47,11 +47,11 @@ impl ImplClient for BrowserProcessClientImpl { Some(UnpackedMessage { message_type: MessageType::Initialized, data: _, - }) => self.event_handler.initialized_web_communication(), + }) => self.delegate.initialized_web_communication(), Some(UnpackedMessage { message_type: MessageType::SendToNative, data, - }) => self.event_handler.receive_web_message(data), + }) => self.delegate.receive_web_message(data), _ => { tracing::error!("Unexpected message type received in browser process"); @@ -90,7 +90,7 @@ impl ImplClient for BrowserProcessClientImpl { } } -impl Clone for BrowserProcessClientImpl { +impl Clone for BrowserProcessClientImpl { fn clone(&self) -> Self { unsafe { let rc_impl = &mut *self.object; @@ -98,7 +98,7 @@ impl Clone for BrowserProcessClientImpl { } Self { object: self.object, - event_handler: self.event_handler.duplicate(), + delegate: self.delegate.clone(), load_handler: self.load_handler.clone(), render_handler: self.render_handler.clone(), display_handler: self.display_handler.clone(), @@ -106,7 +106,7 @@ impl Clone for BrowserProcessClientImpl { } } } -impl Rc for BrowserProcessClientImpl { +impl Rc for BrowserProcessClientImpl { fn as_base(&self) -> &cef_base_ref_counted_t { unsafe { let base = &*self.object; @@ -114,7 +114,7 @@ impl Rc for BrowserProcessClientImpl { } } } -impl WrapClient for BrowserProcessClientImpl { +impl WrapClient for BrowserProcessClientImpl { fn wrap_rc(&mut self, object: *mut RcImpl<_cef_client_t, Self>) { self.object = object; } diff --git a/desktop/src/cef/internal/browser_process_handler.rs b/desktop/ui/src/internal/browser_process_handler.rs similarity index 52% rename from desktop/src/cef/internal/browser_process_handler.rs rename to desktop/ui/src/internal/browser_process_handler.rs index 3560483ba0..37bc6c1866 100644 --- a/desktop/src/cef/internal/browser_process_handler.rs +++ b/desktop/ui/src/internal/browser_process_handler.rs @@ -1,29 +1,17 @@ -use std::time::{Duration, Instant}; - use cef::rc::{Rc, RcImpl}; use cef::sys::{_cef_browser_process_handler_t, cef_base_ref_counted_t, cef_browser_process_handler_t}; use cef::{CefString, ImplBrowserProcessHandler, WrapBrowserProcessHandler}; -use crate::cef::CefEventHandler; - -pub(crate) struct BrowserProcessHandlerImpl { +pub(crate) struct BrowserProcessHandlerImpl { object: *mut RcImpl, - event_handler: H, } -impl BrowserProcessHandlerImpl { - pub(crate) fn new(event_handler: H) -> Self { - Self { - object: std::ptr::null_mut(), - event_handler, - } +impl BrowserProcessHandlerImpl { + pub(crate) fn new() -> Self { + Self { object: std::ptr::null_mut() } } } -impl ImplBrowserProcessHandler for BrowserProcessHandlerImpl { - fn on_schedule_message_pump_work(&self, delay_ms: i64) { - self.event_handler.schedule_cef_message_loop_work(Instant::now() + Duration::from_millis(delay_ms as u64)); - } - +impl ImplBrowserProcessHandler for BrowserProcessHandlerImpl { fn on_already_running_app_relaunch(&self, _command_line: Option<&mut cef::CommandLine>, _current_directory: Option<&CefString>) -> std::ffi::c_int { 1 // Return 1 to prevent default behavior of opening a empty browser window } @@ -33,19 +21,16 @@ impl ImplBrowserProcessHandler for BrowserProcessHandlerImpl } } -impl Clone for BrowserProcessHandlerImpl { +impl Clone for BrowserProcessHandlerImpl { fn clone(&self) -> Self { unsafe { let rc_impl = &mut *self.object; rc_impl.interface.add_ref(); } - Self { - object: self.object, - event_handler: self.event_handler.duplicate(), - } + Self { object: self.object } } } -impl Rc for BrowserProcessHandlerImpl { +impl Rc for BrowserProcessHandlerImpl { fn as_base(&self) -> &cef_base_ref_counted_t { unsafe { let base = &*self.object; @@ -53,7 +38,7 @@ impl Rc for BrowserProcessHandlerImpl { } } } -impl WrapBrowserProcessHandler for BrowserProcessHandlerImpl { +impl WrapBrowserProcessHandler for BrowserProcessHandlerImpl { fn wrap_rc(&mut self, object: *mut RcImpl<_cef_browser_process_handler_t, Self>) { self.object = object; } diff --git a/desktop/src/cef/internal/context_menu_handler.rs b/desktop/ui/src/internal/context_menu_handler.rs similarity index 100% rename from desktop/src/cef/internal/context_menu_handler.rs rename to desktop/ui/src/internal/context_menu_handler.rs diff --git a/desktop/src/cef/internal/display_handler.rs b/desktop/ui/src/internal/display_handler.rs similarity index 85% rename from desktop/src/cef/internal/display_handler.rs rename to desktop/ui/src/internal/display_handler.rs index bd99c29570..08e1c83184 100644 --- a/desktop/src/cef/internal/display_handler.rs +++ b/desktop/ui/src/internal/display_handler.rs @@ -3,18 +3,18 @@ use cef::sys::{_cef_display_handler_t, cef_base_ref_counted_t, cef_cursor_type_t use cef::{CefString, ImplDisplayHandler, Point, Size, WrapDisplayHandler}; use winit::cursor::CursorIcon; -use crate::cef::CefEventHandler; +use crate::delegate::BrowserDelegate; -pub(crate) struct DisplayHandlerImpl { +pub(crate) struct DisplayHandlerImpl { object: *mut RcImpl<_cef_display_handler_t, Self>, - event_handler: H, + delegate: BrowserDelegate, } -impl DisplayHandlerImpl { - pub fn new(event_handler: H) -> Self { +impl DisplayHandlerImpl { + pub fn new(delegate: BrowserDelegate) -> Self { Self { object: std::ptr::null_mut(), - event_handler, + delegate, } } } @@ -24,7 +24,7 @@ type CefCursorHandle = cef::CursorHandle; #[cfg(target_os = "macos")] type CefCursorHandle = *mut u8; -impl ImplDisplayHandler for DisplayHandlerImpl { +impl ImplDisplayHandler for DisplayHandlerImpl { fn on_cursor_change(&self, _browser: Option<&mut cef::Browser>, _cursor: CefCursorHandle, cursor_type: cef::CursorType, custom_cursor_info: Option<&cef::CursorInfo>) -> std::ffi::c_int { if let Some(custom_cursor_info) = custom_cursor_info { let Size { width, height } = custom_cursor_info.size; @@ -34,8 +34,13 @@ impl ImplDisplayHandler for DisplayHandlerImpl { if !buffer_ptr.is_null() && buffer_ptr.align_offset(std::mem::align_of::()) == 0 { let buffer = unsafe { std::slice::from_raw_parts(buffer_ptr, buffer_size) }.to_vec(); - let cursor = winit::cursor::CustomCursorSource::from_rgba(buffer, width as u16, height as u16, hotspot_x as u16, hotspot_y as u16).unwrap(); - self.event_handler.cursor_change(cursor.into()); + self.delegate.cursor_change(crate::Cursor::Custom { + rgba: buffer, + width: width as u16, + height: height as u16, + hotspot_x: hotspot_x as u16, + hotspot_y: hotspot_y as u16, + }); return 1; // We handled the cursor change. } } @@ -91,13 +96,13 @@ impl ImplDisplayHandler for DisplayHandlerImpl { CT_DND_LINK => CursorIcon::Alias, CT_NUM_VALUES => CursorIcon::Default, CT_NONE => { - self.event_handler.cursor_change(crate::window::Cursor::None); + self.delegate.cursor_change(crate::Cursor::None); return 1; // We handled the cursor change. } _ => CursorIcon::Default, }; - self.event_handler.cursor_change(cursor.into()); + self.delegate.cursor_change(cursor.into()); 1 // We handled the cursor change. } @@ -123,7 +128,7 @@ impl ImplDisplayHandler for DisplayHandlerImpl { } } -impl Clone for DisplayHandlerImpl { +impl Clone for DisplayHandlerImpl { fn clone(&self) -> Self { unsafe { let rc_impl = &mut *self.object; @@ -131,11 +136,11 @@ impl Clone for DisplayHandlerImpl { } Self { object: self.object, - event_handler: self.event_handler.duplicate(), + delegate: self.delegate.clone(), } } } -impl Rc for DisplayHandlerImpl { +impl Rc for DisplayHandlerImpl { fn as_base(&self) -> &cef_base_ref_counted_t { unsafe { let base = &*self.object; @@ -143,7 +148,7 @@ impl Rc for DisplayHandlerImpl { } } } -impl WrapDisplayHandler for DisplayHandlerImpl { +impl WrapDisplayHandler for DisplayHandlerImpl { fn wrap_rc(&mut self, object: *mut RcImpl<_cef_display_handler_t, Self>) { self.object = object; } diff --git a/desktop/src/cef/internal/life_span_handler.rs b/desktop/ui/src/internal/life_span_handler.rs similarity index 100% rename from desktop/src/cef/internal/life_span_handler.rs rename to desktop/ui/src/internal/life_span_handler.rs diff --git a/desktop/src/cef/internal/load_handler.rs b/desktop/ui/src/internal/load_handler.rs similarity index 65% rename from desktop/src/cef/internal/load_handler.rs rename to desktop/ui/src/internal/load_handler.rs index c23352aef3..36c8e642d7 100644 --- a/desktop/src/cef/internal/load_handler.rs +++ b/desktop/ui/src/internal/load_handler.rs @@ -2,24 +2,24 @@ use cef::rc::{Rc, RcImpl}; use cef::sys::{_cef_load_handler_t, cef_base_ref_counted_t, cef_load_handler_t}; use cef::{ImplBrowser, ImplBrowserHost, ImplLoadHandler, WrapLoadHandler}; -use crate::cef::CefEventHandler; +use crate::delegate::BrowserDelegate; -pub(crate) struct LoadHandlerImpl { +pub(crate) struct LoadHandlerImpl { object: *mut RcImpl, - event_handler: H, + delegate: BrowserDelegate, } -impl LoadHandlerImpl { - pub(crate) fn new(event_handler: H) -> Self { +impl LoadHandlerImpl { + pub(crate) fn new(delegate: BrowserDelegate) -> Self { Self { object: std::ptr::null_mut(), - event_handler, + delegate, } } } -impl ImplLoadHandler for LoadHandlerImpl { +impl ImplLoadHandler for LoadHandlerImpl { fn on_loading_state_change(&self, browser: Option<&mut cef::Browser>, is_loading: std::ffi::c_int, _can_go_back: std::ffi::c_int, _can_go_forward: std::ffi::c_int) { - let view_info = self.event_handler.view_info(); + let view_info = self.delegate.view_info(); if let Some(browser) = browser && is_loading == 0 @@ -33,7 +33,7 @@ impl ImplLoadHandler for LoadHandlerImpl { } } -impl Clone for LoadHandlerImpl { +impl Clone for LoadHandlerImpl { fn clone(&self) -> Self { unsafe { let rc_impl = &mut *self.object; @@ -41,11 +41,11 @@ impl Clone for LoadHandlerImpl { } Self { object: self.object, - event_handler: self.event_handler.duplicate(), + delegate: self.delegate.clone(), } } } -impl Rc for LoadHandlerImpl { +impl Rc for LoadHandlerImpl { fn as_base(&self) -> &cef_base_ref_counted_t { unsafe { let base = &*self.object; @@ -53,7 +53,7 @@ impl Rc for LoadHandlerImpl { } } } -impl WrapLoadHandler for LoadHandlerImpl { +impl WrapLoadHandler for LoadHandlerImpl { fn wrap_rc(&mut self, object: *mut RcImpl<_cef_load_handler_t, Self>) { self.object = object; } diff --git a/desktop/src/cef/internal/render_handler.rs b/desktop/ui/src/internal/render_handler.rs similarity index 58% rename from desktop/src/cef/internal/render_handler.rs rename to desktop/ui/src/internal/render_handler.rs index bb451ec8bb..86da6bf2bb 100644 --- a/desktop/src/cef/internal/render_handler.rs +++ b/desktop/ui/src/internal/render_handler.rs @@ -2,28 +2,28 @@ use cef::rc::{Rc, RcImpl}; use cef::sys::{_cef_render_handler_t, cef_base_ref_counted_t}; use cef::{Browser, ImplRenderHandler, PaintElementType, Rect, WrapRenderHandler}; -use crate::cef::{CefEventHandler, View}; -use crate::wrapper::WgpuContext; +use crate::delegate::BrowserDelegate; +use crate::frames::FrameStreamer; -pub(crate) struct RenderHandlerImpl { +pub(crate) struct RenderHandlerImpl { object: *mut RcImpl<_cef_render_handler_t, Self>, - event_handler: H, - view: View, + delegate: BrowserDelegate, + frames: FrameStreamer, } -impl RenderHandlerImpl { - pub(crate) fn new(event_handler: H, wgpu_context: WgpuContext) -> Self { +impl RenderHandlerImpl { + pub(crate) fn new(delegate: BrowserDelegate, frames: FrameStreamer) -> Self { Self { object: std::ptr::null_mut(), - event_handler, - view: View::new(wgpu_context), + delegate, + frames, } } } -impl ImplRenderHandler for RenderHandlerImpl { +impl ImplRenderHandler for RenderHandlerImpl { fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) { if let Some(rect) = rect { - let view_info = self.event_handler.view_info(); + let view_info = self.delegate.view_info(); *rect = Rect { x: 0, y: 0, @@ -33,7 +33,7 @@ impl ImplRenderHandler for RenderHandlerImpl { } } - fn on_paint(&self, _browser: Option<&mut Browser>, type_: PaintElementType, dirty_rects: Option<&[Rect]>, buffer: *const u8, width: std::ffi::c_int, height: std::ffi::c_int) { + fn on_paint(&self, _browser: Option<&mut Browser>, type_: PaintElementType, _dirty_rects: Option<&[Rect]>, buffer: *const u8, width: std::ffi::c_int, height: std::ffi::c_int) { if type_ != PaintElementType::default() { return; } @@ -41,8 +41,8 @@ impl ImplRenderHandler for RenderHandlerImpl { let buffer_size = (width * height * 4) as usize; let buffer_slice = unsafe { std::slice::from_raw_parts(buffer, buffer_size) }; - self.view.upload_frame_buffer(buffer_slice, width as u32, height as u32, dirty_rects.unwrap_or(&[])); - self.event_handler.draw(&self.view) + self.frames.stage_buffer(buffer_slice, width as u32, height as u32); + self.frames.publish(); } #[cfg(feature = "accelerated_paint")] @@ -51,8 +51,8 @@ impl ImplRenderHandler for RenderHandlerImpl { return; } - self.view.import_shared_texture(info.unwrap()); - self.event_handler.draw(&self.view) + self.frames.stage_texture(info.unwrap()); + self.frames.publish(); } fn get_raw(&self) -> *mut _cef_render_handler_t { @@ -60,7 +60,7 @@ impl ImplRenderHandler for RenderHandlerImpl { } } -impl Clone for RenderHandlerImpl { +impl Clone for RenderHandlerImpl { fn clone(&self) -> Self { unsafe { let rc_impl = &mut *self.object; @@ -68,12 +68,12 @@ impl Clone for RenderHandlerImpl { } Self { object: self.object, - event_handler: self.event_handler.duplicate(), - view: self.view.clone(), + delegate: self.delegate.clone(), + frames: self.frames.clone(), } } } -impl Rc for RenderHandlerImpl { +impl Rc for RenderHandlerImpl { fn as_base(&self) -> &cef_base_ref_counted_t { unsafe { let base = &*self.object; @@ -81,7 +81,7 @@ impl Rc for RenderHandlerImpl { } } } -impl WrapRenderHandler for RenderHandlerImpl { +impl WrapRenderHandler for RenderHandlerImpl { fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_handler_t, Self>) { self.object = object; } diff --git a/desktop/src/cef/internal/render_process_app.rs b/desktop/ui/src/internal/render_process_app.rs similarity index 70% rename from desktop/src/cef/internal/render_process_app.rs rename to desktop/ui/src/internal/render_process_app.rs index 300690d771..c1a9e45f29 100644 --- a/desktop/src/cef/internal/render_process_app.rs +++ b/desktop/ui/src/internal/render_process_app.rs @@ -3,14 +3,13 @@ use cef::sys::{_cef_app_t, cef_base_ref_counted_t}; use cef::{App, ImplApp, RenderProcessHandler, SchemeRegistrar, WrapApp}; use super::render_process_handler::RenderProcessHandlerImpl; -use super::scheme_handler_factory::SchemeHandlerFactoryImpl; -use crate::cef::CefEventHandler; +use super::scheme_handler_factory::register_schemes; -pub(crate) struct RenderProcessAppImpl { +pub(crate) struct RenderProcessAppImpl { object: *mut RcImpl<_cef_app_t, Self>, render_process_handler: RenderProcessHandler, } -impl RenderProcessAppImpl { +impl RenderProcessAppImpl { pub(crate) fn app() -> App { App::new(Self { object: std::ptr::null_mut(), @@ -19,9 +18,9 @@ impl RenderProcessAppImpl { } } -impl ImplApp for RenderProcessAppImpl { +impl ImplApp for RenderProcessAppImpl { fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { - SchemeHandlerFactoryImpl::::register_schemes(registrar); + register_schemes(registrar); } fn render_process_handler(&self) -> Option { @@ -33,7 +32,7 @@ impl ImplApp for RenderProcessAppImpl { } } -impl Clone for RenderProcessAppImpl { +impl Clone for RenderProcessAppImpl { fn clone(&self) -> Self { unsafe { let rc_impl = &mut *self.object; @@ -45,7 +44,7 @@ impl Clone for RenderProcessAppImpl { } } } -impl Rc for RenderProcessAppImpl { +impl Rc for RenderProcessAppImpl { fn as_base(&self) -> &cef_base_ref_counted_t { unsafe { let base = &*self.object; @@ -53,7 +52,7 @@ impl Rc for RenderProcessAppImpl { } } } -impl WrapApp for RenderProcessAppImpl { +impl WrapApp for RenderProcessAppImpl { fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) { self.object = object; } diff --git a/desktop/src/cef/internal/render_process_handler.rs b/desktop/ui/src/internal/render_process_handler.rs similarity index 98% rename from desktop/src/cef/internal/render_process_handler.rs rename to desktop/ui/src/internal/render_process_handler.rs index 58b0d1b0df..4af3e22217 100644 --- a/desktop/src/cef/internal/render_process_handler.rs +++ b/desktop/ui/src/internal/render_process_handler.rs @@ -2,7 +2,7 @@ use cef::rc::{ConvertReturnValue, Rc, RcImpl}; use cef::sys::{_cef_render_process_handler_t, cef_base_ref_counted_t, cef_render_process_handler_t, cef_v8_propertyattribute_t, cef_v8_value_create_array_buffer_with_copy}; use cef::{ImplFrame, ImplRenderProcessHandler, ImplV8Context, ImplV8Value, V8Handler, V8Propertyattribute, V8Value, WrapRenderProcessHandler, v8_value_create_function}; -use crate::cef::ipc::{MessageType, UnpackMessage, UnpackedMessage}; +use crate::ipc::{MessageType, UnpackMessage, UnpackedMessage}; use super::render_process_v8_handler::RenderProcessV8HandlerImpl; diff --git a/desktop/src/cef/internal/render_process_v8_handler.rs b/desktop/ui/src/internal/render_process_v8_handler.rs similarity index 97% rename from desktop/src/cef/internal/render_process_v8_handler.rs rename to desktop/ui/src/internal/render_process_v8_handler.rs index 2ff18e4703..3028bcc9ff 100644 --- a/desktop/src/cef/internal/render_process_v8_handler.rs +++ b/desktop/ui/src/internal/render_process_v8_handler.rs @@ -1,6 +1,6 @@ use cef::{ImplV8Handler, ImplV8Value, V8Value, WrapV8Handler, rc::Rc, v8_context_get_current_context}; -use crate::cef::ipc::{MessageType, SendMessage}; +use crate::ipc::{MessageType, SendMessage}; pub struct RenderProcessV8HandlerImpl { object: *mut cef::rc::RcImpl, diff --git a/desktop/src/cef/internal/request_handler.rs b/desktop/ui/src/internal/request_handler.rs similarity index 97% rename from desktop/src/cef/internal/request_handler.rs rename to desktop/ui/src/internal/request_handler.rs index c77147ff15..0a255b7d68 100644 --- a/desktop/src/cef/internal/request_handler.rs +++ b/desktop/ui/src/internal/request_handler.rs @@ -4,7 +4,7 @@ use cef::{AuthCallback, Browser, CefString, Frame, ImplRequest, ImplRequestHandl use std::ffi::c_int; use super::resource_request_handler::ResourceRequestHandlerImpl; -use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME}; +use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME}; pub(crate) struct RequestHandlerImpl { object: *mut RcImpl<_cef_request_handler_t, Self>, diff --git a/desktop/src/cef/internal/resource_handler.rs b/desktop/ui/src/internal/resource_handler.rs similarity index 98% rename from desktop/src/cef/internal/resource_handler.rs rename to desktop/ui/src/internal/resource_handler.rs index feb33b937d..25bb99d08f 100644 --- a/desktop/src/cef/internal/resource_handler.rs +++ b/desktop/ui/src/internal/resource_handler.rs @@ -5,7 +5,7 @@ use std::cell::RefCell; use std::ffi::c_int; use std::io::Read; -use crate::cef::{Resource, ResourceReader}; +use crate::resources::{Resource, ResourceReader}; pub(crate) struct ResourceHandlerImpl { object: *mut RcImpl<_cef_resource_handler_t, Self>, diff --git a/desktop/src/cef/internal/resource_request_handler.rs b/desktop/ui/src/internal/resource_request_handler.rs similarity index 96% rename from desktop/src/cef/internal/resource_request_handler.rs rename to desktop/ui/src/internal/resource_request_handler.rs index 94d8ec47e3..7be7acbbfa 100644 --- a/desktop/src/cef/internal/resource_request_handler.rs +++ b/desktop/ui/src/internal/resource_request_handler.rs @@ -2,7 +2,7 @@ use cef::rc::{Rc, RcImpl}; use cef::sys::{_cef_resource_request_handler_t, cef_base_ref_counted_t}; use cef::{Browser, Callback, CefString, Frame, ImplRequest, ImplResourceRequestHandler, Request, ReturnValue, WrapResourceRequestHandler}; -use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME}; +use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME}; // TODO: Deny all external requests once we stop relying on google fonts for font preview fn is_allowed_url(url: &str) -> bool { diff --git a/desktop/src/cef/internal/scheme_handler_factory.rs b/desktop/ui/src/internal/scheme_handler_factory.rs similarity index 54% rename from desktop/src/cef/internal/scheme_handler_factory.rs rename to desktop/ui/src/internal/scheme_handler_factory.rs index e74b8cede5..812252a4c2 100644 --- a/desktop/src/cef/internal/scheme_handler_factory.rs +++ b/desktop/ui/src/internal/scheme_handler_factory.rs @@ -3,41 +3,41 @@ use cef::sys::{_cef_scheme_handler_factory_t, cef_base_ref_counted_t, cef_scheme use cef::{Browser, CefString, Frame, ImplRequest, ImplSchemeHandlerFactory, ImplSchemeRegistrar, Request, ResourceHandler, SchemeRegistrar, WrapSchemeHandlerFactory}; use super::resource_handler::ResourceHandlerImpl; -use crate::cef::CefEventHandler; -use crate::cef::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME}; +use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME}; +use crate::delegate::BrowserDelegate; -pub(crate) struct SchemeHandlerFactoryImpl { +pub(crate) struct SchemeHandlerFactoryImpl { object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>, - event_handler: H, + delegate: BrowserDelegate, } -impl SchemeHandlerFactoryImpl { - pub(crate) fn new(event_handler: H) -> Self { +impl SchemeHandlerFactoryImpl { + pub(crate) fn new(delegate: BrowserDelegate) -> Self { Self { object: std::ptr::null_mut(), - event_handler, + delegate, } } +} - pub(crate) fn register_schemes(registrar: Option<&mut SchemeRegistrar>) { - if let Some(registrar) = registrar { - let mut scheme_options = 0; - scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32; - scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32; - scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32; - scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32; - registrar.add_custom_scheme(Some(&RESOURCE_SCHEME.into()), scheme_options); - } +pub(crate) fn register_schemes(registrar: Option<&mut SchemeRegistrar>) { + if let Some(registrar) = registrar { + let mut scheme_options = 0; + scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32; + scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32; + scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32; + scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32; + registrar.add_custom_scheme(Some(&RESOURCE_SCHEME.into()), scheme_options); } } -impl ImplSchemeHandlerFactory for SchemeHandlerFactoryImpl { +impl ImplSchemeHandlerFactory for SchemeHandlerFactoryImpl { fn create(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, _scheme_name: Option<&CefString>, request: Option<&mut Request>) -> Option { if let Some(request) = request { let url = CefString::from(&request.url()).to_string(); let path = url .strip_prefix(&format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/")) .expect("CEF should only call this for our custom scheme and domain that we registered this factory for"); - let resource = self.event_handler.load_resource(path.to_string().into()); + let resource = self.delegate.load_resource(path.to_string().into()); return Some(ResourceHandler::new(ResourceHandlerImpl::new(resource))); } None @@ -47,7 +47,7 @@ impl ImplSchemeHandlerFactory for SchemeHandlerFactoryImpl Clone for SchemeHandlerFactoryImpl { +impl Clone for SchemeHandlerFactoryImpl { fn clone(&self) -> Self { unsafe { let rc_impl = &mut *self.object; @@ -55,11 +55,11 @@ impl Clone for SchemeHandlerFactoryImpl { } Self { object: self.object, - event_handler: self.event_handler.duplicate(), + delegate: self.delegate.clone(), } } } -impl Rc for SchemeHandlerFactoryImpl { +impl Rc for SchemeHandlerFactoryImpl { fn as_base(&self) -> &cef_base_ref_counted_t { unsafe { let base = &*self.object; @@ -67,7 +67,7 @@ impl Rc for SchemeHandlerFactoryImpl { } } } -impl WrapSchemeHandlerFactory for SchemeHandlerFactoryImpl { +impl WrapSchemeHandlerFactory for SchemeHandlerFactoryImpl { fn wrap_rc(&mut self, object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>) { self.object = object; } diff --git a/desktop/src/cef/internal/task.rs b/desktop/ui/src/internal/task.rs similarity index 100% rename from desktop/src/cef/internal/task.rs rename to desktop/ui/src/internal/task.rs diff --git a/desktop/src/cef/ipc.rs b/desktop/ui/src/ipc.rs similarity index 100% rename from desktop/src/cef/ipc.rs rename to desktop/ui/src/ipc.rs diff --git a/desktop/ui/src/lib.rs b/desktop/ui/src/lib.rs new file mode 100644 index 0000000000..63d46cadb5 --- /dev/null +++ b/desktop/ui/src/lib.rs @@ -0,0 +1,255 @@ +use std::process::ExitCode; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{Receiver, RecvTimeoutError}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use crate::remote::messages::HostControlMessage; +use crate::remote::spawn::HostHandle; + +mod consts; +mod context; +mod delegate; +mod dirs; +mod events; +mod frames; +mod input; +mod internal; +mod ipc; +mod platform; +mod remote; +mod resources; +mod utility; +mod view; + +pub struct UiContext { + inner: S::ContextData, +} + +impl UiContext { + pub fn setup() -> UiSetupResult { + #[cfg(target_os = "macos")] + ipc_channel::set_bootstrap_prefix(consts::IPC_BOOTSTRAP_PREFIX); + + let raw_args: Vec = std::env::args().collect(); + if raw_args.iter().any(|arg| arg.starts_with(consts::BROWSER_HOST_CONFIG_FLAG)) { + remote::host::run(); + return UiSetupResult::Helper(ExitCode::SUCCESS); + } + + if raw_args.iter().any(|arg| arg.starts_with("--type=")) { + return UiSetupResult::Helper(run_helper()); + } + UiSetupResult::Ready(UiContext { inner: () }) + } + + pub fn start(self, config: UiConfig) -> Result, UiError> { + let acceleration = platform::accelerated_paint(matches!(config.acceleration, Acceleration::Disabled)); + let handle = remote::spawn::spawn_host(acceleration)?; + Ok(UiContext { inner: Arc::new(handle) }) + } +} + +#[must_use] +pub enum UiSetupResult { + Ready(UiContext), + Failed, + Helper(ExitCode), +} + +impl UiContext { + pub fn instance(&self, device: &wgpu::Device, queue: &wgpu_sync::Queue) -> Result { + let surface = frames::FrameSurface::new(device.clone(), queue.clone()); + + let (queue, events) = events::EventQueue::new(); + let shutdown_complete = remote::spawn::start_instance(&self.inner, surface, queue.clone())?; + + Ok(UiInstance { + inner: Arc::new(UiInstanceInner { + host: self.inner.clone(), + input: Mutex::new(input::InputState::default()), + events: Mutex::new(events), + queue, + shutdown_complete: Mutex::new(shutdown_complete), + shutdown_started: AtomicBool::new(false), + }), + }) + } +} + +impl Clone for UiContext { + fn clone(&self) -> Self { + UiContext { inner: self.inner.clone() } + } +} + +pub enum Setup {} +pub enum Started {} + +#[expect(private_bounds)] +pub trait Stage: Sealed {} +impl Stage for Setup {} +impl Stage for Started {} +trait Sealed { + type ContextData; +} +impl Sealed for Setup { + type ContextData = (); +} +impl Sealed for Started { + type ContextData = Arc; +} + +pub fn temp_dir_root() -> std::path::PathBuf { + dirs::app_tmp_dir() +} + +pub fn run_helper() -> ExitCode { + context::execute_helper_process() +} + +pub struct UiConfig { + pub acceleration: Acceleration, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Acceleration { + Auto, + Disabled, +} + +#[derive(thiserror::Error, Debug)] +#[non_exhaustive] +pub enum InitError {} + +#[derive(thiserror::Error, Debug)] +#[non_exhaustive] +pub enum UiError { + #[error("failed to bootstrap the UI backend: {0}")] + Bootstrap(String), + #[error("failed to spawn the UI backend host process: {0}")] + Spawn(std::io::Error), + #[error("the UI backend host process exited during startup: {0}")] + HostExited(String), + #[error("timed out waiting for the UI backend host process to connect")] + HandshakeTimeout, + #[error("UI backend handshake failed: {0}")] + Handshake(String), + #[error("the UI runtime already drives an instance")] + InstanceLimit, +} + +pub struct UiInstance { + inner: Arc, +} + +pub(crate) struct UiInstanceInner { + host: Arc, + input: Mutex, + events: Mutex>, + queue: events::EventQueue, + shutdown_complete: Mutex>, + shutdown_started: AtomicBool, +} + +impl UiInstance { + pub fn send(&self, command: UiCommand) { + let shared = &self.inner; + match command { + UiCommand::Input(event) => { + let events = { + let Ok(mut input) = shared.input.lock() else { + tracing::error!("Failed to lock the input state"); + return; + }; + input::translate(&mut input, &event) + }; + if !events.is_empty() { + shared.host.send(HostControlMessage::Input(events)); + } + } + UiCommand::Resized { width, height } => shared.host.send(HostControlMessage::UpdateViewInfo(view::ViewInfoUpdate::Size { width, height })), + UiCommand::ScaleChanged(scale) => shared.host.send(HostControlMessage::UpdateViewInfo(view::ViewInfoUpdate::Scale(scale))), + UiCommand::Refresh => shared.host.send(HostControlMessage::RefreshViewInfo), + UiCommand::Message(message) => shared.host.send(HostControlMessage::SendWebMessage(message)), + } + } + + pub fn recv(&self) -> Option { + let shared = &self.inner; + let Ok(receiver) = shared.events.lock() else { + return None; + }; + loop { + match receiver.recv_timeout(Duration::from_millis(100)) { + Ok(event) => return Some(event), + Err(RecvTimeoutError::Timeout) => { + if shared.queue.is_terminated() { + return receiver.try_recv().ok(); + } + } + Err(RecvTimeoutError::Disconnected) => return None, + } + } + } + + pub fn shutdown(&self) { + self.inner.shutdown(); + } +} + +impl Clone for UiInstance { + fn clone(&self) -> Self { + UiInstance { inner: self.inner.clone() } + } +} + +impl UiInstanceInner { + fn shutdown(&self) { + if self.shutdown_started.swap(true, Ordering::SeqCst) { + return; + } + if let Ok(receiver) = self.shutdown_complete.lock() { + self.host.shutdown(&receiver); + } + self.queue.mark_terminated(); + } +} + +impl Drop for UiInstanceInner { + fn drop(&mut self) { + self.shutdown(); + } +} + +#[derive(Debug)] +pub enum UiCommand { + Input(winit::event::WindowEvent), + Resized { width: u32, height: u32 }, + ScaleChanged(f64), + Refresh, + Message(Vec), +} + +#[derive(Debug)] +pub enum UiEvent { + Ready, + Frame(wgpu::Texture), + Cursor(Cursor), + Message(Vec), + InitFailed(String), + Crashed, +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub enum Cursor { + Icon(winit::cursor::CursorIcon), + Custom { rgba: Vec, width: u16, height: u16, hotspot_x: u16, hotspot_y: u16 }, + None, +} + +impl From for Cursor { + fn from(icon: winit::cursor::CursorIcon) -> Self { + Cursor::Icon(icon) + } +} diff --git a/desktop/src/cef/platform.rs b/desktop/ui/src/platform.rs similarity index 67% rename from desktop/src/cef/platform.rs rename to desktop/ui/src/platform.rs index 7df5d5f982..664952fae2 100644 --- a/desktop/src/cef/platform.rs +++ b/desktop/ui/src/platform.rs @@ -1,5 +1,24 @@ +#[cfg(target_os = "linux")] +pub(crate) mod linux; +#[cfg(target_os = "macos")] +pub(crate) mod mac; +#[cfg(target_os = "windows")] +pub(crate) mod win; + +pub(crate) fn accelerated_paint(disable_gpu_acceleration: bool) -> bool { + #[cfg(feature = "accelerated_paint")] + { + !disable_gpu_acceleration && should_enable_hardware_acceleration() + } + #[cfg(not(feature = "accelerated_paint"))] + { + let _ = disable_gpu_acceleration; + false + } +} + #[cfg(feature = "accelerated_paint")] -pub fn should_enable_hardware_acceleration() -> bool { +fn should_enable_hardware_acceleration() -> bool { #[cfg(target_os = "linux")] { // Check if running on Wayland or X11 @@ -18,11 +37,11 @@ pub fn should_enable_hardware_acceleration() -> bool { } // Check for NVIDIA proprietary driver (known to have issues) - if let Ok(driver_info) = std::fs::read_to_string("/proc/driver/nvidia/version") { - if driver_info.contains("NVIDIA") { - tracing::warn!("NVIDIA proprietary driver detected, hardware acceleration may be unstable"); - // Still return true but with warning - } + if let Ok(driver_info) = std::fs::read_to_string("/proc/driver/nvidia/version") + && driver_info.contains("NVIDIA") + { + tracing::warn!("NVIDIA proprietary driver detected, hardware acceleration may be unstable"); + // Still return true but with warning } // Check for basic GPU capabilities diff --git a/desktop/ui/src/platform/linux.rs b/desktop/ui/src/platform/linux.rs new file mode 100644 index 0000000000..b8e97b2b10 --- /dev/null +++ b/desktop/ui/src/platform/linux.rs @@ -0,0 +1,35 @@ +use std::os::unix::process::CommandExt; +use std::process::Command; + +#[cfg(feature = "accelerated_paint")] +use crate::frames::plane; + +pub(crate) fn setup_command(command: &mut Command, #[cfg(feature = "accelerated_paint")] host_frame_fd: Option) { + let parent_pid = std::process::id() as libc::pid_t; + // SAFETY: the closure runs in the forked child before exec and only makes async-signal-safe calls + unsafe { + command.pre_exec(move || { + // Tie the host's lifetime to the parent process + if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) != 0 { + return Err(std::io::Error::last_os_error()); + } + if libc::getppid() != parent_pid { + return Err(std::io::Error::other("main process died before PDEATHSIG was set")); + } + + // Move the host end of the frame socket to its advertised fd + #[cfg(feature = "accelerated_paint")] + if let Some(fd) = host_frame_fd { + let target = plane::FRAME_SOCKET_CHILD_FD; + if fd == target { + if libc::fcntl(target, libc::F_SETFD, 0) != 0 { + return Err(std::io::Error::last_os_error()); + } + } else if libc::dup2(fd, target) == -1 { + return Err(std::io::Error::last_os_error()); + } + } + Ok(()) + }); + } +} diff --git a/desktop/ui/src/platform/mac.rs b/desktop/ui/src/platform/mac.rs new file mode 100644 index 0000000000..a7d9dc6b8e --- /dev/null +++ b/desktop/ui/src/platform/mac.rs @@ -0,0 +1,63 @@ +use objc2::rc::Retained; +use objc2::runtime::Bool; +use objc2::{ClassType, define_class, msg_send}; +use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSEvent, NSResponder}; +use objc2_foundation::NSObject; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use cef::application_mac::{CefAppProtocol, CrAppControlProtocol, CrAppProtocol}; + +static HANDLING_SEND_EVENT: AtomicBool = AtomicBool::new(false); + +define_class!( + #[unsafe(super(NSApplication, NSResponder, NSObject))] + #[name = "GraphiteCefHostApplication"] + struct CefHostApplication; + + unsafe impl CrAppProtocol for CefHostApplication { + #[unsafe(method(isHandlingSendEvent))] + fn is_handling_send_event(&self) -> Bool { + Bool::new(HANDLING_SEND_EVENT.load(Ordering::Relaxed)) + } + } + + unsafe impl CrAppControlProtocol for CefHostApplication { + #[unsafe(method(setHandlingSendEvent:))] + fn set_handling_send_event(&self, handling: Bool) { + HANDLING_SEND_EVENT.store(handling.as_bool(), Ordering::Relaxed); + } + } + + unsafe impl CefAppProtocol for CefHostApplication {} + + impl CefHostApplication { + #[unsafe(method(sendEvent:))] + fn send_event(&self, event: &NSEvent) { + let was_handling = HANDLING_SEND_EVENT.swap(true, Ordering::Relaxed); + let _: () = unsafe { msg_send![super(self), sendEvent: event] }; + HANDLING_SEND_EVENT.store(was_handling, Ordering::Relaxed); + } + } +); + +pub(crate) fn install_application() { + let app: Retained = unsafe { msg_send![CefHostApplication::class(), sharedApplication] }; + app.setActivationPolicy(NSApplicationActivationPolicy::Prohibited); +} + +pub(crate) fn spawn_parent_watchdog(main_pid: u32) { + let result = std::thread::Builder::new().name("parent-watchdog".to_string()).spawn(move || { + loop { + // SAFETY: getppid is always safe to call. + if unsafe { libc::getppid() } as u32 != main_pid { + tracing::warn!("Main process is gone, exiting CEF host"); + std::process::exit(0); + } + std::thread::sleep(Duration::from_millis(500)); + } + }); + if let Err(e) = result { + tracing::error!("Failed to spawn the parent watchdog: {e}"); + } +} diff --git a/desktop/ui/src/platform/win.rs b/desktop/ui/src/platform/win.rs new file mode 100644 index 0000000000..617eb59543 --- /dev/null +++ b/desktop/ui/src/platform/win.rs @@ -0,0 +1,39 @@ +use windows::Win32::Foundation::{CloseHandle, HANDLE}; +use windows::Win32::System::JobObjects::{ + AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation, SetInformationJobObject, +}; +use windows::core::PCWSTR; + +pub(crate) struct KillOnCloseJob(HANDLE); + +// SAFETY: job object handles may be used and closed from any thread. +unsafe impl Send for KillOnCloseJob {} +unsafe impl Sync for KillOnCloseJob {} + +impl KillOnCloseJob { + pub(crate) fn assign(child: &std::process::Child) -> windows::core::Result { + use std::os::windows::io::AsRawHandle; + unsafe { + let job = CreateJobObjectW(None, PCWSTR::null())?; + let job = Self(job); + let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default(); + info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + SetInformationJobObject( + job.0, + JobObjectExtendedLimitInformation, + &info as *const _ as *const core::ffi::c_void, + std::mem::size_of::() as u32, + )?; + AssignProcessToJobObject(job.0, HANDLE(child.as_raw_handle()))?; + Ok(job) + } + } +} + +impl Drop for KillOnCloseJob { + fn drop(&mut self) { + unsafe { + let _ = CloseHandle(self.0); + } + } +} diff --git a/desktop/ui/src/remote.rs b/desktop/ui/src/remote.rs new file mode 100644 index 0000000000..89099a6d0f --- /dev/null +++ b/desktop/ui/src/remote.rs @@ -0,0 +1,34 @@ +use crate::consts::BROWSER_HOST_CONFIG_FLAG; + +pub(crate) mod host; +pub(crate) mod messages; +pub(crate) mod spawn; + +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) struct HostConfig { + pub(crate) server: String, + pub(crate) main_pid: u32, + pub(crate) acceleration: bool, + #[cfg(target_os = "linux")] + pub(crate) frame_socket_fd: Option, + #[cfg(target_os = "macos")] + pub(crate) frame_service: Option, +} + +impl HostConfig { + pub(crate) fn to_arg(&self) -> String { + let json = serde_json::to_string(self).expect("HostConfig always serializes"); + format!("{BROWSER_HOST_CONFIG_FLAG}{json}") + } + + pub(crate) fn from_args(args: &[String]) -> Option { + let json = args.iter().find_map(|arg| arg.strip_prefix(BROWSER_HOST_CONFIG_FLAG))?; + match serde_json::from_str(json) { + Ok(config) => Some(config), + Err(e) => { + tracing::error!("Malformed host config on the command line: {e}"); + None + } + } + } +} diff --git a/desktop/ui/src/remote/host.rs b/desktop/ui/src/remote/host.rs new file mode 100644 index 0000000000..2537a7bebb --- /dev/null +++ b/desktop/ui/src/remote/host.rs @@ -0,0 +1,118 @@ +use ipc_channel::ipc::{IpcReceiver, IpcSender}; +use std::sync::{Arc, Mutex}; + +use super::HostConfig; +use super::messages::{EventMessage, HostControlMessage}; +use crate::context::{CefContext, CefContextHandle}; +use crate::delegate::BrowserDelegate; +use crate::frames::FrameStreamer; +#[cfg(feature = "accelerated_paint")] +use crate::frames::plane::PlaneSender; +use crate::frames::sequence::SequenceState; +#[cfg(target_os = "macos")] +use crate::platform::mac; + +pub(crate) fn run() { + // Ignore SIGINT, the controlling process is responsible for shutting down the host + #[cfg(any(target_os = "linux", target_os = "macos"))] + unsafe { + libc::signal(libc::SIGINT, libc::SIG_IGN); + } + + let args: Vec = std::env::args().collect(); + let config = HostConfig::from_args(&args).expect("CEF host started without a valid host config argument"); + let acceleration_requested = config.acceleration; + + #[cfg(target_os = "macos")] + mac::spawn_parent_watchdog(config.main_pid); + + let bootstrap = IpcSender::::connect(config.server.clone()).expect("Failed to connect to the main process bootstrap server"); + let event_sender = Arc::new(Mutex::new(bootstrap)); + let (control_sender, control_receiver) = ipc_channel::ipc::channel::().expect("Failed to create control channel"); + + #[cfg(feature = "accelerated_paint")] + let plane = if acceleration_requested { PlaneSender::from_config(&config, event_sender.clone()) } else { None }; + #[cfg(feature = "accelerated_paint")] + let acceleration = plane.is_some(); + #[cfg(not(feature = "accelerated_paint"))] + let acceleration = { + if acceleration_requested { + tracing::error!("UI acceleration requested but the accelerated_paint feature is disabled; using software frames"); + } + false + }; + + event_sender + .lock() + .expect("The host message sender cannot be poisoned before threads exist") + .send(EventMessage::Hello { + pid: std::process::id(), + control_sender, + acceleration, + }) + .expect("Failed to send Hello to the main process"); + + let sequence = Arc::new(SequenceState::new()); + let frames = FrameStreamer::new( + event_sender.clone(), + sequence.clone(), + #[cfg(feature = "accelerated_paint")] + plane, + ); + let (view_info_sender, view_info_receiver) = std::sync::mpsc::channel(); + let delegate = BrowserDelegate::new(event_sender.clone(), view_info_receiver); + + let context = match CefContext::create(delegate, frames, view_info_sender, acceleration) { + Ok(context) => { + if let Ok(sender) = event_sender.lock() { + let _ = sender.send(EventMessage::BrowserCreated); + } + context + } + Err(e) => { + tracing::error!("CEF initialization failed in host process: {e}"); + if let Ok(sender) = event_sender.lock() { + let _ = sender.send(EventMessage::InitFailed(e)); + } + std::process::exit(1); + } + }; + + let outcome = context.run(move |handle| control_loop(&control_receiver, &handle, sequence.as_ref())); + + match outcome { + ControlOutcome::Shutdown => { + tracing::debug!("Shut down CEF host"); + if let Ok(sender) = event_sender.lock() { + let _ = sender.send(EventMessage::ShutdownComplete); + } + } + ControlOutcome::Disconnected => std::process::exit(0), + } + + #[cfg(target_os = "windows")] + std::process::exit(0); +} + +enum ControlOutcome { + Shutdown, + Disconnected, +} + +fn control_loop(receiver: &IpcReceiver, context: &CefContextHandle, sequence: &SequenceState) -> ControlOutcome { + loop { + match receiver.recv() { + Ok(HostControlMessage::Input(events)) => context.apply_input(events), + Ok(HostControlMessage::UpdateViewInfo(update)) => context.update_view_info(update), + Ok(HostControlMessage::RefreshViewInfo) => context.refresh_view_info(), + Ok(HostControlMessage::SendWebMessage(message)) => context.send_web_message(message), + Ok(HostControlMessage::FrameAck { seq }) => sequence.ack(seq), + Ok(HostControlMessage::Shutdown) => return ControlOutcome::Shutdown, + Err(ipc_channel::IpcError::Io(ref io)) if io.kind() == std::io::ErrorKind::Interrupted => {} + Err(e) => { + tracing::warn!("Control channel closed ({e:?}), shutting down CEF host"); + return ControlOutcome::Disconnected; + } + } + } +} diff --git a/desktop/ui/src/remote/messages.rs b/desktop/ui/src/remote/messages.rs new file mode 100644 index 0000000000..6c90d37f7b --- /dev/null +++ b/desktop/ui/src/remote/messages.rs @@ -0,0 +1,50 @@ +use ipc_channel::ipc::{IpcSender, IpcSharedMemory}; +use serde::{Deserialize, Serialize}; + +use crate::Cursor; +use crate::context::InitError; +use crate::input::InputEvent; +use crate::view::ViewInfoUpdate; + +#[derive(Serialize, Deserialize)] +pub(crate) enum HostControlMessage { + Input(Vec), + UpdateViewInfo(ViewInfoUpdate), + RefreshViewInfo, + SendWebMessage(Vec), + FrameAck { seq: u64 }, + Shutdown, +} + +#[derive(Serialize, Deserialize)] +pub(crate) enum EventMessage { + Hello { + pid: u32, + control_sender: IpcSender, + acceleration: bool, + }, + BrowserCreated, + InitFailed(InitError), + WebCommunicationInitialized, + WebMessage(Vec), + CursorChange(Cursor), + AdvertiseFrameSegment { + index: u32, + shm: IpcSharedMemory, + }, + SoftwareFrame { + seq: u64, + segment: u32, + width: u32, + height: u32, + }, + #[cfg(all(target_os = "windows", feature = "accelerated_paint"))] + AcceleratedFrame { + seq: u64, + handle: u64, + width: u32, + height: u32, + format: u32, + }, + ShutdownComplete, +} diff --git a/desktop/ui/src/remote/spawn.rs b/desktop/ui/src/remote/spawn.rs new file mode 100644 index 0000000000..a9103a3fae --- /dev/null +++ b/desktop/ui/src/remote/spawn.rs @@ -0,0 +1,388 @@ +use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender}; +use std::process::{Child, Command}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{self, RecvTimeoutError}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use super::HostConfig; +use super::messages::{EventMessage, HostControlMessage}; +use crate::consts::{HOST_HELLO_TIMEOUT, HOST_SHUTDOWN_TIMEOUT}; +use crate::events::EventQueue; +use crate::frames::FrameSurface; +#[cfg(feature = "accelerated_paint")] +use crate::frames::plane; +use crate::frames::receive::{FrameConsumer, PendingFrame, SegmentTable}; +#[cfg(any(target_os = "linux", target_os = "windows"))] +use crate::platform; +use crate::{UiError, UiEvent}; + +pub(crate) struct HostHandle { + sender: IpcSender, + receivers: Mutex>, + child: Arc>, + shutting_down: Arc, + died_reported: Arc, + #[cfg_attr(not(feature = "accelerated_paint"), expect(dead_code))] + host_acceleration: bool, + #[cfg(target_os = "windows")] + _job: Option, +} + +struct InstanceReceivers { + events: IpcReceiver, + #[cfg(all(any(target_os = "linux", target_os = "macos"), feature = "accelerated_paint"))] + frame_plane: Option, +} + +impl HostHandle { + pub(crate) fn send(&self, message: HostControlMessage) { + if let Err(e) = self.sender.send(message) { + tracing::debug!("Failed to send message to CEF host: {e}"); + } + } + + pub(crate) fn shutdown(&self, shutdown_complete_receiver: &mpsc::Receiver<()>) { + self.shutting_down.store(true, Ordering::SeqCst); + let deadline = Instant::now() + HOST_SHUTDOWN_TIMEOUT; + + if self.sender.send(HostControlMessage::Shutdown).is_ok() { + match shutdown_complete_receiver.recv_timeout(HOST_SHUTDOWN_TIMEOUT) { + Ok(()) => tracing::debug!("CEF host completed shutdown"), + Err(RecvTimeoutError::Timeout) => tracing::warn!("Timed out waiting for the CEF host to shut down"), + Err(RecvTimeoutError::Disconnected) => tracing::debug!("CEF host connection closed during shutdown"), + } + } + + loop { + match self.child.lock() { + Ok(mut child) => match child.try_wait() { + Ok(None) => {} + Ok(Some(_)) | Err(_) => return, + }, + Err(_) => return, + } + if Instant::now() >= deadline { + break; + } + std::thread::sleep(Duration::from_millis(25)); + } + + tracing::warn!("CEF host did not exit in time, killing it"); + if let Ok(mut child) = self.child.lock() { + let _ = child.kill(); + let _ = child.wait(); + } + } +} + +impl Drop for HostHandle { + fn drop(&mut self) { + if !self.shutting_down.load(Ordering::SeqCst) { + let _ = self.sender.send(HostControlMessage::Shutdown); + } + } +} + +pub(crate) fn spawn_host(acceleration: bool) -> Result { + let (server, server_name) = IpcOneShotServer::::new().map_err(|e| UiError::Bootstrap(format!("failed to create the bootstrap server: {e}")))?; + + let executable = std::env::current_exe().map_err(|e| UiError::Bootstrap(format!("failed to get the current executable path: {e}")))?; + let mut command = Command::new(executable); + + #[cfg_attr(not(all(any(target_os = "linux", target_os = "macos"), feature = "accelerated_paint")), expect(unused_mut))] + let mut config = HostConfig { + server: server_name, + main_pid: std::process::id(), + acceleration, + #[cfg(target_os = "linux")] + frame_socket_fd: None, + #[cfg(target_os = "macos")] + frame_service: None, + }; + + #[cfg(all(target_os = "linux", feature = "accelerated_paint"))] + let frame_socket = if acceleration { + match plane::socketpair() { + Ok((main_end, host_end)) => { + config.frame_socket_fd = Some(plane::FRAME_SOCKET_CHILD_FD); + Some((main_end, host_end)) + } + Err(e) => { + tracing::error!("Failed to create the accelerated frame socket, falling back to software frames: {e}"); + None + } + } + } else { + None + }; + + #[cfg(all(target_os = "macos", feature = "accelerated_paint"))] + let frame_service = if acceleration { + let name = format!("art.graphite.Graphite.cef-frames.{}.{:x}", std::process::id(), rand::random::()); + match plane::create_service(&name) { + Ok(port) => { + config.frame_service = Some(name); + Some(port) + } + Err(e) => { + tracing::error!("Failed to create the accelerated frame service, falling back to software frames: {e}"); + None + } + } + } else { + None + }; + + command.arg(config.to_arg()); + + #[cfg(target_os = "linux")] + platform::linux::setup_command( + &mut command, + #[cfg(feature = "accelerated_paint")] + frame_socket.as_ref().map(|(_, host_end)| { + use std::os::fd::AsRawFd; + host_end.as_raw_fd() + }), + ); + + let mut child = command.spawn().map_err(UiError::Spawn)?; + + #[cfg(target_os = "windows")] + let job = match platform::win::KillOnCloseJob::assign(&child) { + Ok(job) => Some(job), + Err(e) => { + tracing::error!("Failed to assign the CEF host to a job object (orphan prevention degraded): {e}"); + None + } + }; + + #[cfg(all(target_os = "linux", feature = "accelerated_paint"))] + let frame_plane = frame_socket.map(|(main_end, host_end)| { + drop(host_end); + plane::PlaneReceiver::new(main_end) + }); + + let (hello_sender, hello_receiver) = mpsc::channel(); + std::thread::Builder::new() + .name("cef-host-accept".to_string()) + .spawn(move || { + let _ = hello_sender.send(server.accept()); + }) + .expect("Failed to spawn CEF host accept thread"); + + let deadline = Instant::now() + HOST_HELLO_TIMEOUT; + let (event_receiver, hello) = loop { + match hello_receiver.recv_timeout(Duration::from_millis(100)) { + Ok(Ok(accepted)) => break accepted, + Ok(Err(e)) => { + let _ = child.kill(); + return Err(UiError::Handshake(format!("failed to accept the host connection: {e}"))); + } + Err(RecvTimeoutError::Timeout) => { + if let Ok(Some(status)) = child.try_wait() { + return Err(UiError::HostExited(status.to_string())); + } + if Instant::now() >= deadline { + let _ = child.kill(); + return Err(UiError::HandshakeTimeout); + } + } + Err(RecvTimeoutError::Disconnected) => { + let _ = child.kill(); + return Err(UiError::Handshake("the accept thread disappeared".to_string())); + } + } + }; + + let EventMessage::Hello { + pid, + control_sender, + acceleration: host_acceleration, + } = hello + else { + let _ = child.kill(); + return Err(UiError::Handshake("the first message from the host was not Hello".to_string())); + }; + tracing::info!("CEF host process connected (pid {pid})"); + if acceleration && !host_acceleration { + tracing::warn!("UI acceleration was requested but the CEF host could not set up its frame plane; falling back to software frames"); + } + + Ok(HostHandle { + sender: control_sender, + receivers: Mutex::new(Some(InstanceReceivers { + events: event_receiver, + #[cfg(all(target_os = "linux", feature = "accelerated_paint"))] + frame_plane, + #[cfg(all(target_os = "macos", feature = "accelerated_paint"))] + frame_plane: frame_service.map(plane::PlaneReceiver::new), + })), + child: Arc::new(Mutex::new(child)), + shutting_down: Arc::new(AtomicBool::new(false)), + died_reported: Arc::new(AtomicBool::new(false)), + host_acceleration, + #[cfg(target_os = "windows")] + _job: job, + }) +} + +pub(crate) fn start_instance(handle: &HostHandle, surface: FrameSurface, events: EventQueue) -> Result, UiError> { + let receive_side = match handle.receivers.lock() { + Ok(mut receive_side) => receive_side.take(), + Err(_) => None, + }; + let Some(receive_side) = receive_side else { + return Err(UiError::InstanceLimit); + }; + + let (shutdown_complete_sender, shutdown_complete_receiver) = mpsc::channel(); + let consumer = FrameConsumer::new(surface, events.clone(), handle.sender.clone()); + + #[cfg(all(any(target_os = "linux", target_os = "macos"), feature = "accelerated_paint"))] + if let Some(receiver) = receive_side.frame_plane + && handle.host_acceleration + { + let consumer = consumer.clone(); + std::thread::Builder::new() + .name("cef-frames".to_string()) + .spawn(move || crate::frames::receive::plane_receiver_loop(receiver, consumer)) + .expect("Failed to spawn CEF frame receiver thread"); + } + + { + let receiver = receive_side.events; + let shutting_down = handle.shutting_down.clone(); + let died_reported = handle.died_reported.clone(); + let events = events.clone(); + std::thread::Builder::new() + .name("cef-host".to_string()) + .spawn(move || event_receiver_loop(receiver, consumer, events, shutting_down, died_reported, shutdown_complete_sender)) + .expect("Failed to spawn CEF host event receiver thread"); + } + + { + let child = handle.child.clone(); + let shutting_down = handle.shutting_down.clone(); + let died_reported = handle.died_reported.clone(); + let events = events.clone(); + std::thread::Builder::new() + .name("cef-host-supervisor".to_string()) + .spawn(move || { + loop { + let status = match child.lock() { + Ok(mut child) => child.try_wait(), + Err(_) => return, + }; + match status { + Ok(None) => std::thread::sleep(Duration::from_millis(100)), + Ok(Some(status)) => { + if shutting_down.load(Ordering::SeqCst) { + tracing::debug!("CEF host exited during shutdown: {status}"); + } else { + report_host_died(&died_reported, &events, &format!("CEF host process exited unexpectedly: {status}")); + } + return; + } + Err(_) => return, + } + } + }) + .expect("Failed to spawn CEF host supervisor thread"); + } + + Ok(shutdown_complete_receiver) +} + +fn report_host_died(died_reported: &AtomicBool, events: &EventQueue, message: &str) { + if died_reported.swap(true, Ordering::SeqCst) { + return; + } + tracing::error!("{message}"); + events.terminate(UiEvent::Crashed); +} + +fn event_receiver_loop( + receiver: IpcReceiver, + consumer: FrameConsumer, + events: EventQueue, + shutting_down: Arc, + died_reported: Arc, + shutdown_complete_sender: mpsc::Sender<()>, +) { + let mut newest_frame: Option = None; + let mut segments = SegmentTable::new(); + + loop { + let message = match receiver.recv() { + Ok(message) => message, + Err(ipc_channel::IpcError::Io(ref io)) if io.kind() == std::io::ErrorKind::Interrupted => continue, + Err(e) => { + if !shutting_down.load(Ordering::SeqCst) { + report_host_died(&died_reported, &events, &format!("Lost connection to the CEF host process: {e:?}")); + } + return; + } + }; + handle_message(message, &events, &shutting_down, &died_reported, &shutdown_complete_sender, &mut newest_frame, &mut segments); + + loop { + match receiver.try_recv() { + Ok(message) => handle_message(message, &events, &shutting_down, &died_reported, &shutdown_complete_sender, &mut newest_frame, &mut segments), + Err(ipc_channel::TryRecvError::Empty) => break, + Err(ipc_channel::TryRecvError::IpcError(ipc_channel::IpcError::Io(ref io))) if io.kind() == std::io::ErrorKind::Interrupted => break, + Err(ipc_channel::TryRecvError::IpcError(e)) => { + if !shutting_down.load(Ordering::SeqCst) { + report_host_died(&died_reported, &events, &format!("Lost connection to the CEF host process: {e:?}")); + } + return; + } + } + } + + if let Some(frame) = newest_frame.take() { + consumer.deliver_pending(frame, &segments); + } + } +} + +fn handle_message( + message: EventMessage, + events: &EventQueue, + shutting_down: &AtomicBool, + died_reported: &AtomicBool, + shutdown_complete_sender: &mpsc::Sender<()>, + newest_frame: &mut Option, + segments: &mut SegmentTable, +) { + match message { + EventMessage::Hello { .. } => tracing::error!("Unexpected second Hello from the CEF host"), + EventMessage::BrowserCreated => tracing::info!("CEF host created the browser"), + EventMessage::InitFailed(e) => { + tracing::error!("CEF initialization failed in the host process: {e}"); + died_reported.store(true, Ordering::SeqCst); + events.terminate(UiEvent::InitFailed(e.to_string())); + } + EventMessage::WebCommunicationInitialized => events.send(UiEvent::Ready), + EventMessage::WebMessage(message) => events.send(UiEvent::Message(message)), + EventMessage::CursorChange(cursor) => events.send(UiEvent::Cursor(cursor)), + EventMessage::AdvertiseFrameSegment { index, shm } => segments.advertise(index, shm), + EventMessage::SoftwareFrame { seq, segment, width, height } => { + let frame = PendingFrame::Software { seq, segment, width, height }; + if newest_frame.as_ref().is_none_or(|newest| newest.seq() < frame.seq()) { + *newest_frame = Some(frame); + } + } + #[cfg(all(target_os = "windows", feature = "accelerated_paint"))] + EventMessage::AcceleratedFrame { seq, handle, width, height, format } => { + let frame = PendingFrame::Accelerated(plane::WireFrame::new(seq, handle, width, height, format)); + if newest_frame.as_ref().is_none_or(|newest| newest.seq() < frame.seq()) { + *newest_frame = Some(frame); + } + } + EventMessage::ShutdownComplete => { + shutting_down.store(true, Ordering::SeqCst); + let _ = shutdown_complete_sender.send(()); + } + } +} diff --git a/desktop/ui/src/resources.rs b/desktop/ui/src/resources.rs new file mode 100644 index 0000000000..4e638d14b8 --- /dev/null +++ b/desktop/ui/src/resources.rs @@ -0,0 +1,101 @@ +use std::fs::File; +#[cfg(feature = "embedded_resources")] +use std::io; +use std::io::Read; +use std::path::PathBuf; +use std::sync::Arc; + +#[derive(Clone)] +pub(crate) struct Resource { + pub(crate) reader: ResourceReader, + pub(crate) mimetype: Option, +} + +#[derive(Clone)] +pub(crate) enum ResourceReader { + #[cfg(feature = "embedded_resources")] + Embedded(io::Cursor<&'static [u8]>), + File(Arc), +} +impl Read for ResourceReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + match self { + #[cfg(feature = "embedded_resources")] + ResourceReader::Embedded(cursor) => cursor.read(buf), + ResourceReader::File(file) => file.as_ref().read(buf), + } + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub enum WebResources { + Embedded, + External(PathBuf), +} + +pub(crate) fn load(path: PathBuf) -> Option { + let resources = if cfg!(feature = "embedded_resources") { + WebResources::Embedded + } else { + let path = std::env::var("GRAPHITE_RESOURCES").expect("GRAPHITE_RESOURCES must point to the frontend assets when embedded resources are disabled"); + WebResources::External(path.into()) + }; + + let path = if path.as_os_str().is_empty() { PathBuf::from("index.html") } else { path }; + + let mimetype = match path.extension().and_then(|s| s.to_str()).unwrap_or("") { + "html" => Some("text/html".to_string()), + "css" => Some("text/css".to_string()), + "txt" => Some("text/plain".to_string()), + "wasm" => Some("application/wasm".to_string()), + "js" => Some("application/javascript".to_string()), + "png" => Some("image/png".to_string()), + "jpg" | "jpeg" => Some("image/jpeg".to_string()), + "svg" => Some("image/svg+xml".to_string()), + "xml" => Some("application/xml".to_string()), + "json" => Some("application/json".to_string()), + "ico" => Some("image/x-icon".to_string()), + "woff" => Some("font/woff".to_string()), + "woff2" => Some("font/woff2".to_string()), + "ttf" => Some("font/ttf".to_string()), + "otf" => Some("font/otf".to_string()), + "webmanifest" => Some("application/manifest+json".to_string()), + "graphite" => Some("application/graphite+json".to_string()), + _ => None, + }; + + match resources { + WebResources::Embedded => { + #[cfg(feature = "embedded_resources")] + { + if let Some(resources) = &graphite_desktop_embedded_resources::EMBEDDED_RESOURCES + && let Some(file) = resources.get_file(&path) + { + return Some(Resource { + reader: ResourceReader::Embedded(io::Cursor::new(file.contents())), + mimetype, + }); + } + None + } + #[cfg(not(feature = "embedded_resources"))] + { + tracing::error!("Embedded resources requested but the embedded_resources feature is disabled"); + None + } + } + WebResources::External(dir) => { + let file_path = dir.join(path.strip_prefix("/").unwrap_or(&path)); + if file_path.exists() + && file_path.is_file() + && let Ok(file) = std::fs::File::open(file_path) + { + return Some(Resource { + reader: ResourceReader::File(file.into()), + mimetype, + }); + } + None + } + } +} diff --git a/desktop/src/cef/utility.rs b/desktop/ui/src/utility.rs similarity index 100% rename from desktop/src/cef/utility.rs rename to desktop/ui/src/utility.rs diff --git a/desktop/ui/src/view.rs b/desktop/ui/src/view.rs new file mode 100644 index 0000000000..3f0e36d263 --- /dev/null +++ b/desktop/ui/src/view.rs @@ -0,0 +1,70 @@ +use std::sync::mpsc::Receiver; + +#[derive(Clone, Copy)] +pub(crate) struct ViewInfo { + width: u32, + height: u32, + scale: f64, +} + +impl ViewInfo { + pub(crate) fn new() -> Self { + Self { width: 1, height: 1, scale: 1. } + } + + pub(crate) fn apply_update(&mut self, update: ViewInfoUpdate) { + match update { + ViewInfoUpdate::Size { width, height } if width > 0 && height > 0 => { + self.width = width; + self.height = height; + } + ViewInfoUpdate::Scale(scale) if scale > 0. => { + self.scale = scale; + } + _ => {} + } + } + + pub(crate) fn zoom(&self) -> f64 { + self.scale.ln() / 1.2_f64.ln() + } + + pub(crate) fn width(&self) -> u32 { + self.width + } + + pub(crate) fn height(&self) -> u32 { + self.height + } +} + +impl Default for ViewInfo { + fn default() -> Self { + Self::new() + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub(crate) enum ViewInfoUpdate { + Size { width: u32, height: u32 }, + Scale(f64), +} + +pub(super) struct ViewInfoReceiver { + view_info: ViewInfo, + receiver: Receiver, +} + +impl ViewInfoReceiver { + pub(super) fn new(receiver: Receiver) -> Self { + Self { view_info: ViewInfo::new(), receiver } + } + + /// Apply all pending updates and return the resulting view info. + pub(super) fn current(&mut self) -> ViewInfo { + for update in self.receiver.try_iter() { + self.view_info.apply_update(update); + } + self.view_info + } +} From 9b8919047f4fdfe23fe36a8c671179a88d048d7d Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 10 Jul 2026 00:52:06 +0000 Subject: [PATCH 2/6] Review --- desktop/ui/src/frames/plane/win.rs | 32 +++++++++++++++--------------- desktop/ui/src/platform/linux.rs | 6 +++--- desktop/ui/src/platform/mac.rs | 4 ++-- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/desktop/ui/src/frames/plane/win.rs b/desktop/ui/src/frames/plane/win.rs index e52e9e8a61..a4c1dcc998 100644 --- a/desktop/ui/src/frames/plane/win.rs +++ b/desktop/ui/src/frames/plane/win.rs @@ -8,13 +8,13 @@ use crate::frames::surface::FrameSurface; use crate::remote::HostConfig; use crate::remote::messages::EventMessage; -struct ParentProcess(HANDLE); +struct MainProcess(HANDLE); // SAFETY: process handles may be used and closed from any thread. -unsafe impl Send for ParentProcess {} -unsafe impl Sync for ParentProcess {} +unsafe impl Send for MainProcess {} +unsafe impl Sync for MainProcess {} -impl ParentProcess { +impl MainProcess { fn open(pid: u32) -> windows::core::Result { // SAFETY: plain OpenProcess call; on success the handle is ours to close. unsafe { OpenProcess(PROCESS_DUP_HANDLE, false, pid).map(Self) } @@ -27,7 +27,7 @@ impl ParentProcess { Ok(target.0 as u64) } - fn close_in_parent(&self, handle: u64) { + fn close_in_main(&self, handle: u64) { let mut reclaimed = HANDLE::default(); // SAFETY: `handle` came from `duplicate_into` and is valid. unsafe { @@ -41,7 +41,7 @@ impl ParentProcess { } } -impl Drop for ParentProcess { +impl Drop for MainProcess { fn drop(&mut self) { // SAFETY: we own the process handle. unsafe { @@ -51,14 +51,14 @@ impl Drop for ParentProcess { } pub(crate) struct PlaneSender { - parent: Arc, + main: Arc, events: Arc>>, } impl PlaneSender { pub(crate) fn from_config(config: &HostConfig, events: Arc>>) -> Option { - match ParentProcess::open(config.main_pid) { - Ok(parent) => Some(Self { parent: Arc::new(parent), events }), + match MainProcess::open(config.main_pid) { + Ok(main) => Some(Self { main: Arc::new(main), events }), Err(e) => { tracing::error!("Failed to open the main process for handle duplication, falling back to software frames: {e}"); None @@ -67,7 +67,7 @@ impl PlaneSender { } pub(crate) fn stage(&self, info: &cef::AcceleratedPaintInfo) -> Option { - let handle = match self.parent.duplicate_into(HANDLE(info.shared_texture_handle)) { + let handle = match self.main.duplicate_into(HANDLE(info.shared_texture_handle)) { Ok(handle) => handle, Err(e) => { tracing::error!("Failed to duplicate the shared texture handle into the main process: {e}"); @@ -75,7 +75,7 @@ impl PlaneSender { } }; Some(StagedFrame { - handle: HandleInParent { handle, parent: self.parent.clone() }, + handle: HandleInMain { handle, main: self.main.clone() }, width: info.extra.coded_size.width as u32, height: info.extra.coded_size.height as u32, format: *info.format.as_ref() as u32, @@ -103,20 +103,20 @@ impl PlaneSender { } pub(crate) struct StagedFrame { - handle: HandleInParent, + handle: HandleInMain, width: u32, height: u32, format: u32, } -struct HandleInParent { +struct HandleInMain { handle: u64, - parent: Arc, + main: Arc, } -impl Drop for HandleInParent { +impl Drop for HandleInMain { fn drop(&mut self) { - self.parent.close_in_parent(self.handle); + self.main.close_in_main(self.handle); } } diff --git a/desktop/ui/src/platform/linux.rs b/desktop/ui/src/platform/linux.rs index b8e97b2b10..2c77159e4f 100644 --- a/desktop/ui/src/platform/linux.rs +++ b/desktop/ui/src/platform/linux.rs @@ -5,15 +5,15 @@ use std::process::Command; use crate::frames::plane; pub(crate) fn setup_command(command: &mut Command, #[cfg(feature = "accelerated_paint")] host_frame_fd: Option) { - let parent_pid = std::process::id() as libc::pid_t; + let main_pid = std::process::id() as libc::pid_t; // SAFETY: the closure runs in the forked child before exec and only makes async-signal-safe calls unsafe { command.pre_exec(move || { - // Tie the host's lifetime to the parent process + // Tie the host's lifetime to the main process if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) != 0 { return Err(std::io::Error::last_os_error()); } - if libc::getppid() != parent_pid { + if libc::getppid() != main_pid { return Err(std::io::Error::other("main process died before PDEATHSIG was set")); } diff --git a/desktop/ui/src/platform/mac.rs b/desktop/ui/src/platform/mac.rs index a7d9dc6b8e..394bd0a067 100644 --- a/desktop/ui/src/platform/mac.rs +++ b/desktop/ui/src/platform/mac.rs @@ -51,13 +51,13 @@ pub(crate) fn spawn_parent_watchdog(main_pid: u32) { loop { // SAFETY: getppid is always safe to call. if unsafe { libc::getppid() } as u32 != main_pid { - tracing::warn!("Main process is gone, exiting CEF host"); + tracing::warn!("Parent process is gone, exiting..."); std::process::exit(0); } std::thread::sleep(Duration::from_millis(500)); } }); if let Err(e) = result { - tracing::error!("Failed to spawn the parent watchdog: {e}"); + tracing::error!("Failed to spawn parent watchdog: {e}"); } } From f5266306494f142d68696699b4f5110bc43e75a0 Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 10 Jul 2026 01:04:12 +0000 Subject: [PATCH 3/6] Review --- desktop/ui/src/view.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/desktop/ui/src/view.rs b/desktop/ui/src/view.rs index 3f0e36d263..9e7ad2d195 100644 --- a/desktop/ui/src/view.rs +++ b/desktop/ui/src/view.rs @@ -60,7 +60,6 @@ impl ViewInfoReceiver { Self { view_info: ViewInfo::new(), receiver } } - /// Apply all pending updates and return the resulting view info. pub(super) fn current(&mut self) -> ViewInfo { for update in self.receiver.try_iter() { self.view_info.apply_update(update); From 3ac0d819e74595175b88531c43646cc73efc874d Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 10 Jul 2026 02:20:00 +0000 Subject: [PATCH 4/6] Review --- desktop/src/lib.rs | 65 ++++++++++--------- desktop/ui/src/context.rs | 77 ++++++++++++++++------- desktop/ui/src/dirs.rs | 4 +- desktop/ui/src/frames/import/d3d11.rs | 21 ++----- desktop/ui/src/frames/import/dmabuf.rs | 64 +++++++++++++------ desktop/ui/src/frames/plane/linux.rs | 10 ++- desktop/ui/src/frames/plane/mac.rs | 18 ++++-- desktop/ui/src/frames/plane/win.rs | 11 +++- desktop/ui/src/frames/surface.rs | 2 +- desktop/ui/src/input.rs | 12 +++- desktop/ui/src/input/state.rs | 16 ++--- desktop/ui/src/internal/render_handler.rs | 6 +- desktop/ui/src/lib.rs | 4 -- desktop/ui/src/platform/linux.rs | 4 +- desktop/ui/src/remote/host.rs | 4 ++ desktop/ui/src/remote/spawn.rs | 28 +++++---- desktop/ui/src/resources.rs | 16 ++++- 17 files changed, 232 insertions(+), 130 deletions(-) diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index 7292fa85d9..0666d93d65 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -94,36 +94,47 @@ pub fn start() -> ExitCode { } let acceleration = if prefs.disable_ui_acceleration { Acceleration::Disabled } else { Acceleration::Auto }; - let ui_context = ui_context.start(UiConfig { acceleration }).unwrap_or_else(|error| panic!("Failed to start the UI runtime: {error}")); - let ui = ui_context - .instance(&wgpu_context.device, &wgpu_context.queue) - .unwrap_or_else(|error| panic!("Failed to start the UI: {error}")); + let ui_context = match ui_context.start(UiConfig { acceleration }) { + Ok(context) => context, + Err(error) => { + tracing::error!("Failed to start the UI runtime: {error}"); + return ExitCode::FAILURE; + } + }; + let ui = match ui_context.instance(&wgpu_context.device, &wgpu_context.queue) { + Ok(ui) => ui, + Err(error) => { + tracing::error!("Failed to start the UI: {error}"); + return ExitCode::FAILURE; + } + }; tracing::info!("UI runtime started successfully"); { let ui = ui.clone(); let scheduler = app_event_scheduler.clone(); - std::thread::Builder::new() - .name("ui-events".to_string()) - .spawn(move || { - while let Some(event) = ui.recv() { - match event { - UiEvent::Ready => scheduler.schedule(AppEvent::WebCommunicationInitialized), - UiEvent::Frame(texture) => scheduler.schedule(AppEvent::UiUpdate(texture)), - UiEvent::Cursor(cursor) => scheduler.schedule(AppEvent::CursorChange(cursor)), - UiEvent::Message(message) => match wrapper::deserialize_editor_message(&message) { - Some(message) => scheduler.schedule(AppEvent::DesktopWrapperMessage(message)), - None => tracing::error!("Failed to deserialize web message"), - }, - UiEvent::InitFailed(error) => { - tracing::error!("UI initialization failed: {error}"); - scheduler.schedule(AppEvent::UiCrashed); - } - UiEvent::Crashed => scheduler.schedule(AppEvent::UiCrashed), + let spawned = std::thread::Builder::new().name("ui-events".to_string()).spawn(move || { + while let Some(event) = ui.recv() { + match event { + UiEvent::Ready => scheduler.schedule(AppEvent::WebCommunicationInitialized), + UiEvent::Frame(texture) => scheduler.schedule(AppEvent::UiUpdate(texture)), + UiEvent::Cursor(cursor) => scheduler.schedule(AppEvent::CursorChange(cursor)), + UiEvent::Message(message) => match wrapper::deserialize_editor_message(&message) { + Some(message) => scheduler.schedule(AppEvent::DesktopWrapperMessage(message)), + None => tracing::error!("Failed to deserialize web message"), + }, + UiEvent::InitFailed(error) => { + tracing::error!("UI initialization failed: {error}"); + scheduler.schedule(AppEvent::UiCrashed); } + UiEvent::Crashed => scheduler.schedule(AppEvent::UiCrashed), } - }) - .expect("Failed to spawn the UI event bridge thread"); + } + }); + if let Err(error) = spawned { + tracing::error!("Failed to spawn the UI event bridge thread: {error}"); + return ExitCode::FAILURE; + } } let app = App::new(ui.clone(), wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli.files); @@ -158,14 +169,6 @@ pub fn start() -> ExitCode { _ => {} } - // Workaround for a Windows-specific exception that occurs when `app` is dropped. - // The issue causes the window to hang for a few seconds before closing. - // Appears to be related to CEF object destruction order. - // Calling `exit` bypasses rust teardown and lets Windows perform process cleanup. - // TODO: Identify and fix the underlying CEF shutdown issue so this workaround can be removed. - #[cfg(target_os = "windows")] - std::process::exit(0); - #[cfg(not(target_os = "windows"))] ExitCode::SUCCESS } diff --git a/desktop/ui/src/context.rs b/desktop/ui/src/context.rs index b98085814e..c54005c185 100644 --- a/desktop/ui/src/context.rs +++ b/desktop/ui/src/context.rs @@ -69,18 +69,27 @@ impl CefContext { let control_thread = std::thread::Builder::new() .name("cef-host-control".to_string()) .spawn(move || { - let result = control(CefContextHandle); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| control(CefContextHandle))); with_context(|context| { - context.browser.host().unwrap().close_browser(1); + if let Some(host) = context.browser.host() { + host.close_browser(1); + } }); run_on_ui_thread(cef::quit_message_loop); - let _ = result_sender.send(result); + match result { + Ok(result) => { + let _ = result_sender.send(result); + } + Err(panic) => std::panic::resume_unwind(panic), + } }) .expect("Failed to spawn the CEF control thread"); cef::run_message_loop(); drop(CONTEXT.take()); cef::shutdown(); - let _ = control_thread.join(); + if let Err(panic) = control_thread.join() { + std::panic::resume_unwind(panic); + } result_receiver.recv().expect("The CEF control thread ended without a result") } } @@ -95,11 +104,12 @@ pub(crate) fn execute_helper_process() -> std::process::ExitCode { fn bootstrap(helper: bool) -> Args { #[cfg(target_os = "macos")] - let _loader = { + { let loader = cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), helper); assert!(loader.load()); - loader - }; + // LibraryLoader unloads the framework on drop + std::mem::forget(loader); + } #[cfg(not(target_os = "macos"))] let _ = helper; @@ -109,13 +119,13 @@ fn bootstrap(helper: bool) -> Args { fn initialize(args: &Args, instance_dir: &Path, accelerated_paint: bool) -> Result<(), InitError> { let mut app = App::new(BrowserProcessAppImpl::new(accelerated_paint)); - if cef::initialize(Some(args.as_main_args()), Some(&platform_settings(instance_dir)), Some(&mut app), std::ptr::null_mut()) != 1 { + if cef::initialize(Some(args.as_main_args()), Some(&platform_settings(instance_dir)?), Some(&mut app), std::ptr::null_mut()) != 1 { return Err(InitError::InitializationFailureCode(cef::get_exit_code() as u32)); } Ok(()) } -fn platform_settings(instance_dir: &Path) -> Settings { +fn platform_settings(instance_dir: &Path) -> Result { let log_severity = match std::env::var("GRAPHITE_BROWSER_LOG").unwrap_or_default().to_lowercase().as_str() { "debug" => cef_log_severity_t::LOGSEVERITY_VERBOSE, "info" => cef_log_severity_t::LOGSEVERITY_INFO, @@ -125,9 +135,12 @@ fn platform_settings(instance_dir: &Path) -> Settings { _ => cef_log_severity_t::LOGSEVERITY_FATAL, }; + let Some(root_cache_path) = instance_dir.to_str().map(CefString::from) else { + return Err(InitError::PathResolutionFailed(format!("non-UTF-8 instance directory path: {}", instance_dir.display()))); + }; let base = Settings { windowless_rendering_enabled: 1, - root_cache_path: instance_dir.to_str().map(CefString::from).unwrap(), + root_cache_path, cache_path: "".into(), disable_signal_handlers: 1, log_severity: LogSeverity::from(log_severity), @@ -136,24 +149,31 @@ fn platform_settings(instance_dir: &Path) -> Settings { #[cfg(target_os = "macos")] { - let exe = std::env::current_exe().expect("cannot get current exe path"); - let app_root = exe.parent().and_then(|p| p.parent()).expect("bad path structure").parent().expect("bad path structure"); - Settings { - main_bundle_path: app_root.to_str().map(CefString::from).unwrap(), + let exe = std::env::current_exe().map_err(|e| InitError::PathResolutionFailed(format!("cannot get current exe path: {e}")))?; + let app_root = exe + .parent() + .and_then(|p| p.parent()) + .and_then(|p| p.parent()) + .ok_or_else(|| InitError::PathResolutionFailed(format!("executable is not inside an app bundle: {}", exe.display())))?; + let Some(main_bundle_path) = app_root.to_str().map(CefString::from) else { + return Err(InitError::PathResolutionFailed(format!("invalid app bundle path: {}", app_root.display()))); + }; + Ok(Settings { + main_bundle_path, multi_threaded_message_loop: 0, external_message_pump: 0, no_sandbox: 1, // GPU helper crashes when running with sandbox ..base - } + }) } #[cfg(not(target_os = "macos"))] - Settings { + Ok(Settings { multi_threaded_message_loop: 1, #[cfg(target_os = "linux")] no_sandbox: 1, ..base - } + }) } fn create_browser(delegate: BrowserDelegate, frames: FrameStreamer, view_info_sender: Sender, instance_dir: TempDir, accelerated_paint: bool) -> Result { @@ -187,7 +207,9 @@ fn create_browser(delegate: BrowserDelegate, frames: FrameStreamer, view_info_se let mut scheme_handler_factory = SchemeHandlerFactory::new(SchemeHandlerFactoryImpl::new(delegate.clone())); incognito_request_context.clear_scheme_handler_factories(); - incognito_request_context.register_scheme_handler_factory(Some(&RESOURCE_SCHEME.into()), Some(&RESOURCE_DOMAIN.into()), Some(&mut scheme_handler_factory)); + if incognito_request_context.register_scheme_handler_factory(Some(&RESOURCE_SCHEME.into()), Some(&RESOURCE_DOMAIN.into()), Some(&mut scheme_handler_factory)) != 1 { + return Err(InitError::SchemeHandlerRegistrationFailed); + } let url = format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/"); browser_host_create_browser_sync( @@ -220,6 +242,10 @@ pub(crate) enum InitError { BrowserCreationFailed, #[error("Request context creation failed")] RequestContextCreationFailed, + #[error("Failed to resolve a required path: {0}")] + PathResolutionFailed(String), + #[error("Scheme handler registration failed")] + SchemeHandlerRegistrationFailed, } #[derive(Clone)] @@ -261,7 +287,10 @@ impl BrowserContext { fn refresh_view_info(&self) { let view_info = self.delegate.view_info(); - let host = self.browser.host().unwrap(); + let Some(host) = self.browser.host() else { + tracing::error!("Browser host is not available, cannot refresh view info"); + return; + }; host.set_zoom_level(view_info.zoom()); host.was_resized(); @@ -278,7 +307,11 @@ impl BrowserContext { impl Drop for BrowserContext { fn drop(&mut self) { tracing::debug!("Shutting down CEF"); - self.browser.host().unwrap().close_browser(1); + if let Some(host) = self.browser.host() { + host.close_browser(1); + } else { + tracing::error!("Browser host is not available, cannot close browser"); + } } } @@ -298,7 +331,9 @@ where { let closure_task = ClosureTask::new(closure); let mut task = Task::new(closure_task); - post_task(ThreadId::from(cef_thread_id_t::TID_UI), Some(&mut task)); + if post_task(ThreadId::from(cef_thread_id_t::TID_UI), Some(&mut task)) != 1 { + tracing::error!("Failed to post a task to the CEF UI thread"); + } } fn with_context(closure: F) diff --git a/desktop/ui/src/dirs.rs b/desktop/ui/src/dirs.rs index 1bef4d549f..f4e8f2ff63 100644 --- a/desktop/ui/src/dirs.rs +++ b/desktop/ui/src/dirs.rs @@ -9,8 +9,8 @@ const APP_DIRECTORY_NAME: &str = "Graphite"; pub(crate) fn app_tmp_dir() -> PathBuf { let path = std::env::temp_dir().join(APP_DIRECTORY_NAME); - if !path.exists() { - fs::create_dir_all(&path).unwrap_or_else(|_| panic!("Failed to create directory at {path:?}")); + if let Err(e) = fs::create_dir_all(&path) { + tracing::error!("Failed to create temp directory at {path:?}: {e}"); } path } diff --git a/desktop/ui/src/frames/import/d3d11.rs b/desktop/ui/src/frames/import/d3d11.rs index 1702470e52..64bf574a25 100644 --- a/desktop/ui/src/frames/import/d3d11.rs +++ b/desktop/ui/src/frames/import/d3d11.rs @@ -1,6 +1,7 @@ use super::{TextureImportError, TextureImportResult, TextureImporter, texture_descriptor, wgpu_format}; use cef::sys::cef_color_type_t; use std::os::raw::c_void; +use wgpu::hal::api; pub struct D3D11Importer { pub handle: *mut c_void, @@ -15,16 +16,11 @@ impl TextureImporter for D3D11Importer { return Err(TextureImportError::InvalidHandle("Null D3D11 shared texture handle".to_string())); } - if is_d3d12_backend(device) { - match self.import_via_d3d12(device) { - Ok(texture) => { - tracing::trace!("Successfully imported D3D11 shared texture via D3D12"); - return Ok(texture); - } - Err(e) => { - tracing::warn!("Failed to import D3D11 via D3D12: {}, trying Vulkan fallback", e); - } - } + let is_d3d12_backend = unsafe { device.as_hal::().is_some() }; + + if is_d3d12_backend { + let texture = self.import_via_d3d12(device)?; + return Ok(texture); } let texture = self.import_via_vulkan(device)?; @@ -138,8 +134,3 @@ impl D3D11Importer { } } } - -fn is_d3d12_backend(device: &wgpu::Device) -> bool { - use wgpu::hal::api; - unsafe { device.as_hal::().is_some() } -} diff --git a/desktop/ui/src/frames/import/dmabuf.rs b/desktop/ui/src/frames/import/dmabuf.rs index a2cd5e5f33..5a72376cea 100644 --- a/desktop/ui/src/frames/import/dmabuf.rs +++ b/desktop/ui/src/frames/import/dmabuf.rs @@ -15,9 +15,19 @@ pub struct DmaBufImporter { impl TextureImporter for DmaBufImporter { fn import_to_wgpu(&self, device: &wgpu::Device) -> TextureImportResult { - if self.fds.is_empty() { - return Err(TextureImportError::InvalidHandle("No DMA-BUF plane fds".to_string())); + if self.fds.len() != 1 { + return Err(TextureImportError::InvalidHandle(format!("Expected exactly one DMA-BUF plane fd, got {}", self.fds.len()))); } + + if self.strides.len() != self.fds.len() || self.offsets.len() != self.fds.len() { + return Err(TextureImportError::InvalidHandle(format!( + "DMA-BUF plane count mismatch: {} fds, {} strides, {} offsets", + self.fds.len(), + self.strides.len(), + self.offsets.len() + ))); + } + let texture = self.import_via_vulkan(device)?; tracing::trace!("Successfully imported DMA-BUF texture via Vulkan"); Ok(texture) @@ -81,7 +91,7 @@ impl DmaBufImporter { fn create_vulkan_image_from_dmabuf(&self, hal_device: &::Device) -> Result<(vk::Image, vk::DeviceMemory), TextureImportError> { let device = hal_device.raw_device(); - let _instance = hal_device.shared_instance().raw_instance(); + let instance = hal_device.shared_instance().raw_instance(); if self.width == 0 || self.height == 0 { return Err(TextureImportError::InvalidHandle("Invalid DMA-BUF dimensions".to_string())); @@ -102,13 +112,25 @@ impl DmaBufImporter { .usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_SRC) .sharing_mode(vk::SharingMode::EXCLUSIVE); - // Set up DRM format modifier - let plane_layouts = self.create_subresource_layouts(); + let plane_layouts = self + .offsets + .iter() + .zip(&self.strides) + .map(|(&offset, &stride)| vk::SubresourceLayout { + offset: offset as u64, + size: 0, // Will be calculated by driver + row_pitch: stride as u64, + array_pitch: 0, + depth_pitch: 0, + }) + .collect::>(); let mut drm_format_modifier = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default() .drm_format_modifier(self.modifier) .plane_layouts(&plane_layouts); - let image_create_info = image_create_info.push_next(&mut drm_format_modifier); + let mut external_memory_info = vk::ExternalMemoryImageCreateInfo::default().handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT); + + let image_create_info = image_create_info.push_next(&mut drm_format_modifier).push_next(&mut external_memory_info); let image = unsafe { device.create_image(&image_create_info, None).map_err(|e| TextureImportError::VulkanError { @@ -128,11 +150,25 @@ impl DmaBufImporter { }); } + let external_memory_fd = ash::khr::external_memory_fd::Device::new(instance, device); + let mut fd_properties = vk::MemoryFdPropertiesKHR::default(); + if let Err(e) = unsafe { external_memory_fd.get_memory_fd_properties(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT, dup_fd, &mut fd_properties) } { + // SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd + unsafe { + device.destroy_image(image, None); + libc::close(dup_fd); + } + return Err(TextureImportError::VulkanError { + operation: format!("Failed to query DMA-BUF fd memory properties: {e:?}"), + }); + } + let mut import_memory_fd = vk::ImportMemoryFdInfoKHR::default().handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT).fd(dup_fd); - let memory_properties = unsafe { hal_device.shared_instance().raw_instance().get_physical_device_memory_properties(hal_device.raw_physical_device()) }; + let memory_properties = unsafe { instance.get_physical_device_memory_properties(hal_device.raw_physical_device()) }; - let Some(memory_type_index) = find_memory_type_index(memory_requirements.memory_type_bits, vk::MemoryPropertyFlags::empty(), &memory_properties) else { + let compatible_type_bits = memory_requirements.memory_type_bits & fd_properties.memory_type_bits; + let Some(memory_type_index) = find_memory_type_index(compatible_type_bits, vk::MemoryPropertyFlags::empty(), &memory_properties) else { // SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd unsafe { device.destroy_image(image, None); @@ -175,18 +211,6 @@ impl DmaBufImporter { Ok((image, device_memory)) } - - fn create_subresource_layouts(&self) -> Vec { - (0..self.fds.len()) - .map(|i| vk::SubresourceLayout { - offset: self.offsets.get(i).copied().unwrap_or(0) as u64, - size: 0, // Will be calculated by driver - row_pitch: self.strides.get(i).copied().unwrap_or(0) as u64, - array_pitch: 0, - depth_pitch: 0, - }) - .collect() - } } fn vulkan_format(format: cef_color_type_t) -> Result { diff --git a/desktop/ui/src/frames/plane/linux.rs b/desktop/ui/src/frames/plane/linux.rs index 0d4b730142..2ae0d879d4 100644 --- a/desktop/ui/src/frames/plane/linux.rs +++ b/desktop/ui/src/frames/plane/linux.rs @@ -54,6 +54,12 @@ impl PlaneSender { } pub(crate) fn stage(&self, info: &cef::AcceleratedPaintInfo) -> Option { + let coded_size = &info.extra.coded_size; + if coded_size.width <= 0 || coded_size.height <= 0 { + tracing::error!("Accelerated paint delivered an invalid coded size: {}x{}", coded_size.width, coded_size.height); + return None; + } + let plane_count = (info.plane_count.max(0) as usize).min(info.planes.len()); let mut fds = Vec::with_capacity(plane_count); let mut strides = [0u32; 4]; @@ -76,8 +82,8 @@ impl PlaneSender { descriptor: FrameDescriptor { seq: 0, modifier: info.modifier, - width: info.extra.coded_size.width as u32, - height: info.extra.coded_size.height as u32, + width: coded_size.width as u32, + height: coded_size.height as u32, format: *info.format.as_ref() as u32, plane_count: plane_count as u32, strides, diff --git a/desktop/ui/src/frames/plane/mac.rs b/desktop/ui/src/frames/plane/mac.rs index 26126f3d47..92c5c98287 100644 --- a/desktop/ui/src/frames/plane/mac.rs +++ b/desktop/ui/src/frames/plane/mac.rs @@ -110,10 +110,17 @@ impl PlaneSender { } pub(crate) fn stage(&self, info: &cef::AcceleratedPaintInfo) -> Option { + let coded_size = &info.extra.coded_size; + if coded_size.width <= 0 || coded_size.height <= 0 { + tracing::error!("Accelerated paint delivered an invalid coded size: {}x{}", coded_size.width, coded_size.height); + return None; + } + let Some(surface) = std::ptr::NonNull::new(info.shared_texture_io_surface.cast::()) else { tracing::error!("Accelerated paint delivered a null IOSurface"); return None; }; + // SAFETY: CEF keeps the surface valid for the `on_accelerated_paint` callback. let port = unsafe { surface.as_ref() }.create_mach_port(); if port == MACH_PORT_NULL { @@ -123,8 +130,8 @@ impl PlaneSender { Some(StagedFrame { descriptor: FrameDescriptor { seq: 0, - width: info.extra.coded_size.width as u32, - height: info.extra.coded_size.height as u32, + width: coded_size.width as u32, + height: coded_size.height as u32, format: *info.format.as_ref() as u32, _pad: 0, }, @@ -155,9 +162,6 @@ impl PlaneSender { descriptor, }; - // Kernel takes ownership of the surface and moves it to the receiver. We must not drop. - std::mem::forget(frame.surface); - // SAFETY: message is a well-formed complex message of the declared size. let result = unsafe { mach_msg( @@ -173,6 +177,10 @@ impl PlaneSender { if result != MACH_MSG_SUCCESS { return Err(std::io::Error::other(format!("mach_msg send failed: {result:#x}"))); } + + // Kernel took ownership of the surface and moves it to the receiver. We must not drop. + std::mem::forget(frame.surface); + Ok(()) } } diff --git a/desktop/ui/src/frames/plane/win.rs b/desktop/ui/src/frames/plane/win.rs index a4c1dcc998..b3146dfaa1 100644 --- a/desktop/ui/src/frames/plane/win.rs +++ b/desktop/ui/src/frames/plane/win.rs @@ -67,6 +67,12 @@ impl PlaneSender { } pub(crate) fn stage(&self, info: &cef::AcceleratedPaintInfo) -> Option { + let coded_size = &info.extra.coded_size; + if coded_size.width <= 0 || coded_size.height <= 0 { + tracing::error!("Accelerated paint delivered an invalid coded size: {}x{}", coded_size.width, coded_size.height); + return None; + } + let handle = match self.main.duplicate_into(HANDLE(info.shared_texture_handle)) { Ok(handle) => handle, Err(e) => { @@ -74,10 +80,11 @@ impl PlaneSender { return None; } }; + Some(StagedFrame { handle: HandleInMain { handle, main: self.main.clone() }, - width: info.extra.coded_size.width as u32, - height: info.extra.coded_size.height as u32, + width: coded_size.width as u32, + height: coded_size.height as u32, format: *info.format.as_ref() as u32, }) } diff --git a/desktop/ui/src/frames/surface.rs b/desktop/ui/src/frames/surface.rs index 4c148df16f..045bada723 100644 --- a/desktop/ui/src/frames/surface.rs +++ b/desktop/ui/src/frames/surface.rs @@ -40,7 +40,7 @@ impl FrameSurface { view_formats: &[], })); } - let texture = slot.as_ref().expect("Texture was just created"); + let texture = slot.as_ref()?; self.queue.write_texture( wgpu::TexelCopyTextureInfo { diff --git a/desktop/ui/src/input.rs b/desktop/ui/src/input.rs index 43f8709fbc..b1c6e40d15 100644 --- a/desktop/ui/src/input.rs +++ b/desktop/ui/src/input.rs @@ -55,7 +55,7 @@ pub(crate) struct KeyData { /// (cursor position, click counting, modifiers) along the way. pub(crate) fn translate(input_state: &mut InputState, event: &WindowEvent) -> Vec { match event { - WindowEvent::PointerMoved { position, .. } | WindowEvent::PointerEntered { position, .. } => { + WindowEvent::PointerMoved { position, .. } => { if !input_state.cursor_move(position) { return Vec::new(); } @@ -64,6 +64,13 @@ pub(crate) fn translate(input_state: &mut InputState, event: &WindowEvent) -> Ve leave: false, }] } + WindowEvent::PointerEntered { position, .. } => { + let _ = input_state.cursor_move(position); + vec![InputEvent::MouseMove { + data: input_state.mouse_data(), + leave: false, + }] + } WindowEvent::PointerLeft { position, .. } => { if let Some(position) = position { let _ = input_state.cursor_move(position); @@ -73,7 +80,7 @@ pub(crate) fn translate(input_state: &mut InputState, event: &WindowEvent) -> Ve leave: true, }] } - WindowEvent::PointerButton { state, button, .. } => { + WindowEvent::PointerButton { state, button, position, .. } => { let mouse_button = match button { ButtonSource::Mouse(mouse_button) => mouse_button, _ => { @@ -81,6 +88,7 @@ pub(crate) fn translate(input_state: &mut InputState, event: &WindowEvent) -> Ve } }; + let _ = input_state.cursor_move(position); let click_count = input_state.mouse_input(mouse_button, state).into(); let up = matches!(state, ElementState::Released); let button = match mouse_button { diff --git a/desktop/ui/src/input/state.rs b/desktop/ui/src/input/state.rs index 8d6de8d167..aceedd6872 100644 --- a/desktop/ui/src/input/state.rs +++ b/desktop/ui/src/input/state.rs @@ -55,8 +55,8 @@ impl InputState { pub(crate) fn mouse_data(&self) -> MouseData { MouseData { - x: self.mouse_position.x as i32, - y: self.mouse_position.y as i32, + x: self.mouse_position.x, + y: self.mouse_position.y, modifiers: self.cef_mouse_modifiers().into(), } } @@ -64,14 +64,14 @@ impl InputState { #[derive(Default, Clone, Copy, Eq, PartialEq)] pub(crate) struct MousePosition { - x: usize, - y: usize, + x: i32, + y: i32, } impl From<&PhysicalPosition> for MousePosition { fn from(position: &PhysicalPosition) -> Self { Self { - x: position.x as usize, - y: position.y as usize, + x: position.x as i32, + y: position.y as i32, } } } @@ -133,8 +133,8 @@ impl ClickTracker { ElementState::Released => (record.up_count, record.up_position), }; - let dx = position.x.abs_diff(prev_position.x); - let dy = position.y.abs_diff(prev_position.y); + let dx = position.x.abs_diff(prev_position.x) as usize; + let dy = position.y.abs_diff(prev_position.y) as usize; let within_dist = dx <= MULTICLICK_ALLOWED_TRAVEL && dy <= MULTICLICK_ALLOWED_TRAVEL; let count = match (prev_count, within_time, within_dist) { diff --git a/desktop/ui/src/internal/render_handler.rs b/desktop/ui/src/internal/render_handler.rs index 86da6bf2bb..26f8c8d69d 100644 --- a/desktop/ui/src/internal/render_handler.rs +++ b/desktop/ui/src/internal/render_handler.rs @@ -51,7 +51,11 @@ impl ImplRenderHandler for RenderHandlerImpl { return; } - self.frames.stage_texture(info.unwrap()); + let Some(info) = info else { + tracing::error!("Accelerated paint callback received no info about the painted frame"); + return; + }; + self.frames.stage_texture(info); self.frames.publish(); } diff --git a/desktop/ui/src/lib.rs b/desktop/ui/src/lib.rs index 63d46cadb5..e413d53441 100644 --- a/desktop/ui/src/lib.rs +++ b/desktop/ui/src/lib.rs @@ -118,10 +118,6 @@ pub enum Acceleration { Disabled, } -#[derive(thiserror::Error, Debug)] -#[non_exhaustive] -pub enum InitError {} - #[derive(thiserror::Error, Debug)] #[non_exhaustive] pub enum UiError { diff --git a/desktop/ui/src/platform/linux.rs b/desktop/ui/src/platform/linux.rs index 2c77159e4f..685ab9095a 100644 --- a/desktop/ui/src/platform/linux.rs +++ b/desktop/ui/src/platform/linux.rs @@ -9,12 +9,12 @@ pub(crate) fn setup_command(command: &mut Command, #[cfg(feature = "accelerated_ // SAFETY: the closure runs in the forked child before exec and only makes async-signal-safe calls unsafe { command.pre_exec(move || { - // Tie the host's lifetime to the main process + // Tie the host lifetime to the main process if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) != 0 { return Err(std::io::Error::last_os_error()); } if libc::getppid() != main_pid { - return Err(std::io::Error::other("main process died before PDEATHSIG was set")); + return Err(std::io::Error::from_raw_os_error(libc::ESRCH)); } // Move the host end of the frame socket to its advertised fd diff --git a/desktop/ui/src/remote/host.rs b/desktop/ui/src/remote/host.rs index 2537a7bebb..21607a87d0 100644 --- a/desktop/ui/src/remote/host.rs +++ b/desktop/ui/src/remote/host.rs @@ -90,6 +90,10 @@ pub(crate) fn run() { ControlOutcome::Disconnected => std::process::exit(0), } + // Workaround for a Windows-specific exception that occurs when `context` is dropped. + // Appears to be related to CEF object destruction order. + // Calling `exit` bypasses rust teardown and lets Windows perform process cleanup. + // TODO: Identify and fix the underlying CEF shutdown issue so this workaround can be removed. #[cfg(target_os = "windows")] std::process::exit(0); } diff --git a/desktop/ui/src/remote/spawn.rs b/desktop/ui/src/remote/spawn.rs index a9103a3fae..7588dd41ab 100644 --- a/desktop/ui/src/remote/spawn.rs +++ b/desktop/ui/src/remote/spawn.rs @@ -163,20 +163,23 @@ pub(crate) fn spawn_host(acceleration: bool) -> Result { plane::PlaneReceiver::new(main_end) }); - let (hello_sender, hello_receiver) = mpsc::channel(); - std::thread::Builder::new() - .name("cef-host-accept".to_string()) - .spawn(move || { - let _ = hello_sender.send(server.accept()); - }) - .expect("Failed to spawn CEF host accept thread"); + let (hello_tx, hello_rx) = mpsc::channel(); + let accept_thread = std::thread::Builder::new().name("cef-host-accept".to_string()).spawn(move || { + let _ = hello_tx.send(server.accept()); + }); + if let Err(e) = accept_thread { + let _ = child.kill(); + let _ = child.wait(); + return Err(UiError::Bootstrap(format!("failed to spawn the host accept thread: {e}"))); + } let deadline = Instant::now() + HOST_HELLO_TIMEOUT; let (event_receiver, hello) = loop { - match hello_receiver.recv_timeout(Duration::from_millis(100)) { + match hello_rx.recv_timeout(Duration::from_millis(100)) { Ok(Ok(accepted)) => break accepted, Ok(Err(e)) => { let _ = child.kill(); + let _ = child.wait(); return Err(UiError::Handshake(format!("failed to accept the host connection: {e}"))); } Err(RecvTimeoutError::Timeout) => { @@ -185,11 +188,13 @@ pub(crate) fn spawn_host(acceleration: bool) -> Result { } if Instant::now() >= deadline { let _ = child.kill(); + let _ = child.wait(); return Err(UiError::HandshakeTimeout); } } Err(RecvTimeoutError::Disconnected) => { let _ = child.kill(); + let _ = child.wait(); return Err(UiError::Handshake("the accept thread disappeared".to_string())); } } @@ -202,6 +207,7 @@ pub(crate) fn spawn_host(acceleration: bool) -> Result { } = hello else { let _ = child.kill(); + let _ = child.wait(); return Err(UiError::Handshake("the first message from the host was not Hello".to_string())); }; tracing::info!("CEF host process connected (pid {pid})"); @@ -247,7 +253,7 @@ pub(crate) fn start_instance(handle: &HostHandle, surface: FrameSurface, events: std::thread::Builder::new() .name("cef-frames".to_string()) .spawn(move || crate::frames::receive::plane_receiver_loop(receiver, consumer)) - .expect("Failed to spawn CEF frame receiver thread"); + .map_err(|e| UiError::Bootstrap(format!("failed to spawn the frame receiver thread: {e}")))?; } { @@ -258,7 +264,7 @@ pub(crate) fn start_instance(handle: &HostHandle, surface: FrameSurface, events: std::thread::Builder::new() .name("cef-host".to_string()) .spawn(move || event_receiver_loop(receiver, consumer, events, shutting_down, died_reported, shutdown_complete_sender)) - .expect("Failed to spawn CEF host event receiver thread"); + .map_err(|e| UiError::Bootstrap(format!("failed to spawn the host event receiver thread: {e}")))?; } { @@ -288,7 +294,7 @@ pub(crate) fn start_instance(handle: &HostHandle, surface: FrameSurface, events: } } }) - .expect("Failed to spawn CEF host supervisor thread"); + .map_err(|e| UiError::Bootstrap(format!("failed to spawn the host supervisor thread: {e}")))?; } Ok(shutdown_complete_receiver) diff --git a/desktop/ui/src/resources.rs b/desktop/ui/src/resources.rs index 4e638d14b8..a7992ef2ef 100644 --- a/desktop/ui/src/resources.rs +++ b/desktop/ui/src/resources.rs @@ -2,7 +2,7 @@ use std::fs::File; #[cfg(feature = "embedded_resources")] use std::io; use std::io::Read; -use std::path::PathBuf; +use std::path::{Component, PathBuf}; use std::sync::Arc; #[derive(Clone)] @@ -34,11 +34,21 @@ pub enum WebResources { } pub(crate) fn load(path: PathBuf) -> Option { + if path.components().any(|component| matches!(component, Component::ParentDir)) { + tracing::error!("Rejected resource path with a parent directory component: {path:?}"); + return None; + } + let resources = if cfg!(feature = "embedded_resources") { WebResources::Embedded } else { - let path = std::env::var("GRAPHITE_RESOURCES").expect("GRAPHITE_RESOURCES must point to the frontend assets when embedded resources are disabled"); - WebResources::External(path.into()) + match std::env::var("GRAPHITE_RESOURCES") { + Ok(dir) => WebResources::External(dir.into()), + Err(_) => { + tracing::error!("GRAPHITE_RESOURCES must point to the frontend assets when embedded resources are disabled"); + return None; + } + } }; let path = if path.as_os_str().is_empty() { PathBuf::from("index.html") } else { path }; From 24c5cb23ad4aa30062bd8e72538d36772e5eba58 Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 10 Jul 2026 02:36:18 +0000 Subject: [PATCH 5/6] Review --- desktop/src/lib.rs | 1 - desktop/ui/src/frames/import.rs | 2 ++ desktop/ui/src/remote/spawn.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index 0666d93d65..e228586f63 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -169,6 +169,5 @@ pub fn start() -> ExitCode { _ => {} } - #[cfg(not(target_os = "windows"))] ExitCode::SUCCESS } diff --git a/desktop/ui/src/frames/import.rs b/desktop/ui/src/frames/import.rs index 856b87feb8..98a5a79584 100644 --- a/desktop/ui/src/frames/import.rs +++ b/desktop/ui/src/frames/import.rs @@ -15,9 +15,11 @@ pub(crate) enum TextureImportError { InvalidHandle(String), #[error("Unsupported texture format: {format:?}")] UnsupportedFormat { format: cef_color_type_t }, + #[cfg(not(target_os = "macos"))] #[error("Hardware acceleration not available: {reason}")] HardwareUnavailable { reason: String }, #[error("Vulkan operation failed: {operation}")] + #[cfg(target_os = "linux")] VulkanError { operation: String }, #[error("Platform-specific error: {message}")] PlatformError { message: String }, diff --git a/desktop/ui/src/remote/spawn.rs b/desktop/ui/src/remote/spawn.rs index 7588dd41ab..12a5ad0c20 100644 --- a/desktop/ui/src/remote/spawn.rs +++ b/desktop/ui/src/remote/spawn.rs @@ -23,7 +23,7 @@ pub(crate) struct HostHandle { child: Arc>, shutting_down: Arc, died_reported: Arc, - #[cfg_attr(not(feature = "accelerated_paint"), expect(dead_code))] + #[cfg_attr(any(not(feature = "accelerated_paint"), target_os = "windows"), expect(dead_code))] host_acceleration: bool, #[cfg(target_os = "windows")] _job: Option, From 74ede9827a19fee4c80d390c01002cbc03671d83 Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 10 Jul 2026 10:35:14 +0000 Subject: [PATCH 6/6] Remove necessary workarounds --- desktop/ui/src/input.rs | 15 --------------- desktop/ui/src/remote/host.rs | 7 ------- 2 files changed, 22 deletions(-) diff --git a/desktop/ui/src/input.rs b/desktop/ui/src/input.rs index b1c6e40d15..a98b72cc48 100644 --- a/desktop/ui/src/input.rs +++ b/desktop/ui/src/input.rs @@ -150,22 +150,7 @@ pub(crate) fn translate(input_state: &mut InputState, event: &WindowEvent) -> Ve kind = KeyEventKind::Char; } - // Mitigation for CEF on Mac bug to prevent NSMenu being triggered by this key event. - // - // CEF converts the key event into an `NSEvent` internally and passes that to Chromium. - // In some cases the `NSEvent` gets to the native Cocoa application, is considered "unhandled" and can trigger menus. - // - // Why mitigation works: - // Leaving `key_event.unmodified_character = 0` still leads to CEF forwarding a "unhandled" event to the native application - // but that event is discarded because `key_event.unmodified_character = 0` is considered non-printable and not used for shortcut matching. - // - // See https://github.com/chromiumembedded/cef/issues/3857 - // - // TODO: Remove mitigation once bug is fixed or a better solution is found. - #[cfg(not(target_os = "macos"))] let unmodified_character = event.key_without_modifiers.to_char_representation() as u16; - #[cfg(target_os = "macos")] - let unmodified_character = 0; #[cfg(target_os = "macos")] // See https://www.magpcss.org/ceforum/viewtopic.php?start=10&t=11650 if character == 0 && unmodified_character == 0 && event.text_with_all_modifiers.is_some() { diff --git a/desktop/ui/src/remote/host.rs b/desktop/ui/src/remote/host.rs index 21607a87d0..cb3d0c26b7 100644 --- a/desktop/ui/src/remote/host.rs +++ b/desktop/ui/src/remote/host.rs @@ -89,13 +89,6 @@ pub(crate) fn run() { } ControlOutcome::Disconnected => std::process::exit(0), } - - // Workaround for a Windows-specific exception that occurs when `context` is dropped. - // Appears to be related to CEF object destruction order. - // Calling `exit` bypasses rust teardown and lets Windows perform process cleanup. - // TODO: Identify and fix the underlying CEF shutdown issue so this workaround can be removed. - #[cfg(target_os = "windows")] - std::process::exit(0); } enum ControlOutcome {