mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Extract CEF rendered UI into a separate process and crate
This commit is contained in:
214
Cargo.lock
generated
214
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -7,6 +7,7 @@ members = [
|
||||
"desktop/platform/linux",
|
||||
"desktop/platform/mac",
|
||||
"desktop/platform/win",
|
||||
"desktop/ui",
|
||||
"document/graph-storage",
|
||||
"document/container",
|
||||
"document/document-format",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<PathBuf, Box<dyn Error>> {
|
||||
pub(crate) fn build_bin(package: &str, bin: Option<&str>, features: Option<&str>) -> Result<PathBuf, Box<dyn Error>> {
|
||||
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) };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::common::*;
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ const GRAPHITE_FILE_EXTENSION: &str = "graphite";
|
||||
const GRAPHITE_MIME_TYPE: &str = "application/graphite+json";
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
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);
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::common::*;
|
||||
const EXECUTABLE: &str = "Graphite.exe";
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
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);
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
graphite_desktop::start();
|
||||
fn main() -> std::process::ExitCode {
|
||||
graphite_desktop::start()
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
graphite_desktop::start_helper();
|
||||
fn main() -> std::process::ExitCode {
|
||||
graphite_desktop_ui::run_helper()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
graphite_desktop::start();
|
||||
fn main() -> std::process::ExitCode {
|
||||
graphite_desktop::start()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
fn main() {
|
||||
graphite_desktop::start();
|
||||
fn main() -> std::process::ExitCode {
|
||||
graphite_desktop::start()
|
||||
}
|
||||
|
||||
@@ -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<AppEvent>,
|
||||
app_event_scheduler: AppEventScheduler,
|
||||
desktop_wrapper: DesktopWrapper,
|
||||
cef_context: Box<dyn cef::CefContext>,
|
||||
cef_schedule: Option<Instant>,
|
||||
cef_view_info_sender: Sender<cef::ViewInfoUpdate>,
|
||||
cef_init_successful: bool,
|
||||
ui: UiInstance,
|
||||
ui_frame_received: bool,
|
||||
start_render_sender: SyncSender<()>,
|
||||
web_communication_initialized: bool,
|
||||
web_communication_startup_buffer: Vec<Vec<u8>>,
|
||||
@@ -59,10 +57,8 @@ impl App {
|
||||
Window::init();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
cef_context: Box<dyn cef::CefContext>,
|
||||
cef_view_info_sender: Sender<cef::ViewInfoUpdate>,
|
||||
ui: UiInstance,
|
||||
wgpu_context: WgpuContext,
|
||||
app_event_receiver: Receiver<AppEvent>,
|
||||
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<u8>) {
|
||||
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<winit::event::DeviceId>, 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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Resource>;
|
||||
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<String>,
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ResourceReader {
|
||||
Embedded(io::Cursor<&'static [u8]>),
|
||||
File(Arc<File>),
|
||||
}
|
||||
impl Read for ResourceReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
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<Mutex<ViewInfoReceiver>>,
|
||||
}
|
||||
|
||||
impl CefHandler {
|
||||
pub(crate) fn new(app_event_scheduler: AppEventScheduler, view_info_receiver: Receiver<ViewInfoUpdate>) -> 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<Resource> {
|
||||
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<ViewInfoUpdate>,
|
||||
}
|
||||
impl ViewInfoReceiver {
|
||||
fn new(receiver: Receiver<ViewInfoUpdate>) -> Self {
|
||||
Self { view_info: ViewInfo::new(), receiver }
|
||||
}
|
||||
}
|
||||
@@ -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<u8>);
|
||||
}
|
||||
@@ -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<H: CefEventHandler> {
|
||||
pub(crate) args: Args,
|
||||
pub(crate) is_sub_process: bool,
|
||||
_marker: std::marker::PhantomData<H>,
|
||||
}
|
||||
|
||||
unsafe impl<H: CefEventHandler> Send for CefContextBuilder<H> {}
|
||||
|
||||
impl<H: CefEventHandler> CefContextBuilder<H> {
|
||||
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::<H>::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<impl CefContext, InitError> {
|
||||
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<impl CefContext, InitError> {
|
||||
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<H: CefEventHandler>(event_handler: H, wgpu_context: WgpuContext, instance_dir: TempDir, accelerated_paint: bool) -> Result<SingleThreadedCefContext, InitError> {
|
||||
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,
|
||||
}
|
||||
@@ -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<Option<SingleThreadedCefContext>> = 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<u8>) {
|
||||
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<F>(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));
|
||||
}
|
||||
@@ -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<dyn CefEventHandler>,
|
||||
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<u8>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -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<Mutex<Option<wgpu::Texture>>>,
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub(crate) fn new(context: WgpuContext) -> Self {
|
||||
Self {
|
||||
context,
|
||||
texture: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn texture(&self) -> Option<wgpu::Texture> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
Self::new_with_parent(app_tmp_dir())
|
||||
}
|
||||
|
||||
pub fn new_with_parent(parent: impl AsRef<Path>) -> io::Result<Self> {
|
||||
let random_suffix = format!("{:032x}", rand::random::<u128>());
|
||||
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<Path> 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");
|
||||
|
||||
@@ -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<std::path::PathBuf>),
|
||||
#[cfg(target_os = "macos")]
|
||||
MenuEvent {
|
||||
|
||||
@@ -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::<CefHandler>::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 app = App::new(Box::new(cef_context), cef_view_info_sender, wgpu_context, app_event_receiver, app_event_scheduler, prefs, cli.files);
|
||||
{
|
||||
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(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::<CefHandler>::new_helper();
|
||||
assert!(cef_context_builder.is_sub_process());
|
||||
cef_context_builder.execute_sub_process();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
graphite_desktop::start();
|
||||
fn main() -> std::process::ExitCode {
|
||||
graphite_desktop::start()
|
||||
}
|
||||
|
||||
@@ -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<CursorIcon> for Cursor {
|
||||
fn from(icon: CursorIcon) -> Self {
|
||||
Cursor::Icon(icon)
|
||||
}
|
||||
}
|
||||
impl From<CustomCursorSource> for Cursor {
|
||||
fn from(custom: CustomCursorSource) -> Self {
|
||||
Cursor::Custom(custom)
|
||||
}
|
||||
}
|
||||
|
||||
54
desktop/ui/Cargo.toml
Normal file
54
desktop/ui/Cargo.toml
Normal file
@@ -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 }
|
||||
@@ -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;
|
||||
315
desktop/ui/src/context.rs
Normal file
315
desktop/ui/src/context.rs
Normal file
@@ -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<Option<BrowserContext>> = 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<ViewInfoUpdate>, accelerated_paint: bool) -> Result<Self, InitError> {
|
||||
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<R: Send + 'static>(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<R: Send + 'static>(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<ViewInfoUpdate>, instance_dir: TempDir, accelerated_paint: bool) -> Result<BrowserContext, InitError> {
|
||||
#[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<InputEvent>) {
|
||||
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<u8>) {
|
||||
with_context(move |context| context.send_web_message(message));
|
||||
}
|
||||
}
|
||||
|
||||
struct BrowserContext {
|
||||
delegate: BrowserDelegate,
|
||||
browser: Browser,
|
||||
view_info_sender: Sender<ViewInfoUpdate>,
|
||||
_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<u8>) {
|
||||
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<F>(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<F>(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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
59
desktop/ui/src/delegate.rs
Normal file
59
desktop/ui/src/delegate.rs
Normal file
@@ -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<Inner>);
|
||||
|
||||
struct Inner {
|
||||
sender: Arc<Mutex<IpcSender<EventMessage>>>,
|
||||
view_info: Mutex<ViewInfoReceiver>,
|
||||
}
|
||||
|
||||
impl BrowserDelegate {
|
||||
pub(crate) fn new(sender: Arc<Mutex<IpcSender<EventMessage>>>, view_info_receiver: Receiver<ViewInfoUpdate>) -> 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::Resource> {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
50
desktop/ui/src/dirs.rs
Normal file
50
desktop/ui/src/dirs.rs
Normal file
@@ -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> {
|
||||
Self::new_with_parent(app_tmp_dir())
|
||||
}
|
||||
|
||||
pub fn new_with_parent(parent: impl AsRef<Path>) -> io::Result<Self> {
|
||||
let random_suffix = format!("{:032x}", rand::random::<u128>());
|
||||
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<Path> for TempDir {
|
||||
fn as_ref(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
41
desktop/ui/src/events.rs
Normal file
41
desktop/ui/src/events.rs
Normal file
@@ -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<UiEvent>,
|
||||
terminated: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl EventQueue {
|
||||
pub(crate) fn new() -> (Self, Receiver<UiEvent>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
12
desktop/ui/src/frames.rs
Normal file
12
desktop/ui/src/frames.rs
Normal file
@@ -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;
|
||||
61
desktop/ui/src/frames/import.rs
Normal file
61
desktop/ui/src/frames/import.rs
Normal file
@@ -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<wgpu::Texture, TextureImportError>;
|
||||
|
||||
#[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<wgpu::hal::DeviceError> 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<wgpu::TextureFormat, TextureImportError> {
|
||||
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<wgpu::TextureDescriptor<'static>, 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: &[],
|
||||
})
|
||||
}
|
||||
145
desktop/ui/src/frames/import/d3d11.rs
Normal file
145
desktop/ui/src/frames/import/d3d11.rs
Normal file
@@ -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::<api::Dx12>();
|
||||
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 = <api::Dx12 as wgpu::hal::Api>::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::<api::Dx12>(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::<Vulkan>();
|
||||
let Some(hal_device) = hal_device_guard else {
|
||||
return Err(TextureImportError::HardwareUnavailable {
|
||||
reason: "Device is not using Vulkan backend".to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
let hal_texture = <Vulkan as wgpu::hal::Api>::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::<Vulkan>(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: &<wgpu::hal::api::Dx12 as wgpu::hal::Api>::Device) -> Result<windows::Win32::Graphics::Direct3D12::ID3D12Resource, TextureImportError> {
|
||||
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<ID3D12Resource> = 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::<api::Dx12>().is_some() }
|
||||
}
|
||||
202
desktop/ui/src/frames/import/dmabuf.rs
Normal file
202
desktop/ui/src/frames/import/dmabuf.rs
Normal file
@@ -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<std::os::fd::OwnedFd>,
|
||||
format: cef_color_type_t,
|
||||
modifier: u64,
|
||||
width: u32,
|
||||
height: u32,
|
||||
strides: Vec<u32>,
|
||||
offsets: Vec<u32>,
|
||||
}
|
||||
|
||||
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<std::os::fd::OwnedFd>, strides: Vec<u32>, offsets: Vec<u32>, 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::<api::Vulkan>();
|
||||
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 = <api::Vulkan as wgpu::hal::Api>::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::<Vulkan>(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: &<api::Vulkan as wgpu::hal::Api>::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<vk::SubresourceLayout> {
|
||||
(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<vk::Format, TextureImportError> {
|
||||
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<u32> {
|
||||
(0..mem_properties.memory_type_count).find(|&i| (type_filter & (1 << i)) != 0 && mem_properties.memory_types[i as usize].property_flags.contains(properties))
|
||||
}
|
||||
93
desktop/ui/src/frames/import/iosurface.rs
Normal file
93
desktop/ui/src/frames/import/iosurface.rs
Normal file
@@ -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<Retained<MTLTextureDescriptor>, 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::<IOSurfaceRef>()).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::<wgpu::wgc::api::Metal>();
|
||||
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 = <wgpu::wgc::api::Metal as wgpu::hal::Api>::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::<wgpu::wgc::api::Metal>(hal_tex, &texture_desc) })
|
||||
}
|
||||
}
|
||||
35
desktop/ui/src/frames/plane.rs
Normal file
35
desktop/ui/src/frames/plane.rs
Normal file
@@ -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<cef::sys::cef_color_type_t> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
238
desktop/ui/src/frames/plane/linux.rs
Normal file
238
desktop/ui/src/frames/plane/linux.rs
Normal file
@@ -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::<FrameDescriptor>();
|
||||
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<Mutex<IpcSender<EventMessage>>>) -> Option<Self> {
|
||||
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<StagedFrame> {
|
||||
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::<RawFd>();
|
||||
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::<RawFd>()) 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<OwnedFd>,
|
||||
}
|
||||
|
||||
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<RecvResult> {
|
||||
self.recv(false)
|
||||
}
|
||||
|
||||
pub(crate) fn try_recv(&self) -> std::io::Result<RecvResult> {
|
||||
self.recv(true)
|
||||
}
|
||||
|
||||
fn recv(&self, nonblocking: bool) -> std::io::Result<RecvResult> {
|
||||
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::<RawFd>()) 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::<RawFd>();
|
||||
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<OwnedFd>,
|
||||
}
|
||||
|
||||
impl WireFrame {
|
||||
pub(crate) fn seq(&self) -> u64 {
|
||||
self.descriptor.seq
|
||||
}
|
||||
|
||||
pub(crate) fn import(self, surface: &FrameSurface) -> Option<wgpu::Texture> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
275
desktop/ui/src/frames/plane/mac.rs
Normal file
275
desktop/ui/src/frames/plane/mac.rs
Normal file
@@ -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<mach_port_t> {
|
||||
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<SendRight> {
|
||||
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<Mutex<IpcSender<EventMessage>>>) -> Option<Self> {
|
||||
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<StagedFrame> {
|
||||
let Some(surface) = std::ptr::NonNull::new(info.shared_texture_io_surface.cast::<IOSurfaceRef>()) 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::<FrameMessage>() 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::<FrameMessage>() 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<RecvResult> {
|
||||
self.recv(false)
|
||||
}
|
||||
|
||||
pub(crate) fn try_recv(&self) -> std::io::Result<RecvResult> {
|
||||
self.recv(true)
|
||||
}
|
||||
|
||||
fn recv(&self, nonblocking: bool) -> std::io::Result<RecvResult> {
|
||||
// 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::<FrameMessageBuffer>() 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::<FrameMessage>() {
|
||||
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<wgpu::Texture> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
161
desktop/ui/src/frames/plane/win.rs
Normal file
161
desktop/ui/src/frames/plane/win.rs
Normal file
@@ -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<Self> {
|
||||
// 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<u64> {
|
||||
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<ParentProcess>,
|
||||
events: Arc<Mutex<IpcSender<EventMessage>>>,
|
||||
}
|
||||
|
||||
impl PlaneSender {
|
||||
pub(crate) fn from_config(config: &HostConfig, events: Arc<Mutex<IpcSender<EventMessage>>>) -> Option<Self> {
|
||||
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<StagedFrame> {
|
||||
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<ParentProcess>,
|
||||
}
|
||||
|
||||
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<wgpu::Texture> {
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
131
desktop/ui/src/frames/receive.rs
Normal file
131
desktop/ui/src/frames/receive.rs
Normal file
@@ -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<Option<IpcSharedMemory>>);
|
||||
|
||||
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<HostControlMessage>,
|
||||
sink: Arc<FrameSink>,
|
||||
}
|
||||
|
||||
impl FrameConsumer {
|
||||
pub(crate) fn new(surface: FrameSurface, events: EventQueue, sender: IpcSender<HostControlMessage>) -> Self {
|
||||
Self {
|
||||
surface,
|
||||
events,
|
||||
sender,
|
||||
sink: Arc::new(FrameSink::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn deliver(&self, seq: u64, install: impl FnOnce(&FrameSurface) -> Option<wgpu::Texture>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
62
desktop/ui/src/frames/sequence.rs
Normal file
62
desktop/ui/src/frames/sequence.rs
Normal file
@@ -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<Self>) -> Option<FrameSequenceClaim> {
|
||||
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<SequenceState>,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
desktop/ui/src/frames/sink.rs
Normal file
45
desktop/ui/src/frames/sink.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::remote::messages::HostControlMessage;
|
||||
|
||||
pub(super) struct FrameSink {
|
||||
state: Mutex<FrameSinkState>,
|
||||
}
|
||||
|
||||
#[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<HostControlMessage>, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
155
desktop/ui/src/frames/streamer.rs
Normal file
155
desktop/ui/src/frames/streamer.rs
Normal file
@@ -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<StreamerInner>);
|
||||
|
||||
struct StreamerInner {
|
||||
events: Arc<Mutex<IpcSender<EventMessage>>>,
|
||||
sequence: Arc<SequenceState>,
|
||||
#[cfg(feature = "accelerated_paint")]
|
||||
plane: Option<plane::PlaneSender>,
|
||||
staged: Mutex<Staged>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Staged {
|
||||
segments: Vec<IpcSharedMemory>,
|
||||
pending_adverts: Vec<(u32, IpcSharedMemory)>,
|
||||
buffer: Option<StagedBuffer>,
|
||||
#[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<Mutex<IpcSender<EventMessage>>>, sequence: Arc<SequenceState>, #[cfg(feature = "accelerated_paint")] plane: Option<plane::PlaneSender>) -> 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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
129
desktop/ui/src/frames/surface.rs
Normal file
129
desktop/ui/src/frames/surface.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct FrameSurface {
|
||||
device: wgpu::Device,
|
||||
queue: wgpu_sync::Queue,
|
||||
slot: Arc<Mutex<Option<wgpu::Texture>>>,
|
||||
}
|
||||
|
||||
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<wgpu::Texture> {
|
||||
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<wgpu::Texture> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
258
desktop/ui/src/input.rs
Normal file
258
desktop/ui/src/input.rs
Normal file
@@ -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<InputEvent> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<InputState> 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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<H: CefEventHandler> {
|
||||
pub(crate) struct BrowserProcessAppImpl {
|
||||
object: *mut RcImpl<_cef_app_t, Self>,
|
||||
event_handler: H,
|
||||
accelerated_paint: bool,
|
||||
}
|
||||
impl<H: CefEventHandler> BrowserProcessAppImpl<H> {
|
||||
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<H: CefEventHandler> ImplApp for BrowserProcessAppImpl<H> {
|
||||
impl ImplApp for BrowserProcessAppImpl {
|
||||
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
|
||||
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::<H>::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<H: CefEventHandler> ImplApp for BrowserProcessAppImpl<H> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> Clone for BrowserProcessAppImpl<H> {
|
||||
impl Clone for BrowserProcessAppImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
@@ -118,12 +115,11 @@ impl<H: CefEventHandler> Clone for BrowserProcessAppImpl<H> {
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
event_handler: self.event_handler.duplicate(),
|
||||
accelerated_paint: self.accelerated_paint,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> Rc for BrowserProcessAppImpl<H> {
|
||||
impl Rc for BrowserProcessAppImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
@@ -131,7 +127,7 @@ impl<H: CefEventHandler> Rc for BrowserProcessAppImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> WrapApp for BrowserProcessAppImpl<H> {
|
||||
impl WrapApp for BrowserProcessAppImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
@@ -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<H: CefEventHandler> {
|
||||
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<H: CefEventHandler> BrowserProcessClientImpl<H> {
|
||||
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<H: CefEventHandler> ImplClient for BrowserProcessClientImpl<H> {
|
||||
impl ImplClient for BrowserProcessClientImpl {
|
||||
fn on_process_message_received(
|
||||
&self,
|
||||
_browser: Option<&mut cef::Browser>,
|
||||
@@ -47,11 +47,11 @@ impl<H: CefEventHandler> ImplClient for BrowserProcessClientImpl<H> {
|
||||
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<H: CefEventHandler> ImplClient for BrowserProcessClientImpl<H> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> Clone for BrowserProcessClientImpl<H> {
|
||||
impl Clone for BrowserProcessClientImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
@@ -98,7 +98,7 @@ impl<H: CefEventHandler> Clone for BrowserProcessClientImpl<H> {
|
||||
}
|
||||
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<H: CefEventHandler> Clone for BrowserProcessClientImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> Rc for BrowserProcessClientImpl<H> {
|
||||
impl Rc for BrowserProcessClientImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
@@ -114,7 +114,7 @@ impl<H: CefEventHandler> Rc for BrowserProcessClientImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> WrapClient for BrowserProcessClientImpl<H> {
|
||||
impl WrapClient for BrowserProcessClientImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_client_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
@@ -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<H: CefEventHandler> {
|
||||
pub(crate) struct BrowserProcessHandlerImpl {
|
||||
object: *mut RcImpl<cef_browser_process_handler_t, Self>,
|
||||
event_handler: H,
|
||||
}
|
||||
impl<H: CefEventHandler> BrowserProcessHandlerImpl<H> {
|
||||
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<H: CefEventHandler> ImplBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
|
||||
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<H: CefEventHandler> ImplBrowserProcessHandler for BrowserProcessHandlerImpl
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> Clone for BrowserProcessHandlerImpl<H> {
|
||||
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<H: CefEventHandler> Rc for BrowserProcessHandlerImpl<H> {
|
||||
impl Rc for BrowserProcessHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
@@ -53,7 +38,7 @@ impl<H: CefEventHandler> Rc for BrowserProcessHandlerImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> WrapBrowserProcessHandler for BrowserProcessHandlerImpl<H> {
|
||||
impl WrapBrowserProcessHandler for BrowserProcessHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_browser_process_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
@@ -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<H: CefEventHandler> {
|
||||
pub(crate) struct DisplayHandlerImpl {
|
||||
object: *mut RcImpl<_cef_display_handler_t, Self>,
|
||||
event_handler: H,
|
||||
delegate: BrowserDelegate,
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> DisplayHandlerImpl<H> {
|
||||
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<H: CefEventHandler> ImplDisplayHandler for DisplayHandlerImpl<H> {
|
||||
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<H: CefEventHandler> ImplDisplayHandler for DisplayHandlerImpl<H> {
|
||||
|
||||
if !buffer_ptr.is_null() && buffer_ptr.align_offset(std::mem::align_of::<u8>()) == 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<H: CefEventHandler> ImplDisplayHandler for DisplayHandlerImpl<H> {
|
||||
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<H: CefEventHandler> ImplDisplayHandler for DisplayHandlerImpl<H> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> Clone for DisplayHandlerImpl<H> {
|
||||
impl Clone for DisplayHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
@@ -131,11 +136,11 @@ impl<H: CefEventHandler> Clone for DisplayHandlerImpl<H> {
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
event_handler: self.event_handler.duplicate(),
|
||||
delegate: self.delegate.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> Rc for DisplayHandlerImpl<H> {
|
||||
impl Rc for DisplayHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
@@ -143,7 +148,7 @@ impl<H: CefEventHandler> Rc for DisplayHandlerImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> WrapDisplayHandler for DisplayHandlerImpl<H> {
|
||||
impl WrapDisplayHandler for DisplayHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_display_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
@@ -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<H: CefEventHandler> {
|
||||
pub(crate) struct LoadHandlerImpl {
|
||||
object: *mut RcImpl<cef_load_handler_t, Self>,
|
||||
event_handler: H,
|
||||
delegate: BrowserDelegate,
|
||||
}
|
||||
impl<H: CefEventHandler> LoadHandlerImpl<H> {
|
||||
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<H: CefEventHandler> ImplLoadHandler for LoadHandlerImpl<H> {
|
||||
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<H: CefEventHandler> ImplLoadHandler for LoadHandlerImpl<H> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> Clone for LoadHandlerImpl<H> {
|
||||
impl Clone for LoadHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
@@ -41,11 +41,11 @@ impl<H: CefEventHandler> Clone for LoadHandlerImpl<H> {
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
event_handler: self.event_handler.duplicate(),
|
||||
delegate: self.delegate.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> Rc for LoadHandlerImpl<H> {
|
||||
impl Rc for LoadHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
@@ -53,7 +53,7 @@ impl<H: CefEventHandler> Rc for LoadHandlerImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> WrapLoadHandler for LoadHandlerImpl<H> {
|
||||
impl WrapLoadHandler for LoadHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_load_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
@@ -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<H: CefEventHandler> {
|
||||
pub(crate) struct RenderHandlerImpl {
|
||||
object: *mut RcImpl<_cef_render_handler_t, Self>,
|
||||
event_handler: H,
|
||||
view: View,
|
||||
delegate: BrowserDelegate,
|
||||
frames: FrameStreamer,
|
||||
}
|
||||
impl<H: CefEventHandler> RenderHandlerImpl<H> {
|
||||
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<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
|
||||
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<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
|
||||
}
|
||||
}
|
||||
|
||||
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<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
|
||||
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<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
|
||||
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<H: CefEventHandler> ImplRenderHandler for RenderHandlerImpl<H> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> Clone for RenderHandlerImpl<H> {
|
||||
impl Clone for RenderHandlerImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
@@ -68,12 +68,12 @@ impl<H: CefEventHandler> Clone for RenderHandlerImpl<H> {
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
event_handler: self.event_handler.duplicate(),
|
||||
view: self.view.clone(),
|
||||
delegate: self.delegate.clone(),
|
||||
frames: self.frames.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> Rc for RenderHandlerImpl<H> {
|
||||
impl Rc for RenderHandlerImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
@@ -81,7 +81,7 @@ impl<H: CefEventHandler> Rc for RenderHandlerImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> WrapRenderHandler for RenderHandlerImpl<H> {
|
||||
impl WrapRenderHandler for RenderHandlerImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_handler_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
@@ -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<H: CefEventHandler> {
|
||||
pub(crate) struct RenderProcessAppImpl {
|
||||
object: *mut RcImpl<_cef_app_t, Self>,
|
||||
render_process_handler: RenderProcessHandler,
|
||||
}
|
||||
impl<H: CefEventHandler> RenderProcessAppImpl<H> {
|
||||
impl RenderProcessAppImpl {
|
||||
pub(crate) fn app() -> App {
|
||||
App::new(Self {
|
||||
object: std::ptr::null_mut(),
|
||||
@@ -19,9 +18,9 @@ impl<H: CefEventHandler> RenderProcessAppImpl<H> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> ImplApp for RenderProcessAppImpl<H> {
|
||||
impl ImplApp for RenderProcessAppImpl {
|
||||
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
|
||||
SchemeHandlerFactoryImpl::<H>::register_schemes(registrar);
|
||||
register_schemes(registrar);
|
||||
}
|
||||
|
||||
fn render_process_handler(&self) -> Option<RenderProcessHandler> {
|
||||
@@ -33,7 +32,7 @@ impl<H: CefEventHandler> ImplApp for RenderProcessAppImpl<H> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> Clone for RenderProcessAppImpl<H> {
|
||||
impl Clone for RenderProcessAppImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
@@ -45,7 +44,7 @@ impl<H: CefEventHandler> Clone for RenderProcessAppImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> Rc for RenderProcessAppImpl<H> {
|
||||
impl Rc for RenderProcessAppImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
@@ -53,7 +52,7 @@ impl<H: CefEventHandler> Rc for RenderProcessAppImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> WrapApp for RenderProcessAppImpl<H> {
|
||||
impl WrapApp for RenderProcessAppImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<cef::sys::_cef_v8_handler_t, Self>,
|
||||
@@ -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>,
|
||||
@@ -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>,
|
||||
@@ -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 {
|
||||
@@ -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<H: CefEventHandler> {
|
||||
pub(crate) struct SchemeHandlerFactoryImpl {
|
||||
object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>,
|
||||
event_handler: H,
|
||||
delegate: BrowserDelegate,
|
||||
}
|
||||
impl<H: CefEventHandler> SchemeHandlerFactoryImpl<H> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
delegate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> ImplSchemeHandlerFactory for SchemeHandlerFactoryImpl<H> {
|
||||
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 {
|
||||
fn create(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, _scheme_name: Option<&CefString>, request: Option<&mut Request>) -> Option<ResourceHandler> {
|
||||
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<H: CefEventHandler> ImplSchemeHandlerFactory for SchemeHandlerFactoryImpl<H
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: CefEventHandler> Clone for SchemeHandlerFactoryImpl<H> {
|
||||
impl Clone for SchemeHandlerFactoryImpl {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
let rc_impl = &mut *self.object;
|
||||
@@ -55,11 +55,11 @@ impl<H: CefEventHandler> Clone for SchemeHandlerFactoryImpl<H> {
|
||||
}
|
||||
Self {
|
||||
object: self.object,
|
||||
event_handler: self.event_handler.duplicate(),
|
||||
delegate: self.delegate.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> Rc for SchemeHandlerFactoryImpl<H> {
|
||||
impl Rc for SchemeHandlerFactoryImpl {
|
||||
fn as_base(&self) -> &cef_base_ref_counted_t {
|
||||
unsafe {
|
||||
let base = &*self.object;
|
||||
@@ -67,7 +67,7 @@ impl<H: CefEventHandler> Rc for SchemeHandlerFactoryImpl<H> {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<H: CefEventHandler> WrapSchemeHandlerFactory for SchemeHandlerFactoryImpl<H> {
|
||||
impl WrapSchemeHandlerFactory for SchemeHandlerFactoryImpl {
|
||||
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>) {
|
||||
self.object = object;
|
||||
}
|
||||
255
desktop/ui/src/lib.rs
Normal file
255
desktop/ui/src/lib.rs
Normal file
@@ -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<S: Stage = Started> {
|
||||
inner: S::ContextData,
|
||||
}
|
||||
|
||||
impl UiContext<Setup> {
|
||||
pub fn setup() -> UiSetupResult {
|
||||
#[cfg(target_os = "macos")]
|
||||
ipc_channel::set_bootstrap_prefix(consts::IPC_BOOTSTRAP_PREFIX);
|
||||
|
||||
let raw_args: Vec<String> = 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<UiContext<Started>, 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<Setup>),
|
||||
Failed,
|
||||
Helper(ExitCode),
|
||||
}
|
||||
|
||||
impl UiContext<Started> {
|
||||
pub fn instance(&self, device: &wgpu::Device, queue: &wgpu_sync::Queue) -> Result<UiInstance, UiError> {
|
||||
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<Started> {
|
||||
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<HostHandle>;
|
||||
}
|
||||
|
||||
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<UiInstanceInner>,
|
||||
}
|
||||
|
||||
pub(crate) struct UiInstanceInner {
|
||||
host: Arc<HostHandle>,
|
||||
input: Mutex<input::InputState>,
|
||||
events: Mutex<Receiver<UiEvent>>,
|
||||
queue: events::EventQueue,
|
||||
shutdown_complete: Mutex<Receiver<()>>,
|
||||
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<UiEvent> {
|
||||
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<u8>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UiEvent {
|
||||
Ready,
|
||||
Frame(wgpu::Texture),
|
||||
Cursor(Cursor),
|
||||
Message(Vec<u8>),
|
||||
InitFailed(String),
|
||||
Crashed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Cursor {
|
||||
Icon(winit::cursor::CursorIcon),
|
||||
Custom { rgba: Vec<u8>, width: u16, height: u16, hotspot_x: u16, hotspot_y: u16 },
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<winit::cursor::CursorIcon> for Cursor {
|
||||
fn from(icon: winit::cursor::CursorIcon) -> Self {
|
||||
Cursor::Icon(icon)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
35
desktop/ui/src/platform/linux.rs
Normal file
35
desktop/ui/src/platform/linux.rs
Normal file
@@ -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<std::os::fd::RawFd>) {
|
||||
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(())
|
||||
});
|
||||
}
|
||||
}
|
||||
63
desktop/ui/src/platform/mac.rs
Normal file
63
desktop/ui/src/platform/mac.rs
Normal file
@@ -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<NSApplication> = 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}");
|
||||
}
|
||||
}
|
||||
39
desktop/ui/src/platform/win.rs
Normal file
39
desktop/ui/src/platform/win.rs
Normal file
@@ -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<Self> {
|
||||
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::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
desktop/ui/src/remote.rs
Normal file
34
desktop/ui/src/remote.rs
Normal file
@@ -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<std::os::fd::RawFd>,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) frame_service: Option<String>,
|
||||
}
|
||||
|
||||
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<Self> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
118
desktop/ui/src/remote/host.rs
Normal file
118
desktop/ui/src/remote/host.rs
Normal file
@@ -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<String> = 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::<EventMessage>::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::<HostControlMessage>().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<HostControlMessage>, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
desktop/ui/src/remote/messages.rs
Normal file
50
desktop/ui/src/remote/messages.rs
Normal file
@@ -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<InputEvent>),
|
||||
UpdateViewInfo(ViewInfoUpdate),
|
||||
RefreshViewInfo,
|
||||
SendWebMessage(Vec<u8>),
|
||||
FrameAck { seq: u64 },
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) enum EventMessage {
|
||||
Hello {
|
||||
pid: u32,
|
||||
control_sender: IpcSender<HostControlMessage>,
|
||||
acceleration: bool,
|
||||
},
|
||||
BrowserCreated,
|
||||
InitFailed(InitError),
|
||||
WebCommunicationInitialized,
|
||||
WebMessage(Vec<u8>),
|
||||
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,
|
||||
}
|
||||
388
desktop/ui/src/remote/spawn.rs
Normal file
388
desktop/ui/src/remote/spawn.rs
Normal file
@@ -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<HostControlMessage>,
|
||||
receivers: Mutex<Option<InstanceReceivers>>,
|
||||
child: Arc<Mutex<Child>>,
|
||||
shutting_down: Arc<AtomicBool>,
|
||||
died_reported: Arc<AtomicBool>,
|
||||
#[cfg_attr(not(feature = "accelerated_paint"), expect(dead_code))]
|
||||
host_acceleration: bool,
|
||||
#[cfg(target_os = "windows")]
|
||||
_job: Option<platform::win::KillOnCloseJob>,
|
||||
}
|
||||
|
||||
struct InstanceReceivers {
|
||||
events: IpcReceiver<EventMessage>,
|
||||
#[cfg(all(any(target_os = "linux", target_os = "macos"), feature = "accelerated_paint"))]
|
||||
frame_plane: Option<plane::PlaneReceiver>,
|
||||
}
|
||||
|
||||
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<HostHandle, UiError> {
|
||||
let (server, server_name) = IpcOneShotServer::<EventMessage>::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::<u64>());
|
||||
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<mpsc::Receiver<()>, 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<EventMessage>,
|
||||
consumer: FrameConsumer,
|
||||
events: EventQueue,
|
||||
shutting_down: Arc<AtomicBool>,
|
||||
died_reported: Arc<AtomicBool>,
|
||||
shutdown_complete_sender: mpsc::Sender<()>,
|
||||
) {
|
||||
let mut newest_frame: Option<PendingFrame> = 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<PendingFrame>,
|
||||
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(());
|
||||
}
|
||||
}
|
||||
}
|
||||
101
desktop/ui/src/resources.rs
Normal file
101
desktop/ui/src/resources.rs
Normal file
@@ -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<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ResourceReader {
|
||||
#[cfg(feature = "embedded_resources")]
|
||||
Embedded(io::Cursor<&'static [u8]>),
|
||||
File(Arc<File>),
|
||||
}
|
||||
impl Read for ResourceReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
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<Resource> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
70
desktop/ui/src/view.rs
Normal file
70
desktop/ui/src/view.rs
Normal file
@@ -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<ViewInfoUpdate>,
|
||||
}
|
||||
|
||||
impl ViewInfoReceiver {
|
||||
pub(super) fn new(receiver: Receiver<ViewInfoUpdate>) -> 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user