Extract CEF rendered UI into a separate process and crate

This commit is contained in:
Timon
2026-07-08 23:34:00 +00:00
parent 97f8113fe4
commit 5daea64585
79 changed files with 4159 additions and 1257 deletions

214
Cargo.lock generated
View File

@@ -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"

View File

@@ -7,6 +7,7 @@ members = [
"desktop/platform/linux",
"desktop/platform/mac",
"desktop/platform/win",
"desktop/ui",
"document/graph-storage",
"document/container",
"document/document-format",

View File

@@ -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 }

View File

@@ -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) };

View File

@@ -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

View File

@@ -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);

View File

@@ -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);

View File

@@ -1,3 +1,3 @@
fn main() {
graphite_desktop::start();
fn main() -> std::process::ExitCode {
graphite_desktop::start()
}

View File

@@ -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 }

View File

@@ -1,3 +1,3 @@
fn main() {
graphite_desktop::start_helper();
fn main() -> std::process::ExitCode {
graphite_desktop_ui::run_helper()
}

View File

@@ -1,3 +1,3 @@
fn main() {
graphite_desktop::start();
fn main() -> std::process::ExitCode {
graphite_desktop::start()
}

View File

@@ -1,4 +1,4 @@
#![windows_subsystem = "windows"]
fn main() {
graphite_desktop::start();
fn main() -> std::process::ExitCode {
graphite_desktop::start()
}

View File

@@ -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)));
}
}

View File

@@ -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 }
}
}

View File

@@ -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>);
}

View File

@@ -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,
}

View File

@@ -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));
}

View File

@@ -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);
}
}

View File

@@ -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);
}
_ => {}
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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");

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -1,3 +1,3 @@
fn main() {
graphite_desktop::start();
fn main() -> std::process::ExitCode {
graphite_desktop::start()
}

View File

@@ -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
View 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 }

View File

@@ -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
View 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);
}
});
});
}

View 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
View 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
View 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
View 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;

View 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: &[],
})
}

View 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() }
}

View 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))
}

View 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) })
}
}

View 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
}
}
}

View 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)
}
}

View 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)
}
}

View 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}");
}
}
}

View 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);
}
}

View 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);
}
}
}

View 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;
}
}
}

View 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}"),
}
}
}
}

View 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
View 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,
}
}
}

View File

@@ -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(),
}
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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>,

View File

@@ -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>,

View File

@@ -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>,

View File

@@ -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 {

View File

@@ -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
View 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)
}
}

View File

@@ -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

View 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(())
});
}
}

View 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}");
}
}

View 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
View 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
}
}
}
}

View 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;
}
}
}
}

View 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,
}

View 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
View 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
View 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
}
}