Desktop: Isolate CEF-rendered UI into separate crate and process (#4321)

* Extract CEF rendered UI into a separate process and crate

* Review

* Review

* Review

* Review

* Remove necessary workarounds

* Block on frame copy ack

* Crop and resample frames correctly

* Skip blank frames

* Fix deps

* Fix fmt

* Fix clippy warning

* Review

* Fix todo
This commit is contained in:
Timon
2026-07-14 13:19:16 +02:00
committed by Keavon Chambers
parent 97a43e66fb
commit ba0a97aefe
82 changed files with 4768 additions and 1291 deletions

53
desktop/ui/Cargo.toml Normal file
View File

@@ -0,0 +1,53 @@
[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 }
bytemuck = { workspace = true, optional = true }
ipc-channel = "0.22"
# Linux-specific dependencies
[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2"
ash = { version = "0.38", 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",
] }
# Mac-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 }

37
desktop/ui/src/consts.rs Normal file
View File

@@ -0,0 +1,37 @@
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
#[cfg(feature = "accelerated_paint")]
pub(crate) const FRAME_ACK_TIMEOUT: Duration = Duration::from_millis(250);
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;
#[cfg(target_os = "linux")]
pub(crate) const SCROLL_SPEED_X: f32 = 3.;
#[cfg(target_os = "linux")]
pub(crate) const SCROLL_SPEED_Y: f32 = 3.;
#[cfg(not(target_os = "linux"))]
pub(crate) const SCROLL_SPEED_X: f32 = 1.;
#[cfg(not(target_os = "linux"))]
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(500);
pub(crate) const MULTICLICK_ALLOWED_TRAVEL: usize = 4;

350
desktop/ui/src/context.rs Normal file
View File

@@ -0,0 +1,350 @@
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 = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| control(CefContextHandle)));
with_context(|context| {
if let Some(host) = context.browser.host() {
host.close_browser(1);
}
});
run_on_ui_thread(cef::quit_message_loop);
match result {
Ok(result) => {
let _ = result_sender.send(result);
}
Err(panic) => std::panic::resume_unwind(panic),
}
})
.expect("Failed to spawn the CEF control thread");
cef::run_message_loop();
drop(CONTEXT.take());
cef::shutdown();
if let Err(panic) = control_thread.join() {
std::panic::resume_unwind(panic);
}
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 = cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), helper);
assert!(loader.load());
// LibraryLoader unloads the framework on drop
std::mem::forget(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) -> Result<Settings, InitError> {
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 Some(root_cache_path) = instance_dir.to_str().map(CefString::from) else {
return Err(InitError::PathResolutionFailed(format!("non-UTF-8 instance directory path: {}", instance_dir.display())));
};
let base = Settings {
windowless_rendering_enabled: 1,
root_cache_path,
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().map_err(|e| InitError::PathResolutionFailed(format!("cannot get current exe path: {e}")))?;
let app_root = exe
.parent()
.and_then(|p| p.parent())
.and_then(|p| p.parent())
.ok_or_else(|| InitError::PathResolutionFailed(format!("executable is not inside an app bundle: {}", exe.display())))?;
let Some(main_bundle_path) = app_root.to_str().map(CefString::from) else {
return Err(InitError::PathResolutionFailed(format!("invalid app bundle path: {}", app_root.display())));
};
Ok(Settings {
main_bundle_path,
multi_threaded_message_loop: 0,
external_message_pump: 0,
no_sandbox: 1, // GPU helper crashes when running with sandbox
..base
})
}
#[cfg(not(target_os = "macos"))]
Ok(Settings {
multi_threaded_message_loop: 1,
#[cfg(target_os = "linux")]
no_sandbox: 1,
..base
})
}
fn create_browser(delegate: BrowserDelegate, frames: FrameStreamer, view_info_sender: Sender<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();
if incognito_request_context.register_scheme_handler_factory(Some(&RESOURCE_SCHEME.into()), Some(&RESOURCE_DOMAIN.into()), Some(&mut scheme_handler_factory)) != 1 {
return Err(InitError::SchemeHandlerRegistrationFailed);
}
let url = format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/");
browser_host_create_browser_sync(
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,
#[error("Failed to resolve a required path: {0}")]
PathResolutionFailed(String),
#[error("Scheme handler registration failed")]
SchemeHandlerRegistrationFailed,
}
#[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 Some(host) = self.browser.host() else {
tracing::error!("Browser host is not available, cannot refresh view info");
return;
};
host.set_zoom_level(view_info.zoom());
host.was_resized();
// 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");
if let Some(host) = self.browser.host() {
host.close_browser(1);
} else {
tracing::error!("Browser host is not available, cannot close browser");
}
}
}
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);
if post_task(ThreadId::from(cef_thread_id_t::TID_UI), Some(&mut task)) != 1 {
tracing::error!("Failed to post a task to the CEF UI thread");
}
}
fn with_context<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 let Err(e) = fs::create_dir_all(&path) {
tracing::error!("Failed to create temp directory at {path:?}: {e}");
}
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)
}
}

14
desktop/ui/src/frames.rs Normal file
View File

@@ -0,0 +1,14 @@
#[cfg(feature = "accelerated_paint")]
pub(crate) mod import;
#[cfg(feature = "accelerated_paint")]
pub(crate) mod plane;
pub(crate) mod receive;
#[cfg(feature = "accelerated_paint")]
mod resample;
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,117 @@
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 },
#[cfg(not(target_os = "macos"))]
#[error("Hardware acceleration not available: {reason}")]
HardwareUnavailable { reason: String },
#[error("Vulkan operation failed: {operation}")]
#[cfg(target_os = "linux")]
VulkanError { operation: String },
#[error("Platform-specific error: {message}")]
PlatformError { message: String },
}
impl From<wgpu::hal::DeviceError> for TextureImportError {
fn from(e: wgpu::hal::DeviceError) -> Self {
TextureImportError::PlatformError {
message: format!("wgpu-hal DeviceError: {:?}", e),
}
}
}
#[derive(Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
pub(crate) struct ContentRect {
pub(crate) x: u32,
pub(crate) y: u32,
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) source_width: u32,
pub(crate) source_height: u32,
}
#[derive(Clone, Copy)]
pub(crate) enum ContentMapping {
Identity,
Scaled(ContentRect),
}
impl ContentRect {
pub(crate) fn mapping(self, width: u32, height: u32) -> ContentMapping {
let valid = self.width > 0
&& self.height > 0
&& self.source_width > 0
&& self.source_height > 0
&& self.x.checked_add(self.width).is_some_and(|right| right <= width)
&& self.y.checked_add(self.height).is_some_and(|bottom| bottom <= height);
let full = self.x == 0 && self.y == 0 && (self.width, self.height) == (width, height) && (self.source_width, self.source_height) == (width, height);
if valid && !full { ContentMapping::Scaled(self) } else { ContentMapping::Identity }
}
}
impl TryFrom<&cef::AcceleratedPaintInfo> for ContentRect {
type Error = TextureImportError;
fn try_from(info: &cef::AcceleratedPaintInfo) -> Result<Self, Self::Error> {
let invalid = || TextureImportError::InvalidHandle("Failed to create content rect".into());
let content = &info.extra.content_rect;
let width = u32::try_from(content.width).ok().filter(|&width| width > 0).ok_or_else(invalid)?;
let height = u32::try_from(content.height).ok().filter(|&height| height > 0).ok_or_else(invalid)?;
let source = &info.extra.source_size;
let (source_width, source_height) = if info.extra.has_source_size != 0 && source.width > 0 && source.height > 0 {
(source.width as u32, source.height as u32)
} else {
(width, height)
};
Ok(Self {
x: content.x.max(0) as u32,
y: content.y.max(0) as u32,
width,
height,
source_width,
source_height,
})
}
}
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,136 @@
use super::{TextureImportError, TextureImportResult, TextureImporter, texture_descriptor, wgpu_format};
use cef::sys::cef_color_type_t;
use std::os::raw::c_void;
use wgpu::hal::api;
pub struct D3D11Importer {
pub handle: *mut c_void,
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()));
}
let is_d3d12_backend = unsafe { device.as_hal::<api::Dx12>().is_some() };
if is_d3d12_backend {
let texture = self.import_via_d3d12(device)?;
return Ok(texture);
}
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()))
}
}
}

View File

@@ -0,0 +1,226 @@
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.len() != 1 {
return Err(TextureImportError::InvalidHandle(format!("Expected exactly one DMA-BUF plane fd, got {}", self.fds.len())));
}
if self.strides.len() != self.fds.len() || self.offsets.len() != self.fds.len() {
return Err(TextureImportError::InvalidHandle(format!(
"DMA-BUF plane count mismatch: {} fds, {} strides, {} offsets",
self.fds.len(),
self.strides.len(),
self.offsets.len()
)));
}
let texture = self.import_via_vulkan(device)?;
tracing::trace!("Successfully imported DMA-BUF texture via Vulkan");
Ok(texture)
}
}
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);
let plane_layouts = self
.offsets
.iter()
.zip(&self.strides)
.map(|(&offset, &stride)| vk::SubresourceLayout {
offset: offset as u64,
size: 0, // Will be calculated by driver
row_pitch: stride as u64,
array_pitch: 0,
depth_pitch: 0,
})
.collect::<Vec<_>>();
let mut drm_format_modifier = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default()
.drm_format_modifier(self.modifier)
.plane_layouts(&plane_layouts);
let mut external_memory_info = vk::ExternalMemoryImageCreateInfo::default().handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT);
let image_create_info = image_create_info.push_next(&mut drm_format_modifier).push_next(&mut external_memory_info);
let image = unsafe {
device.create_image(&image_create_info, None).map_err(|e| TextureImportError::VulkanError {
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 external_memory_fd = ash::khr::external_memory_fd::Device::new(instance, device);
let mut fd_properties = vk::MemoryFdPropertiesKHR::default();
if let Err(e) = unsafe { external_memory_fd.get_memory_fd_properties(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT, dup_fd, &mut fd_properties) } {
// SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd
unsafe {
device.destroy_image(image, None);
libc::close(dup_fd);
}
return Err(TextureImportError::VulkanError {
operation: format!("Failed to query DMA-BUF fd memory properties: {e:?}"),
});
}
let mut import_memory_fd = vk::ImportMemoryFdInfoKHR::default().handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT).fd(dup_fd);
let memory_properties = unsafe { instance.get_physical_device_memory_properties(hal_device.raw_physical_device()) };
let compatible_type_bits = memory_requirements.memory_type_bits & fd_properties.memory_type_bits;
let Some(memory_type_index) = find_memory_type_index(compatible_type_bits, vk::MemoryPropertyFlags::empty(), &memory_properties) else {
// SAFETY: import failed and the fd is still ours, need to clean up the image and close the fd
unsafe {
device.destroy_image(image, None);
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 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,265 @@
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],
content_x: u32,
content_y: u32,
content_width: u32,
content_height: u32,
source_width: u32,
source_height: u32,
}
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 coded_size = &info.extra.coded_size;
if coded_size.width <= 0 || coded_size.height <= 0 {
tracing::error!("Accelerated paint delivered an invalid coded size: {}x{}", coded_size.width, coded_size.height);
return None;
}
let plane_count = (info.plane_count.max(0) as usize).min(info.planes.len());
let mut fds = Vec::with_capacity(plane_count);
let mut strides = [0u32; 4];
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;
}
let content = crate::frames::import::ContentRect::try_from(info).unwrap_or_default();
Some(StagedFrame {
descriptor: FrameDescriptor {
seq: 0,
modifier: info.modifier,
width: coded_size.width as u32,
height: coded_size.height as u32,
format: *info.format.as_ref() as u32,
plane_count: plane_count as u32,
strides,
offsets,
content_x: content.x,
content_y: content.y,
content_width: content.width,
content_height: content.height,
source_width: content.source_width,
source_height: content.source_height,
},
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 content = crate::frames::import::ContentRect {
x: descriptor.content_x,
y: descriptor.content_y,
width: descriptor.content_width,
height: descriptor.content_height,
source_width: descriptor.source_width,
source_height: descriptor.source_height,
};
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, content)
}
}

View File

@@ -0,0 +1,305 @@
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,
content_x: u32,
content_y: u32,
content_width: u32,
content_height: u32,
source_width: u32,
source_height: 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 coded_size = &info.extra.coded_size;
if coded_size.width <= 0 || coded_size.height <= 0 {
tracing::error!("Accelerated paint delivered an invalid coded size: {}x{}", coded_size.width, coded_size.height);
return None;
}
let Some(surface) = std::ptr::NonNull::new(info.shared_texture_io_surface.cast::<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;
}
let content = crate::frames::import::ContentRect::try_from(info).unwrap_or_default();
Some(StagedFrame {
descriptor: FrameDescriptor {
seq: 0,
width: coded_size.width as u32,
height: coded_size.height as u32,
format: *info.format.as_ref() as u32,
content_x: content.x,
content_y: content.y,
content_width: content.width,
content_height: content.height,
source_width: content.source_width,
source_height: content.source_height,
_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,
};
// 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}")));
}
// Kernel took ownership of the surface and moves it to the receiver. We must not drop.
std::mem::forget(frame.surface);
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 content = crate::frames::import::ContentRect {
x: descriptor.content_x,
y: descriptor.content_y,
width: descriptor.content_width,
height: descriptor.content_height,
source_width: descriptor.source_width,
source_height: descriptor.source_height,
};
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, content)
}
}

View File

@@ -0,0 +1,175 @@
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::import::ContentRect;
use crate::frames::surface::FrameSurface;
use crate::remote::HostConfig;
use crate::remote::messages::EventMessage;
struct MainProcess(HANDLE);
// SAFETY: process handles may be used and closed from any thread.
unsafe impl Send for MainProcess {}
unsafe impl Sync for MainProcess {}
impl MainProcess {
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_main(&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 MainProcess {
fn drop(&mut self) {
// SAFETY: we own the process handle.
unsafe {
let _ = CloseHandle(self.0);
}
}
}
pub(crate) struct PlaneSender {
main: Arc<MainProcess>,
events: Arc<Mutex<IpcSender<EventMessage>>>,
}
impl PlaneSender {
pub(crate) fn from_config(config: &HostConfig, events: Arc<Mutex<IpcSender<EventMessage>>>) -> Option<Self> {
match MainProcess::open(config.main_pid) {
Ok(main) => Some(Self { main: Arc::new(main), events }),
Err(e) => {
tracing::error!("Failed to open the main process for handle duplication, falling back to software frames: {e}");
None
}
}
}
pub(crate) fn stage(&self, info: &cef::AcceleratedPaintInfo) -> Option<StagedFrame> {
let coded_size = &info.extra.coded_size;
if coded_size.width <= 0 || coded_size.height <= 0 {
tracing::error!("Accelerated paint delivered an invalid coded size: {}x{}", coded_size.width, coded_size.height);
return None;
}
let handle = match self.main.duplicate_into(HANDLE(info.shared_texture_handle)) {
Ok(handle) => handle,
Err(e) => {
tracing::error!("Failed to duplicate the shared texture handle into the main process: {e}");
return None;
}
};
Some(StagedFrame {
handle: HandleInMain { handle, main: self.main.clone() },
width: coded_size.width as u32,
height: coded_size.height as u32,
format: *info.format.as_ref() as u32,
content: ContentRect::try_from(info).ok(),
})
}
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,
content: frame.content,
};
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: HandleInMain,
width: u32,
height: u32,
format: u32,
content: Option<ContentRect>,
}
struct HandleInMain {
handle: u64,
main: Arc<MainProcess>,
}
impl Drop for HandleInMain {
fn drop(&mut self) {
self.main.close_in_main(self.handle);
}
}
pub(crate) struct WireFrame {
seq: u64,
handle: ReceivedHandle,
width: u32,
height: u32,
format: u32,
content: Option<ContentRect>,
}
impl WireFrame {
pub(crate) fn new(seq: u64, handle: u64, width: u32, height: u32, format: u32, content: Option<ContentRect>) -> Self {
Self {
seq,
handle: ReceivedHandle(handle),
width,
height,
format,
content,
}
}
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)?;
let content = self.content.unwrap_or_default();
surface.import_texture(crate::frames::import::d3d11::D3D11Importer::from_parts(self.handle.0, self.width, self.height, format), content)
}
}
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,143 @@
use std::sync::{Arc, OnceLock};
#[derive(Clone)]
pub(super) struct Resampler {
device: wgpu::Device,
pipeline: Arc<OnceLock<Pipeline>>,
}
struct Pipeline {
format: wgpu::TextureFormat,
sampler: wgpu::Sampler,
layout: wgpu::BindGroupLayout,
pipeline: wgpu::RenderPipeline,
}
impl Resampler {
pub(super) fn new(device: wgpu::Device) -> Self {
Self {
device,
pipeline: Arc::new(OnceLock::new()),
}
}
pub(super) fn encode(&self, encoder: &mut wgpu::CommandEncoder, source: &wgpu::Texture, content_origin: wgpu::Origin3d, content_size: wgpu::Extent3d, target: &wgpu::Texture) {
let pipeline = self.pipeline.get_or_init(|| Pipeline::new(&self.device, target.format()));
debug_assert_eq!(pipeline.format, target.format());
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("CEF Resample Bind Group"),
layout: &pipeline.layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&source.create_view(&Default::default())),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&pipeline.sampler),
},
],
});
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("CEF Resample Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &target.create_view(&Default::default()),
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
pass.set_pipeline(&pipeline.pipeline);
pass.set_immediates(
0,
bytemuck::bytes_of(&Immediates {
content_origin: [content_origin.x as f32, content_origin.y as f32],
content_size: [content_size.width as f32, content_size.height as f32],
}),
);
pass.set_bind_group(0, &bind_group, &[]);
pass.draw(0..3, 0..1);
}
}
impl Pipeline {
fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("CEF Resample Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("CEF Resample Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("CEF Resample Pipeline Layout"),
bind_group_layouts: &[Some(&layout)],
immediate_size: std::mem::size_of::<Immediates>() as u32,
});
let shader = device.create_shader_module(wgpu::include_wgsl!("resample.wgsl"));
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("CEF Resample Pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
buffers: &[],
},
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview_mask: None,
cache: None,
});
Self { format, sampler, layout, pipeline }
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Immediates {
content_origin: [f32; 2],
content_size: [f32; 2],
}

View File

@@ -0,0 +1,77 @@
// =============
// VERTEX SHADER
// =============
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) tex_coords: vec2<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
var out: VertexOutput;
let pos = array(
vec2f(-1.0, -1.0),
vec2f(3.0, -1.0),
vec2f(-1.0, 3.0),
);
let xy = pos[vertex_index];
out.clip_position = vec4f(xy, 0.0, 1.0);
let coords = xy / 2. + 0.5;
out.tex_coords = vec2f(coords.x, 1. - coords.y);
return out;
}
// ===============
// FRAGMENT SHADER
// ===============
struct Immediates {
content_origin: vec2<f32>,
content_size: vec2<f32>,
};
var<immediate> immediates: Immediates;
@group(0) @binding(0)
var t_frame: texture_2d<f32>;
@group(0) @binding(1)
var s_frame: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let sample_pos = in.tex_coords * immediates.content_size;
let nearest = floor(sample_pos - 0.5) + 0.5;
let t = sample_pos - nearest;
// Catmull-Rom spline interpolation based sampeling
// See https://gist.github.com/TheRealMJP/c83b8c0f46b63f3a88a5986f4fa982b1
let weight_before = t * (-0.5 + t * (1.0 - 0.5 * t));
let weight_nearest = 1.0 + t * t * (-2.5 + 1.5 * t);
let weight_next = t * (0.5 + t * (2.0 - 1.5 * t));
let weight_after = t * t * (-0.5 + 0.5 * t);
let weight_middle = weight_nearest + weight_next;
let middle = nearest + weight_next / weight_middle;
let frame_size = vec2<f32>(textureDimensions(t_frame));
let content_min = vec2<f32>(0.5);
let content_max = immediates.content_size - 0.5;
let uv_before = (immediates.content_origin + clamp(nearest - 1.0, content_min, content_max)) / frame_size;
let uv_middle = (immediates.content_origin + clamp(middle, content_min, content_max)) / frame_size;
let uv_after = (immediates.content_origin + clamp(nearest + 2.0, content_min, content_max)) / frame_size;
var color = vec4<f32>(0.0);
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_before.x, uv_before.y), 0.0) * weight_before.x * weight_before.y;
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_middle.x, uv_before.y), 0.0) * weight_middle.x * weight_before.y;
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_after.x, uv_before.y), 0.0) * weight_after.x * weight_before.y;
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_before.x, uv_middle.y), 0.0) * weight_before.x * weight_middle.y;
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_middle.x, uv_middle.y), 0.0) * weight_middle.x * weight_middle.y;
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_after.x, uv_middle.y), 0.0) * weight_after.x * weight_middle.y;
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_before.x, uv_after.y), 0.0) * weight_before.x * weight_after.y;
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_middle.x, uv_after.y), 0.0) * weight_middle.x * weight_after.y;
color += textureSampleLevel(t_frame, s_frame, vec2<f32>(uv_after.x, uv_after.y), 0.0) * weight_after.x * weight_after.y;
return clamp(color, vec4<f32>(0.0), vec4<f32>(1.0));
}

View File

@@ -0,0 +1,85 @@
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex, PoisonError};
#[cfg(feature = "accelerated_paint")]
use std::time::Instant;
use crate::consts::FRAMES_IN_FLIGHT_LIMIT;
pub(crate) struct SequenceState {
last_sent: AtomicU64,
last_acked: AtomicU64,
ack_lock: Mutex<()>,
ack_signal: Condvar,
}
impl SequenceState {
pub(crate) fn new() -> Self {
Self {
last_sent: AtomicU64::new(0),
last_acked: AtomicU64::new(0),
ack_lock: Mutex::new(()),
ack_signal: Condvar::new(),
}
}
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);
drop(self.ack_lock.lock().unwrap_or_else(PoisonError::into_inner));
self.ack_signal.notify_all();
}
}
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;
}
#[cfg(feature = "accelerated_paint")]
pub(crate) fn wait_for_ack(&self) -> bool {
let deadline = Instant::now() + crate::consts::FRAME_ACK_TIMEOUT;
let mut guard = self.sequence.ack_lock.lock().unwrap_or_else(PoisonError::into_inner);
loop {
if self.sequence.last_acked.load(Ordering::Relaxed) >= self.seq {
return true;
}
let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
return false;
};
guard = self.sequence.ack_signal.wait_timeout(guard, remaining).unwrap_or_else(PoisonError::into_inner).0;
}
}
}
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;
}
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(()) => {
if !claim.wait_for_ack() {
tracing::warn!("Accelerated frame {} was not acked", claim.seq());
}
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,263 @@
use std::sync::{Arc, Mutex};
#[cfg(feature = "accelerated_paint")]
use super::import::ContentMapping;
#[cfg(feature = "accelerated_paint")]
use super::resample::Resampler;
#[derive(Clone)]
pub(crate) struct FrameSurface {
device: wgpu::Device,
queue: wgpu_sync::Queue,
slot: Arc<Mutex<Option<wgpu::Texture>>>,
#[cfg(feature = "accelerated_paint")]
resampler: Resampler,
}
impl FrameSurface {
pub(crate) fn new(device: wgpu::Device, queue: wgpu_sync::Queue) -> Self {
Self {
#[cfg(feature = "accelerated_paint")]
resampler: Resampler::new(device.clone()),
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 buffer.chunks_exact(4).take(width as usize).all(|pixel| pixel[3] == 0) {
tracing::debug!("Skipping fully transparent frame");
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()?;
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, content_rect: crate::frames::import::ContentRect) -> 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 mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("CEF Frame Copy Encoder"),
});
let output = match content_rect.mapping(imported.width(), imported.height()) {
ContentMapping::Identity => {
let output = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("CEF Imported Frame Copy"),
size: imported.size(),
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: imported.format(),
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: &imported,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyTextureInfo {
texture: &output,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
imported.size(),
);
output
}
ContentMapping::Scaled(content_rect) => {
let output = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("CEF Imported Scaled Frame Copy"),
size: wgpu::Extent3d {
width: content_rect.source_width,
height: content_rect.source_height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: imported.format(),
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let size = wgpu::Extent3d {
width: content_rect.width,
height: content_rect.height,
depth_or_array_layers: 1,
};
self.resampler.encode(
&mut encoder,
&imported,
wgpu::Origin3d {
x: content_rect.x,
y: content_rect.y,
z: 0,
},
size,
&output,
);
output
}
};
let blank_check = blank_check::encode_readback(&self.device, &mut encoder, &output);
let submission = self.queue.submit([encoder.finish()]);
let blank_check = blank_check.map();
let _ = self.device.poll(wgpu::PollType::Wait {
submission_index: Some(submission),
timeout: None,
});
if blank_check.check_is_blank() {
tracing::debug!("Skipping fully transparent accelerated frame");
return None;
}
let Ok(mut slot) = self.slot.lock() else {
tracing::error!("Failed to lock the frame surface");
return None;
};
*slot = Some(output.clone());
Some(output)
}
}
#[cfg(feature = "accelerated_paint")]
mod blank_check {
use std::sync::mpsc;
const STRIP_BYTES: u32 = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
const STRIP_TEXELS: u32 = STRIP_BYTES / 4;
pub(super) fn encode_readback(device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, texture: &wgpu::Texture) -> PendingBlankCheck {
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("CEF Blank Check"),
size: STRIP_BYTES as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let width = texture.width().min(STRIP_TEXELS);
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: (texture.width() - width) / 2,
y: 0,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(STRIP_BYTES),
rows_per_image: None,
},
},
wgpu::Extent3d {
width,
height: 1,
depth_or_array_layers: 1,
},
);
PendingBlankCheck { buffer, width }
}
pub(super) struct PendingBlankCheck {
buffer: wgpu::Buffer,
width: u32,
}
impl PendingBlankCheck {
pub(super) fn map(self) -> MappedBlankCheck {
let (sender, receiver) = mpsc::channel();
self.buffer.slice(..u64::from(self.width) * 4).map_async(wgpu::MapMode::Read, move |result| {
let _ = sender.send(result);
});
MappedBlankCheck {
buffer: self.buffer,
width: self.width,
receiver,
}
}
}
pub(super) struct MappedBlankCheck {
buffer: wgpu::Buffer,
width: u32,
receiver: mpsc::Receiver<Result<(), wgpu::BufferAsyncError>>,
}
impl MappedBlankCheck {
pub(super) fn check_is_blank(self) -> bool {
match self.receiver.try_recv() {
Ok(Ok(())) => {
let slice = self.buffer.slice(..u64::from(self.width) * 4);
slice.get_mapped_range().chunks_exact(4).all(|texel| texel[3] == 0)
}
_ => false,
}
}
}
}

251
desktop/ui/src/input.rs Normal file
View File

@@ -0,0 +1,251 @@
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, .. } => {
if !input_state.cursor_move(position) {
return Vec::new();
}
vec![InputEvent::MouseMove {
data: input_state.mouse_data(),
leave: false,
}]
}
WindowEvent::PointerEntered { position, .. } => {
let _ = input_state.cursor_move(position);
vec![InputEvent::MouseMove {
data: input_state.mouse_data(),
leave: false,
}]
}
WindowEvent::PointerLeft { position, .. } => {
if let Some(position) = position {
let _ = input_state.cursor_move(position);
}
vec![InputEvent::MouseMove {
data: input_state.mouse_data(),
leave: true,
}]
}
WindowEvent::PointerButton { state, button, position, .. } => {
let mouse_button = match button {
ButtonSource::Mouse(mouse_button) => mouse_button,
_ => {
return Vec::new(); // TODO: Handle touch input
}
};
let _ = input_state.cursor_move(position);
let click_count = input_state.mouse_input(mouse_button, state).into();
let up = matches!(state, ElementState::Released);
let button = match mouse_button {
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;
}
let 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 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

@@ -0,0 +1,275 @@
use winit::keyboard::{Key, NamedKey, PhysicalKey};
pub(crate) trait ToCharRepresentation {
fn to_char_representation(&self) -> char;
}
impl ToCharRepresentation for Key {
fn to_char_representation(&self) -> char {
match self {
Key::Named(named) => match named {
NamedKey::Tab => '\t',
NamedKey::Enter => '\r',
NamedKey::Backspace => '\x08',
NamedKey::Escape => '\x1b',
_ => '\0',
},
Key::Character(char) => char.chars().next().unwrap_or_default(),
_ => '\0',
}
}
}
pub(crate) trait ToNativeKeycode {
fn to_native_keycode(&self) -> i32;
}
impl ToNativeKeycode for PhysicalKey {
fn to_native_keycode(&self) -> i32 {
use winit::platform::scancode::PhysicalKeyExtScancode;
#[cfg(target_os = "linux")]
{
self.to_scancode().map(|evdev| (evdev + 8) as i32).unwrap_or_default()
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
self.to_scancode().map(|c| c as i32).unwrap_or_default()
}
}
}
pub(crate) trait ToVKBits {
fn to_vk_bits(&self) -> i32;
}
macro_rules! map_enum {
($target:expr, $enum:ident, $( ($code:expr, $variant:ident), )+ ) => {
match $target {
$(
$enum::$variant => $code,
)+
_ => 0,
}
};
}
impl ToVKBits for winit::keyboard::NamedKey {
fn to_vk_bits(&self) -> i32 {
map_enum!(
self,
NamedKey,
(0x12, Alt),
(0xA5, AltGraph),
(0x14, CapsLock),
(0x11, Control),
(0x90, NumLock),
(0x91, ScrollLock),
(0x10, Shift),
(0x5B, Meta),
(0x0D, Enter),
(0x09, Tab),
(0x25, ArrowLeft),
(0x26, ArrowUp),
(0x27, ArrowRight),
(0x28, ArrowDown),
(0x23, End),
(0x24, Home),
(0x22, PageDown),
(0x21, PageUp),
(0x08, Backspace),
(0x0C, Clear),
(0xF7, CrSel),
(0x2E, Delete),
(0xF9, EraseEof),
(0xF8, ExSel),
(0x2D, Insert),
(0x1E, Accept),
(0xF6, Attn),
(0x03, Cancel),
(0x5D, ContextMenu),
(0x1B, Escape),
(0x2B, Execute),
(0x2F, Help),
(0x13, Pause),
(0xFA, Play),
(0x5D, Props),
(0x29, Select),
(0xFB, ZoomIn),
(0xFB, ZoomOut),
(0x2C, PrintScreen),
(0x5F, Standby),
(0x1C, Convert),
(0x18, FinalMode),
(0x1F, ModeChange),
(0x1D, NonConvert),
(0xE5, Process),
(0x15, HangulMode),
(0x19, HanjaMode),
(0x17, JunjaMode),
(0x15, KanaMode),
(0x19, KanjiMode),
(0xB0, MediaFastForward),
(0xB3, MediaPause),
(0xB3, MediaPlay),
(0xB3, MediaPlayPause),
(0xB1, MediaRewind),
(0xB2, MediaStop),
(0xB0, MediaTrackNext),
(0xB1, MediaTrackPrevious),
(0x2A, Print),
(0xAE, AudioVolumeDown),
(0xAF, AudioVolumeUp),
(0xAD, AudioVolumeMute),
(0xB6, LaunchApplication1),
(0xB7, LaunchApplication2),
(0xB4, LaunchMail),
(0xB5, LaunchMediaPlayer),
(0xB5, LaunchMusicPlayer),
(0xA6, BrowserBack),
(0xAB, BrowserFavorites),
(0xA7, BrowserForward),
(0xAC, BrowserHome),
(0xA8, BrowserRefresh),
(0xAA, BrowserSearch),
(0xA9, BrowserStop),
(0xFB, ZoomToggle),
(0x70, F1),
(0x71, F2),
(0x72, F3),
(0x73, F4),
(0x74, F5),
(0x75, F6),
(0x76, F7),
(0x77, F8),
(0x78, F9),
(0x79, F10),
(0x7A, F11),
(0x7B, F12),
(0x7C, F13),
(0x7D, F14),
(0x7E, F15),
(0x7F, F16),
(0x80, F17),
(0x81, F18),
(0x82, F19),
(0x83, F20),
(0x84, F21),
(0x85, F22),
(0x86, F23),
(0x87, F24),
)
}
}
macro_rules! map {
($target:expr, $( ($code:expr, $variant:literal), )+ ) => {
match $target {
$(
$variant => $code,
)+
_ => 0,
}
};
}
impl ToVKBits for char {
fn to_vk_bits(&self) -> i32 {
map!(
self,
(0x41, 'a'),
(0x42, 'b'),
(0x43, 'c'),
(0x44, 'd'),
(0x45, 'e'),
(0x46, 'f'),
(0x47, 'g'),
(0x48, 'h'),
(0x49, 'i'),
(0x4a, 'j'),
(0x4b, 'k'),
(0x4c, 'l'),
(0x4d, 'm'),
(0x4e, 'n'),
(0x4f, 'o'),
(0x50, 'p'),
(0x51, 'q'),
(0x52, 'r'),
(0x53, 's'),
(0x54, 't'),
(0x55, 'u'),
(0x56, 'v'),
(0x57, 'w'),
(0x58, 'x'),
(0x59, 'y'),
(0x5a, 'z'),
(0x41, 'A'),
(0x42, 'B'),
(0x43, 'C'),
(0x44, 'D'),
(0x45, 'E'),
(0x46, 'F'),
(0x47, 'G'),
(0x48, 'H'),
(0x49, 'I'),
(0x4a, 'J'),
(0x4b, 'K'),
(0x4c, 'L'),
(0x4d, 'M'),
(0x4e, 'N'),
(0x4f, 'O'),
(0x50, 'P'),
(0x51, 'Q'),
(0x52, 'R'),
(0x53, 'S'),
(0x54, 'T'),
(0x55, 'U'),
(0x56, 'V'),
(0x57, 'W'),
(0x58, 'X'),
(0x59, 'Y'),
(0x5a, 'Z'),
(0x31, '1'),
(0x32, '2'),
(0x33, '3'),
(0x34, '4'),
(0x35, '5'),
(0x36, '6'),
(0x37, '7'),
(0x38, '8'),
(0x39, '9'),
(0x30, '0'),
(0x31, '!'),
(0x32, '@'),
(0x33, '#'),
(0x34, '$'),
(0x35, '%'),
(0x36, '^'),
(0x37, '&'),
(0x38, '*'),
(0x39, '('),
(0x30, ')'),
(0xC0, '`'),
(0xC0, '~'),
(0xBD, '-'),
(0xBD, '_'),
(0xBB, '='),
(0xBB, '+'),
(0xDB, '['),
(0xDB, '{'),
(0xDD, ']'),
(0xDD, '}'),
(0xDC, '\\'),
(0xDC, '|'),
(0xBA, ';'),
(0xBA, ':'),
(0xBC, ','),
(0xBC, '<'),
(0xBE, '.'),
(0xBE, '>'),
(0xDE, '\''),
(0xDE, '"'),
(0xBF, '/'),
(0xBF, '?'),
(0x20, ' '),
)
}
}

View File

@@ -0,0 +1,256 @@
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 super::MouseData;
use crate::consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT};
#[derive(Default)]
pub(crate) struct InputState {
modifiers: ModifiersState,
mouse_position: MousePosition,
mouse_state: MouseState,
mouse_click_tracker: ClickTracker,
}
impl InputState {
pub(crate) fn modifiers_changed(&mut self, modifiers: &ModifiersState) {
self.modifiers = *modifiers;
}
pub(crate) fn modifiers_apply_key_event(&mut self, key: &Key, state: &ElementState) {
let bits = match key {
Key::Named(NamedKey::Shift) => ModifiersState::SHIFT,
Key::Named(NamedKey::Control) => ModifiersState::CONTROL,
Key::Named(NamedKey::Alt) => ModifiersState::ALT,
Key::Named(NamedKey::Meta) => ModifiersState::META,
_ => return,
};
let is_pressed = matches!(state, ElementState::Pressed);
self.modifiers.set(bits, is_pressed);
}
pub(crate) fn cursor_move(&mut self, position: &PhysicalPosition<f64>) -> bool {
let new = position.into();
if self.mouse_position == new {
return false;
}
self.mouse_position = new;
true
}
pub(crate) fn mouse_input(&mut self, button: &MouseButton, state: &ElementState) -> ClickCount {
self.mouse_state.update(button, state);
self.mouse_click_tracker.input(button, state, self.mouse_position)
}
pub(crate) fn cef_modifiers(&self, location: &KeyLocation, is_repeat: bool) -> CefModifiers {
CefModifiers::new(self, location, is_repeat)
}
pub(crate) fn cef_mouse_modifiers(&self) -> CefModifiers {
self.cef_modifiers(&KeyLocation::Standard, false)
}
pub(crate) fn mouse_data(&self) -> MouseData {
MouseData {
x: self.mouse_position.x,
y: self.mouse_position.y,
modifiers: self.cef_mouse_modifiers().into(),
}
}
}
#[derive(Default, Clone, Copy, Eq, PartialEq)]
pub(crate) struct MousePosition {
x: i32,
y: i32,
}
impl From<&PhysicalPosition<f64>> for MousePosition {
fn from(position: &PhysicalPosition<f64>) -> Self {
Self {
x: position.x as i32,
y: position.y as i32,
}
}
}
#[derive(Default, Clone)]
pub(crate) struct MouseState {
left: bool,
right: bool,
middle: bool,
}
impl MouseState {
pub(crate) fn update(&mut self, button: &MouseButton, state: &ElementState) {
match state {
ElementState::Pressed => match button {
MouseButton::Left => self.left = true,
MouseButton::Right => self.right = true,
MouseButton::Middle => self.middle = true,
_ => {}
},
ElementState::Released => match button {
MouseButton::Left => self.left = false,
MouseButton::Right => self.right = false,
MouseButton::Middle => self.middle = false,
_ => {}
},
}
}
}
#[derive(Default)]
struct ClickTracker {
left: Option<ClickRecord>,
middle: Option<ClickRecord>,
right: Option<ClickRecord>,
}
impl ClickTracker {
fn input(&mut self, button: &MouseButton, state: &ElementState, position: MousePosition) -> ClickCount {
let record = match button {
MouseButton::Left => &mut self.left,
MouseButton::Right => &mut self.right,
MouseButton::Middle => &mut self.middle,
_ => return ClickCount::Single,
};
let Some(record) = record else {
*record = Some(ClickRecord {
down_position: position,
up_position: position,
..Default::default()
});
return ClickCount::Single;
};
let now = Instant::now();
let within_time = now.saturating_duration_since(record.time) <= MULTICLICK_TIMEOUT;
let (prev_count, prev_position) = match state {
ElementState::Pressed => (record.down_count, record.down_position),
ElementState::Released => (record.up_count, record.up_position),
};
let dx = position.x.abs_diff(prev_position.x) as usize;
let dy = position.y.abs_diff(prev_position.y) as usize;
let within_dist = dx <= MULTICLICK_ALLOWED_TRAVEL && dy <= MULTICLICK_ALLOWED_TRAVEL;
let count = match (prev_count, within_time, within_dist) {
(ClickCount::Single, true, true) => ClickCount::Double,
(ClickCount::Double, true, true) => ClickCount::Triple,
(ClickCount::Triple, true, true) => ClickCount::Double,
_ => ClickCount::Single,
};
record.time = now;
match state {
ElementState::Pressed => {
record.down_position = position;
record.down_count = count;
}
ElementState::Released => {
record.up_position = position;
record.up_count = count;
}
}
count
}
}
#[derive(Clone, Copy, PartialEq, Default)]
pub(crate) enum ClickCount {
#[default]
Single,
Double,
Triple,
}
impl From<ClickCount> for i32 {
fn from(count: ClickCount) -> i32 {
match count {
ClickCount::Single => 1,
ClickCount::Double => 2,
ClickCount::Triple => 3,
}
}
}
#[derive(Clone, Copy)]
struct ClickRecord {
time: Instant,
down_position: MousePosition,
up_position: MousePosition,
down_count: ClickCount,
up_count: ClickCount,
}
impl Default for ClickRecord {
fn default() -> Self {
Self {
time: Instant::now(),
down_position: Default::default(),
up_position: Default::default(),
down_count: Default::default(),
up_count: Default::default(),
}
}
}
pub(crate) struct CefModifiers(cef_event_flags_t);
impl CefModifiers {
fn new(input_state: &InputState, location: &KeyLocation, is_repeat: bool) -> Self {
let mut inner = cef_event_flags_t::EVENTFLAG_NONE;
if input_state.modifiers.shift_key() {
inner |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN;
}
if input_state.modifiers.control_key() {
inner |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN;
}
if input_state.modifiers.alt_key() {
inner |= cef_event_flags_t::EVENTFLAG_ALT_DOWN;
}
if input_state.modifiers.meta_key() {
inner |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN;
}
if input_state.mouse_state.left {
inner |= cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON;
}
if input_state.mouse_state.right {
inner |= cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON;
}
if input_state.mouse_state.middle {
inner |= cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON;
}
if is_repeat {
inner |= cef_event_flags_t::EVENTFLAG_IS_REPEAT;
}
inner |= match location {
KeyLocation::Left => cef_event_flags_t::EVENTFLAG_IS_LEFT,
KeyLocation::Right => cef_event_flags_t::EVENTFLAG_IS_RIGHT,
KeyLocation::Numpad => cef_event_flags_t::EVENTFLAG_IS_KEY_PAD,
KeyLocation::Standard => cef_event_flags_t::EVENTFLAG_NONE,
};
Self(inner)
}
pub(super) const PINCH_MODIFIERS: Self = Self(cef_event_flags_t(
cef_event_flags_t::EVENTFLAG_CONTROL_DOWN.0 | cef_event_flags_t::EVENTFLAG_PRECISION_SCROLLING_DELTA.0,
));
}
impl From<CefModifiers> for u32 {
fn from(val: CefModifiers) -> Self {
#[cfg(not(target_os = "windows"))]
return val.0.0;
#[cfg(target_os = "windows")]
return val.0.0 as u32;
}
}

View File

@@ -0,0 +1,25 @@
mod browser_process_app;
mod browser_process_client;
mod browser_process_handler;
mod render_process_app;
mod render_process_handler;
mod render_process_v8_handler;
mod context_menu_handler;
mod display_handler;
mod life_span_handler;
mod load_handler;
mod request_handler;
mod resource_handler;
mod resource_request_handler;
mod scheme_handler_factory;
pub(super) mod render_handler;
pub(super) mod task;
pub(super) use browser_process_app::BrowserProcessAppImpl;
pub(super) use browser_process_client::BrowserProcessClientImpl;
pub(super) use render_process_app::RenderProcessAppImpl;
pub(super) use scheme_handler_factory::SchemeHandlerFactoryImpl;

View File

@@ -0,0 +1,134 @@
use cef::rc::{Rc, RcImpl};
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::register_schemes;
pub(crate) struct BrowserProcessAppImpl {
object: *mut RcImpl<_cef_app_t, Self>,
accelerated_paint: bool,
}
impl BrowserProcessAppImpl {
pub(crate) fn new(accelerated_paint: bool) -> Self {
Self {
object: std::ptr::null_mut(),
accelerated_paint,
}
}
}
impl ImplApp for BrowserProcessAppImpl {
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(BrowserProcessHandler::new(BrowserProcessHandlerImpl::new()))
}
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
register_schemes(registrar);
}
fn on_before_command_line_processing(&self, _process_type: Option<&cef::CefString>, command_line: Option<&mut cef::CommandLine>) {
if let Some(cmd) = command_line {
cmd.append_switch_with_value(Some(&"renderer-process-limit".into()), Some(&"1".into()));
cmd.append_switch_with_value(Some(&"password-store".into()), Some(&"basic".into()));
cmd.append_switch_with_value(Some(&"disk-cache-size".into()), Some(&"0".into()));
cmd.append_switch(Some(&"no-sandbox".into()));
cmd.append_switch(Some(&"no-first-run".into()));
cmd.append_switch(Some(&"noerrdialogs".into()));
cmd.append_switch(Some(&"no-default-browser-check".into()));
cmd.append_switch(Some(&"mute-audio".into()));
cmd.append_switch(Some(&"use-fake-device-for-media-stream".into()));
cmd.append_switch(Some(&"incognito".into()));
cmd.append_switch(Some(&"disable-sync".into()));
cmd.append_switch(Some(&"disable-file-system".into()));
cmd.append_switch(Some(&"disable-component-update".into()));
cmd.append_switch(Some(&"disable-geolocation".into()));
cmd.append_switch(Some(&"disable-notifications".into()));
cmd.append_switch(Some(&"disable-background-networking".into()));
cmd.append_switch(Some(&"disable-default-apps".into()));
cmd.append_switch(Some(&"disable-breakpad".into()));
cmd.append_switch_with_value(Some(&"disable-blink-features".into()), Some(&"WebBluetooth,WebUSB,Serial".into()));
let extra_disabled_features = ["OptimizationHints", "OnDeviceModelService", "TranslateUI"];
let disabled_features_switch = Some(&"disable-features".into());
let mut disabled_features: Vec<String> = CefString::from(&cmd.switch_value(disabled_features_switch))
.to_string()
.split(',')
.filter(|feature| !feature.is_empty())
.map(ToOwned::to_owned)
.collect();
disabled_features.extend(extra_disabled_features.into_iter().map(ToOwned::to_owned));
cmd.append_switch_with_value(disabled_features_switch, Some(&disabled_features.join(",").as_str().into()));
if self.accelerated_paint {
cmd.append_switch(Some(&"enable-gpu".into()));
cmd.append_switch(Some(&"enable-gpu-compositing".into()));
cmd.append_switch(Some(&"enable-begin-frame-scheduling".into()));
cmd.append_switch(Some(&"off-screen-rendering-enabled".into()));
cmd.append_switch(Some(&"enable-accelerated-2d-canvas".into()));
#[cfg(target_os = "linux")]
{
cmd.append_switch_with_value(Some(&"use-angle".into()), Some(&"gl-egl".into()));
let use_wayland = std::env::var("WAYLAND_DISPLAY")
.ok()
.filter(|var| !var.is_empty())
.or_else(|| std::env::var("WAYLAND_SOCKET").ok())
.filter(|var| !var.is_empty())
.is_some();
if use_wayland {
cmd.append_switch_with_value(Some(&"ozone-platform".into()), Some(&"wayland".into()));
}
}
} else {
cmd.append_switch(Some(&"disable-gpu".into()));
cmd.append_switch(Some(&"disable-gpu-compositing".into()));
}
#[cfg(target_os = "macos")]
{
// Hide user prompt asking for keychain access
cmd.append_switch(Some(&"use-mock-keychain".into()));
}
// Enable browser debugging via environment variable
if let Some(env) = std::env::var("GRAPHITE_BROWSER_DEBUG_PORT").ok()
&& let Some(port) = env.parse::<u16>().ok()
{
cmd.append_switch_with_value(Some(&"remote-debugging-port".into()), Some(&port.to_string().as_str().into()));
cmd.append_switch_with_value(Some(&"remote-allow-origins".into()), Some(&"*".into()));
}
}
}
fn get_raw(&self) -> *mut _cef_app_t {
self.object.cast()
}
}
impl Clone for BrowserProcessAppImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
accelerated_paint: self.accelerated_paint,
}
}
}
impl Rc for BrowserProcessAppImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapApp for BrowserProcessAppImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,121 @@
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::delegate::BrowserDelegate;
use crate::frames::FrameStreamer;
use crate::ipc::{MessageType, UnpackMessage, UnpackedMessage};
use super::context_menu_handler::ContextMenuHandlerImpl;
use super::display_handler::DisplayHandlerImpl;
use super::life_span_handler::LifeSpanHandlerImpl;
use super::load_handler::LoadHandlerImpl;
use super::render_handler::RenderHandlerImpl;
use super::request_handler::RequestHandlerImpl;
pub(crate) struct BrowserProcessClientImpl {
object: *mut RcImpl<_cef_client_t, Self>,
delegate: BrowserDelegate,
load_handler: LoadHandler,
render_handler: RenderHandler,
display_handler: DisplayHandler,
request_handler: RequestHandler,
}
impl BrowserProcessClientImpl {
pub(crate) fn new(delegate: &BrowserDelegate, frames: FrameStreamer) -> Self {
Self {
object: std::ptr::null_mut(),
delegate: delegate.clone(),
load_handler: LoadHandler::new(LoadHandlerImpl::new(delegate.clone())),
render_handler: RenderHandler::new(RenderHandlerImpl::new(delegate.clone(), frames)),
display_handler: DisplayHandler::new(DisplayHandlerImpl::new(delegate.clone())),
request_handler: RequestHandler::new(RequestHandlerImpl::new()),
}
}
}
impl ImplClient for BrowserProcessClientImpl {
fn on_process_message_received(
&self,
_browser: Option<&mut cef::Browser>,
_frame: Option<&mut cef::Frame>,
_source_process: cef::ProcessId,
message: Option<&mut cef::ProcessMessage>,
) -> std::ffi::c_int {
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
match unpacked_message {
Some(UnpackedMessage {
message_type: MessageType::Initialized,
data: _,
}) => self.delegate.initialized_web_communication(),
Some(UnpackedMessage {
message_type: MessageType::SendToNative,
data,
}) => self.delegate.receive_web_message(data),
_ => {
tracing::error!("Unexpected message type received in browser process");
return 0;
}
}
1
}
fn load_handler(&self) -> Option<cef::LoadHandler> {
Some(self.load_handler.clone())
}
fn render_handler(&self) -> Option<RenderHandler> {
Some(self.render_handler.clone())
}
fn life_span_handler(&self) -> Option<cef::LifeSpanHandler> {
Some(LifeSpanHandler::new(LifeSpanHandlerImpl::new()))
}
fn display_handler(&self) -> Option<cef::DisplayHandler> {
Some(self.display_handler.clone())
}
fn request_handler(&self) -> Option<cef::RequestHandler> {
Some(self.request_handler.clone())
}
fn context_menu_handler(&self) -> Option<cef::ContextMenuHandler> {
Some(ContextMenuHandler::new(ContextMenuHandlerImpl::new()))
}
fn get_raw(&self) -> *mut _cef_client_t {
self.object.cast()
}
}
impl Clone for BrowserProcessClientImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
delegate: self.delegate.clone(),
load_handler: self.load_handler.clone(),
render_handler: self.render_handler.clone(),
display_handler: self.display_handler.clone(),
request_handler: self.request_handler.clone(),
}
}
}
impl Rc for BrowserProcessClientImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapClient for BrowserProcessClientImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_client_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,45 @@
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};
pub(crate) struct BrowserProcessHandlerImpl {
object: *mut RcImpl<cef_browser_process_handler_t, Self>,
}
impl BrowserProcessHandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
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
}
fn get_raw(&self) -> *mut _cef_browser_process_handler_t {
self.object.cast()
}
}
impl Clone for BrowserProcessHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl Rc for BrowserProcessHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapBrowserProcessHandler for BrowserProcessHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_browser_process_handler_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,66 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_context_menu_handler_t, cef_base_ref_counted_t};
use cef::{ImplContextMenuHandler, WrapContextMenuHandler};
pub(crate) struct ContextMenuHandlerImpl {
object: *mut RcImpl<_cef_context_menu_handler_t, Self>,
}
impl ContextMenuHandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
impl ImplContextMenuHandler for ContextMenuHandlerImpl {
fn run_context_menu(
&self,
_browser: Option<&mut cef::Browser>,
_frame: Option<&mut cef::Frame>,
_params: Option<&mut cef::ContextMenuParams>,
_model: Option<&mut cef::MenuModel>,
_callback: Option<&mut cef::RunContextMenuCallback>,
) -> std::ffi::c_int {
// Prevent context menu
1
}
fn run_quick_menu(
&self,
_browser: Option<&mut cef::Browser>,
_frame: Option<&mut cef::Frame>,
_location: Option<&cef::Point>,
_size: Option<&cef::Size>,
_edit_state_flags: cef::QuickMenuEditStateFlags,
_callback: Option<&mut cef::RunQuickMenuCallback>,
) -> std::ffi::c_int {
// Prevent quick menu
1
}
fn get_raw(&self) -> *mut _cef_context_menu_handler_t {
self.object.cast()
}
}
impl Clone for ContextMenuHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl Rc for ContextMenuHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapContextMenuHandler for ContextMenuHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_context_menu_handler_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,155 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_display_handler_t, cef_base_ref_counted_t, cef_cursor_type_t::*, cef_log_severity_t::*};
use cef::{CefString, ImplDisplayHandler, Point, Size, WrapDisplayHandler};
use winit::cursor::CursorIcon;
use crate::delegate::BrowserDelegate;
pub(crate) struct DisplayHandlerImpl {
object: *mut RcImpl<_cef_display_handler_t, Self>,
delegate: BrowserDelegate,
}
impl DisplayHandlerImpl {
pub fn new(delegate: BrowserDelegate) -> Self {
Self {
object: std::ptr::null_mut(),
delegate,
}
}
}
#[cfg(not(target_os = "macos"))]
type CefCursorHandle = cef::CursorHandle;
#[cfg(target_os = "macos")]
type CefCursorHandle = *mut u8;
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;
let Point { x: hotspot_x, y: hotspot_y } = custom_cursor_info.hotspot;
let buffer_size = (width * height * 4) as usize;
let buffer_ptr = custom_cursor_info.buffer as *const u8;
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();
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.
}
}
let cursor = match cursor_type.into() {
CT_POINTER => CursorIcon::Default,
CT_CROSS => CursorIcon::Crosshair,
CT_HAND => CursorIcon::Pointer,
CT_IBEAM => CursorIcon::Text,
CT_WAIT => CursorIcon::Wait,
CT_HELP => CursorIcon::Help,
CT_EASTRESIZE => CursorIcon::EResize,
CT_NORTHRESIZE => CursorIcon::NResize,
CT_NORTHEASTRESIZE => CursorIcon::NeResize,
CT_NORTHWESTRESIZE => CursorIcon::NwResize,
CT_SOUTHRESIZE => CursorIcon::SResize,
CT_SOUTHEASTRESIZE => CursorIcon::SeResize,
CT_SOUTHWESTRESIZE => CursorIcon::SwResize,
CT_WESTRESIZE => CursorIcon::WResize,
CT_NORTHSOUTHRESIZE => CursorIcon::NsResize,
CT_EASTWESTRESIZE => CursorIcon::EwResize,
CT_NORTHEASTSOUTHWESTRESIZE => CursorIcon::NeswResize,
CT_NORTHWESTSOUTHEASTRESIZE => CursorIcon::NwseResize,
CT_COLUMNRESIZE => CursorIcon::ColResize,
CT_ROWRESIZE => CursorIcon::RowResize,
CT_MIDDLEPANNING => CursorIcon::AllScroll,
CT_EASTPANNING => CursorIcon::AllScroll,
CT_NORTHPANNING => CursorIcon::AllScroll,
CT_NORTHEASTPANNING => CursorIcon::AllScroll,
CT_NORTHWESTPANNING => CursorIcon::AllScroll,
CT_SOUTHPANNING => CursorIcon::AllScroll,
CT_SOUTHEASTPANNING => CursorIcon::AllScroll,
CT_SOUTHWESTPANNING => CursorIcon::AllScroll,
CT_WESTPANNING => CursorIcon::AllScroll,
CT_MOVE => CursorIcon::Move,
CT_VERTICALTEXT => CursorIcon::VerticalText,
CT_CELL => CursorIcon::Cell,
CT_CONTEXTMENU => CursorIcon::ContextMenu,
CT_ALIAS => CursorIcon::Alias,
CT_PROGRESS => CursorIcon::Progress,
CT_NODROP => CursorIcon::NoDrop,
CT_COPY => CursorIcon::Copy,
CT_NOTALLOWED => CursorIcon::NotAllowed,
CT_ZOOMIN => CursorIcon::ZoomIn,
CT_ZOOMOUT => CursorIcon::ZoomOut,
CT_GRAB => CursorIcon::Grab,
CT_GRABBING => CursorIcon::Grabbing,
CT_MIDDLE_PANNING_VERTICAL => CursorIcon::AllScroll,
CT_MIDDLE_PANNING_HORIZONTAL => CursorIcon::AllScroll,
CT_DND_NONE => CursorIcon::Default,
CT_DND_MOVE => CursorIcon::Move,
CT_DND_COPY => CursorIcon::Copy,
CT_DND_LINK => CursorIcon::Alias,
CT_NUM_VALUES => CursorIcon::Default,
CT_NONE => {
self.delegate.cursor_change(crate::Cursor::None);
return 1; // We handled the cursor change.
}
_ => CursorIcon::Default,
};
self.delegate.cursor_change(cursor.into());
1 // We handled the cursor change.
}
fn on_console_message(&self, _browser: Option<&mut cef::Browser>, level: cef::LogSeverity, message: Option<&CefString>, source: Option<&CefString>, line: std::ffi::c_int) -> std::ffi::c_int {
let message = message.map(|m| m.to_string()).unwrap_or_default();
let source = source.map(|s| s.to_string()).unwrap_or_default();
let line = line as i64;
let browser_source = format!("{source}:{line}");
static BROWSER: &str = "browser";
match level.as_ref() {
LOGSEVERITY_FATAL | LOGSEVERITY_ERROR => tracing::error!(target: BROWSER, "{browser_source} {message}"),
LOGSEVERITY_WARNING => tracing::warn!(target: BROWSER, "{browser_source} {message}"),
LOGSEVERITY_INFO => tracing::info!(target: BROWSER, "{browser_source} {message}"),
LOGSEVERITY_DEFAULT | LOGSEVERITY_VERBOSE => tracing::debug!(target: BROWSER, "{browser_source} {message}"),
_ => tracing::trace!(target: BROWSER, "{browser_source} {message}"),
}
0
}
fn get_raw(&self) -> *mut _cef_display_handler_t {
self.object.cast()
}
}
impl Clone for DisplayHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
delegate: self.delegate.clone(),
}
}
}
impl Rc for DisplayHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapDisplayHandler for DisplayHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_display_handler_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,64 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_life_span_handler_t, cef_base_ref_counted_t};
use cef::{ImplLifeSpanHandler, WrapLifeSpanHandler};
pub(crate) struct LifeSpanHandlerImpl {
object: *mut RcImpl<_cef_life_span_handler_t, Self>,
}
impl LifeSpanHandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
impl ImplLifeSpanHandler for LifeSpanHandlerImpl {
fn on_before_popup(
&self,
_browser: Option<&mut cef::Browser>,
_frame: Option<&mut cef::Frame>,
_popup_id: std::ffi::c_int,
target_url: Option<&cef::CefString>,
_target_frame_name: Option<&cef::CefString>,
_target_disposition: cef::WindowOpenDisposition,
_user_gesture: std::ffi::c_int,
_popup_features: Option<&cef::PopupFeatures>,
_window_info: Option<&mut cef::WindowInfo>,
_client: Option<&mut Option<cef::Client>>,
_settings: Option<&mut cef::BrowserSettings>,
_extra_info: Option<&mut Option<cef::DictionaryValue>>,
_no_javascript_access: Option<&mut std::ffi::c_int>,
) -> std::ffi::c_int {
let target = target_url.map(|url| url.to_string()).unwrap_or("unknown".to_string());
tracing::error!("Browser tried to open a popup at URL: {}", target);
// Deny any popup by returning 1
1
}
fn get_raw(&self) -> *mut _cef_life_span_handler_t {
self.object.cast()
}
}
impl Clone for LifeSpanHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl Rc for LifeSpanHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapLifeSpanHandler for LifeSpanHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_life_span_handler_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,60 @@
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::delegate::BrowserDelegate;
pub(crate) struct LoadHandlerImpl {
object: *mut RcImpl<cef_load_handler_t, Self>,
delegate: BrowserDelegate,
}
impl LoadHandlerImpl {
pub(crate) fn new(delegate: BrowserDelegate) -> Self {
Self {
object: std::ptr::null_mut(),
delegate,
}
}
}
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.delegate.view_info();
if let Some(browser) = browser
&& is_loading == 0
{
browser.host().unwrap().set_zoom_level(view_info.zoom());
}
}
fn get_raw(&self) -> *mut _cef_load_handler_t {
self.object.cast()
}
}
impl Clone for LoadHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
delegate: self.delegate.clone(),
}
}
}
impl Rc for LoadHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapLoadHandler for LoadHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_load_handler_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,92 @@
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::delegate::BrowserDelegate;
use crate::frames::FrameStreamer;
pub(crate) struct RenderHandlerImpl {
object: *mut RcImpl<_cef_render_handler_t, Self>,
delegate: BrowserDelegate,
frames: FrameStreamer,
}
impl RenderHandlerImpl {
pub(crate) fn new(delegate: BrowserDelegate, frames: FrameStreamer) -> Self {
Self {
object: std::ptr::null_mut(),
delegate,
frames,
}
}
}
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.delegate.view_info();
*rect = Rect {
x: 0,
y: 0,
width: view_info.width() as i32,
height: view_info.height() as i32,
};
}
}
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;
}
let buffer_size = (width * height * 4) as usize;
let buffer_slice = unsafe { std::slice::from_raw_parts(buffer, buffer_size) };
self.frames.stage_buffer(buffer_slice, width as u32, height as u32);
self.frames.publish();
}
#[cfg(feature = "accelerated_paint")]
fn on_accelerated_paint(&self, _browser: Option<&mut Browser>, type_: PaintElementType, _dirty_rects: Option<&[Rect]>, info: Option<&cef::AcceleratedPaintInfo>) {
if type_ != PaintElementType::default() {
return;
}
let Some(info) = info else {
tracing::error!("Accelerated paint callback received no info about the painted frame");
return;
};
self.frames.stage_texture(info);
self.frames.publish();
}
fn get_raw(&self) -> *mut _cef_render_handler_t {
self.object.cast()
}
}
impl Clone for RenderHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
delegate: self.delegate.clone(),
frames: self.frames.clone(),
}
}
}
impl Rc for RenderHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapRenderHandler for RenderHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_handler_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,59 @@
use cef::rc::{Rc, RcImpl};
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::register_schemes;
pub(crate) struct RenderProcessAppImpl {
object: *mut RcImpl<_cef_app_t, Self>,
render_process_handler: RenderProcessHandler,
}
impl RenderProcessAppImpl {
pub(crate) fn app() -> App {
App::new(Self {
object: std::ptr::null_mut(),
render_process_handler: RenderProcessHandler::new(RenderProcessHandlerImpl::new()),
})
}
}
impl ImplApp for RenderProcessAppImpl {
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
register_schemes(registrar);
}
fn render_process_handler(&self) -> Option<RenderProcessHandler> {
Some(self.render_process_handler.clone())
}
fn get_raw(&self) -> *mut _cef_app_t {
self.object.cast()
}
}
impl Clone for RenderProcessAppImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
render_process_handler: self.render_process_handler.clone(),
}
}
}
impl Rc for RenderProcessAppImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapApp for RenderProcessAppImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,127 @@
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::ipc::{MessageType, UnpackMessage, UnpackedMessage};
use super::render_process_v8_handler::RenderProcessV8HandlerImpl;
pub(crate) struct RenderProcessHandlerImpl {
object: *mut RcImpl<cef_render_process_handler_t, Self>,
}
impl RenderProcessHandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
impl ImplRenderProcessHandler for RenderProcessHandlerImpl {
fn on_process_message_received(
&self,
_browser: Option<&mut cef::Browser>,
frame: Option<&mut cef::Frame>,
_source_process: cef::ProcessId,
message: Option<&mut cef::ProcessMessage>,
) -> std::ffi::c_int {
let unpacked_message = unsafe { message.and_then(|m| m.unpack()) };
match unpacked_message {
Some(UnpackedMessage {
message_type: MessageType::SendToJS,
data,
}) => {
let Some(frame) = frame else {
tracing::error!("Frame is not available");
return 0;
};
let Some(context) = frame.v8_context() else {
tracing::error!("V8 context is not available");
return 0;
};
if context.enter() == 0 {
tracing::error!("Failed to enter V8 context");
return 0;
}
let mut value: V8Value = unsafe { cef_v8_value_create_array_buffer_with_copy(data.as_ptr() as *mut std::ffi::c_void, data.len()) }.wrap_result();
let Some(global) = context.global() else {
tracing::error!("Global object is not available in V8 context");
return 0;
};
let function_name = "receiveNativeMessage";
let property_name = "receiveNativeMessageData";
let function_call = format!("window.{function_name}(window.{property_name})");
global.set_value_bykey(Some(&property_name.into()), Some(&mut value), cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_READONLY.wrap_result());
if global.value_bykey(Some(&function_name.into())).is_some() {
frame.execute_java_script(Some(&function_call.as_str().into()), None, 0);
}
if context.exit() == 0 {
tracing::error!("Failed to exit V8 context");
return 0;
}
}
_ => {
tracing::error!("Unexpected message type received in render process");
return 0;
}
}
1
}
fn on_context_created(&self, _browser: Option<&mut cef::Browser>, _frame: Option<&mut cef::Frame>, context: Option<&mut cef::V8Context>) {
let register_js_function = |context: &mut cef::V8Context, name: &'static str| {
let mut v8_handler = V8Handler::new(RenderProcessV8HandlerImpl::new());
let Some(mut function) = v8_value_create_function(Some(&name.into()), Some(&mut v8_handler)) else {
tracing::error!("Failed to create V8 function {name}");
return;
};
let Some(global) = context.global() else {
tracing::error!("Global object is not available in V8 context");
return;
};
global.set_value_bykey(Some(&name.into()), Some(&mut function), V8Propertyattribute::default());
};
let Some(context) = context else {
tracing::error!("V8 context is not available");
return;
};
let initialized_function_name = "initializeNativeCommunication";
let send_function_name = "sendNativeMessage";
register_js_function(context, initialized_function_name);
register_js_function(context, send_function_name);
}
fn get_raw(&self) -> *mut _cef_render_process_handler_t {
self.object.cast()
}
}
impl Clone for RenderProcessHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl Rc for RenderProcessHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapRenderProcessHandler for RenderProcessHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_render_process_handler_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,86 @@
use cef::{ImplV8Handler, ImplV8Value, V8Value, WrapV8Handler, rc::Rc, v8_context_get_current_context};
use crate::ipc::{MessageType, SendMessage};
pub struct RenderProcessV8HandlerImpl {
object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>,
}
impl RenderProcessV8HandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
impl ImplV8Handler for RenderProcessV8HandlerImpl {
fn execute(
&self,
name: Option<&cef::CefString>,
_object: Option<&mut V8Value>,
arguments: Option<&[Option<V8Value>]>,
_retval: Option<&mut Option<V8Value>>,
_exception: Option<&mut cef::CefString>,
) -> std::ffi::c_int {
match name.map(|s| s.to_string()).unwrap_or_default().as_str() {
"initializeNativeCommunication" => {
v8_context_get_current_context().send_message(MessageType::Initialized, vec![0u8].as_slice());
}
"sendNativeMessage" => {
let Some(args) = arguments else {
tracing::error!("No arguments provided to sendNativeMessage");
return 0;
};
let Some(arg1) = args.first() else {
tracing::error!("No arguments provided to sendNativeMessage");
return 0;
};
let Some(arg1) = arg1.as_ref() else {
tracing::error!("First argument to sendNativeMessage is not an ArrayBuffer");
return 0;
};
if arg1.is_array_buffer() == 0 {
tracing::error!("First argument to sendNativeMessage is not an ArrayBuffer");
return 0;
}
let size = arg1.array_buffer_byte_length();
let ptr = arg1.array_buffer_data();
let data = unsafe { std::slice::from_raw_parts_mut(ptr as *mut u8, size) };
v8_context_get_current_context().send_message(MessageType::SendToNative, data);
return 1;
}
name => {
tracing::error!("Unknown V8 function called: {}", name);
}
}
1
}
fn get_raw(&self) -> *mut cef::sys::_cef_v8_handler_t {
self.object.cast()
}
}
impl Clone for RenderProcessV8HandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl Rc for RenderProcessV8HandlerImpl {
fn as_base(&self) -> &cef::sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapV8Handler for RenderProcessV8HandlerImpl {
fn wrap_rc(&mut self, object: *mut cef::rc::RcImpl<cef::sys::_cef_v8_handler_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,84 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_request_handler_t, cef_base_ref_counted_t};
use cef::{AuthCallback, Browser, CefString, Frame, ImplRequest, ImplRequestHandler, Request, ResourceRequestHandler, WrapRequestHandler};
use std::ffi::c_int;
use super::resource_request_handler::ResourceRequestHandlerImpl;
use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
pub(crate) struct RequestHandlerImpl {
object: *mut RcImpl<_cef_request_handler_t, Self>,
}
impl RequestHandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
impl ImplRequestHandler for RequestHandlerImpl {
fn on_before_browse(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, request: Option<&mut Request>, _user_gesture: c_int, _is_redirect: c_int) -> c_int {
let Some(request) = request else { return 1 };
let url = CefString::from(&request.url()).to_string();
if url.starts_with(&format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/")) {
0
} else {
tracing::warn!("Blocked navigation to: {}", url);
1
}
}
fn resource_request_handler(
&self,
_browser: Option<&mut Browser>,
_frame: Option<&mut Frame>,
_request: Option<&mut Request>,
_is_navigation: c_int,
_is_download: c_int,
_request_initiator: Option<&CefString>,
_disable_default_handling: Option<&mut c_int>,
) -> Option<ResourceRequestHandler> {
Some(ResourceRequestHandler::new(ResourceRequestHandlerImpl::new()))
}
fn auth_credentials(
&self,
_browser: Option<&mut Browser>,
_origin_url: Option<&CefString>,
_is_proxy: c_int,
_host: Option<&CefString>,
_port: c_int,
_realm: Option<&CefString>,
_scheme: Option<&CefString>,
_callback: Option<&mut AuthCallback>,
) -> c_int {
0
}
fn get_raw(&self) -> *mut _cef_request_handler_t {
self.object.cast()
}
}
impl Clone for RequestHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl Rc for RequestHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapRequestHandler for RequestHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_request_handler_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,107 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_resource_handler_t, cef_base_ref_counted_t};
use cef::{Callback, CefString, ImplResourceHandler, ImplResponse, Request, ResourceReadCallback, Response, WrapResourceHandler};
use std::cell::RefCell;
use std::ffi::c_int;
use std::io::Read;
use crate::resources::{Resource, ResourceReader};
pub(crate) struct ResourceHandlerImpl {
object: *mut RcImpl<_cef_resource_handler_t, Self>,
reader: Option<RefCell<ResourceReader>>,
mimetype: Option<String>,
}
impl ResourceHandlerImpl {
pub fn new(resource: Option<Resource>) -> Self {
if let Some(resource) = resource {
Self {
object: std::ptr::null_mut(),
reader: Some(resource.reader.into()),
mimetype: resource.mimetype,
}
} else {
Self {
object: std::ptr::null_mut(),
reader: None,
mimetype: None,
}
}
}
}
impl ImplResourceHandler for ResourceHandlerImpl {
fn open(&self, _request: Option<&mut Request>, handle_request: Option<&mut c_int>, _callback: Option<&mut Callback>) -> c_int {
if let Some(handle_request) = handle_request {
*handle_request = 1;
}
1
}
fn response_headers(&self, response: Option<&mut Response>, response_length: Option<&mut i64>, _redirect_url: Option<&mut CefString>) {
if let Some(response_length) = response_length {
*response_length = -1; // Indicating that the length is unknown
}
if let Some(response) = response {
if self.reader.is_some() {
if let Some(mimetype) = &self.mimetype {
response.set_mime_type(Some(&mimetype.as_str().into()));
} else {
response.set_mime_type(None);
}
response.set_status(200);
} else {
response.set_status(404);
response.set_mime_type(Some(&"text/plain".into()));
}
}
}
fn read(&self, data_out: *mut u8, bytes_to_read: c_int, bytes_read: Option<&mut c_int>, _callback: Option<&mut ResourceReadCallback>) -> c_int {
let Some(bytes_read) = bytes_read else { unreachable!() };
let out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read as usize) };
if let Some(reader) = &self.reader {
if let Ok(read) = reader.borrow_mut().read(out) {
*bytes_read = read as i32;
if read > 0 {
return 1; // Indicating that data was read
}
} else {
*bytes_read = -2; // Indicating ERR_FAILED
}
}
0 // Indicating no data was read
}
fn get_raw(&self) -> *mut _cef_resource_handler_t {
self.object.cast()
}
}
impl Clone for ResourceHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
reader: self.reader.clone(),
mimetype: self.mimetype.clone(),
}
}
}
impl Rc for ResourceHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapResourceHandler for ResourceHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_resource_handler_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,60 @@
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::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 {
url.starts_with(&format!("{RESOURCE_SCHEME}://{RESOURCE_DOMAIN}/")) || url.starts_with("https://fonts.googleapis.com/css2") || url.starts_with("https://fonts.gstatic.com/")
}
pub(crate) struct ResourceRequestHandlerImpl {
object: *mut RcImpl<_cef_resource_request_handler_t, Self>,
}
impl ResourceRequestHandlerImpl {
pub(crate) fn new() -> Self {
Self { object: std::ptr::null_mut() }
}
}
impl ImplResourceRequestHandler for ResourceRequestHandlerImpl {
fn on_before_resource_load(&self, _browser: Option<&mut Browser>, _frame: Option<&mut Frame>, request: Option<&mut Request>, _callback: Option<&mut Callback>) -> ReturnValue {
let Some(request) = request else { return ReturnValue::CANCEL };
let url = CefString::from(&request.url()).to_string();
if is_allowed_url(&url) {
ReturnValue::CONTINUE
} else {
tracing::error!("Blocked resource load: {}", url);
ReturnValue::CANCEL
}
}
fn get_raw(&self) -> *mut _cef_resource_request_handler_t {
self.object.cast()
}
}
impl Clone for ResourceRequestHandlerImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self { object: self.object }
}
}
impl Rc for ResourceRequestHandlerImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapResourceRequestHandler for ResourceRequestHandlerImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_resource_request_handler_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,74 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_scheme_handler_factory_t, cef_base_ref_counted_t, cef_scheme_options_t};
use cef::{Browser, CefString, Frame, ImplRequest, ImplSchemeHandlerFactory, ImplSchemeRegistrar, Request, ResourceHandler, SchemeRegistrar, WrapSchemeHandlerFactory};
use super::resource_handler::ResourceHandlerImpl;
use crate::consts::{RESOURCE_DOMAIN, RESOURCE_SCHEME};
use crate::delegate::BrowserDelegate;
pub(crate) struct SchemeHandlerFactoryImpl {
object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>,
delegate: BrowserDelegate,
}
impl SchemeHandlerFactoryImpl {
pub(crate) fn new(delegate: BrowserDelegate) -> Self {
Self {
object: std::ptr::null_mut(),
delegate,
}
}
}
pub(crate) fn register_schemes(registrar: Option<&mut SchemeRegistrar>) {
if let Some(registrar) = registrar {
let mut scheme_options = 0;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32;
scheme_options |= cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32;
registrar.add_custom_scheme(Some(&RESOURCE_SCHEME.into()), scheme_options);
}
}
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.delegate.load_resource(path.to_string().into());
return Some(ResourceHandler::new(ResourceHandlerImpl::new(resource)));
}
None
}
fn get_raw(&self) -> *mut _cef_scheme_handler_factory_t {
self.object.cast()
}
}
impl Clone for SchemeHandlerFactoryImpl {
fn clone(&self) -> Self {
unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
Self {
object: self.object,
delegate: self.delegate.clone(),
}
}
}
impl Rc for SchemeHandlerFactoryImpl {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapSchemeHandlerFactory for SchemeHandlerFactoryImpl {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_scheme_handler_factory_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,61 @@
use cef::rc::{Rc, RcImpl};
use cef::sys::{_cef_task_t, cef_base_ref_counted_t};
use cef::{ImplTask, WrapTask};
use std::cell::RefCell;
// Closure-based task wrapper following CEF patterns
pub struct ClosureTask<F> {
pub(crate) object: *mut RcImpl<_cef_task_t, Self>,
pub(crate) closure: RefCell<Option<F>>,
}
impl<F: FnOnce() + Send + 'static> ClosureTask<F> {
pub fn new(closure: F) -> Self {
Self {
object: std::ptr::null_mut(),
closure: RefCell::new(Some(closure)),
}
}
}
impl<F: FnOnce() + Send + 'static> ImplTask for ClosureTask<F> {
fn execute(&self) {
if let Some(closure) = self.closure.borrow_mut().take() {
closure();
}
}
fn get_raw(&self) -> *mut _cef_task_t {
self.object.cast()
}
}
impl<F: FnOnce() + Send + 'static> Clone for ClosureTask<F> {
fn clone(&self) -> Self {
unsafe {
if !self.object.is_null() {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
}
}
Self {
object: self.object,
closure: RefCell::new(None), // Closure can only be executed once
}
}
}
impl<F: FnOnce() + Send + 'static> Rc for ClosureTask<F> {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl<F: FnOnce() + Send + 'static> WrapTask for ClosureTask<F> {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_task_t, Self>) {
self.object = object;
}
}

114
desktop/ui/src/ipc.rs Normal file
View File

@@ -0,0 +1,114 @@
// TODO: Consider inlining this file into internal
use cef::{Frame, ImplBinaryValue, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ProcessId, V8Context, sys::cef_process_id_t};
pub(crate) enum MessageType {
Initialized,
SendToJS,
SendToNative,
}
impl From<MessageType> for MessageInfo {
fn from(val: MessageType) -> Self {
match val {
MessageType::Initialized => MessageInfo {
name: "initialized".to_string(),
target: cef_process_id_t::PID_BROWSER.into(),
},
MessageType::SendToJS => MessageInfo {
name: "send_to_js".to_string(),
target: cef_process_id_t::PID_RENDERER.into(),
},
MessageType::SendToNative => MessageInfo {
name: "send_to_native".to_string(),
target: cef_process_id_t::PID_BROWSER.into(),
},
}
}
}
impl TryFrom<String> for MessageType {
type Error = ();
fn try_from(value: String) -> Result<Self, Self::Error> {
match value.as_str() {
"initialized" => Ok(MessageType::Initialized),
"send_to_js" => Ok(MessageType::SendToJS),
"send_to_native" => Ok(MessageType::SendToNative),
_ => Err(()),
}
}
}
pub(crate) struct MessageInfo {
name: String,
target: ProcessId,
}
pub(crate) trait SendMessage {
fn send_message(&self, message_type: MessageType, message: &[u8]);
}
impl SendMessage for Option<V8Context> {
fn send_message(&self, message_type: MessageType, message: &[u8]) {
let Some(context) = self else {
tracing::error!("Current V8 context is not available, cannot send message");
return;
};
context.send_message(message_type, message);
}
}
impl SendMessage for V8Context {
fn send_message(&self, message_type: MessageType, message: &[u8]) {
let Some(frame) = self.frame() else {
tracing::error!("Current V8 context does not have a frame, cannot send message");
return;
};
frame.send_message(message_type, message);
}
}
impl SendMessage for Frame {
fn send_message(&self, message_type: MessageType, message: &[u8]) {
let MessageInfo { name, target } = message_type.into();
let Some(mut process_message) = cef::process_message_create(Some(&name.as_str().into())) else {
tracing::error!("Failed to create process message: {}", name);
return;
};
let Some(arg_list) = process_message.argument_list() else { return };
let mut value = ::cef::binary_value_create(Some(message));
arg_list.set_binary(0, value.as_mut());
self.send_process_message(target, Some(&mut process_message));
}
}
pub(crate) struct UnpackedMessage<'a> {
pub(crate) message_type: MessageType,
pub(crate) data: &'a [u8],
}
trait Sealed {}
impl Sealed for cef::ProcessMessage {}
#[allow(private_bounds)]
pub(crate) trait UnpackMessage: Sealed {
/// # Safety
///
/// The caller must ensure that the message is valid.
/// Message should come from cef.
unsafe fn unpack(&self) -> Option<UnpackedMessage<'_>>;
}
impl UnpackMessage for cef::ProcessMessage {
unsafe fn unpack(&self) -> Option<UnpackedMessage<'_>> {
let pointer: *mut cef::sys::_cef_string_utf16_t = self.name().into();
let message = unsafe { super::utility::pointer_to_string(pointer) };
let Ok(message_type) = message.try_into() else {
tracing::error!("Failed to get message type from process message");
return None;
};
let arglist = self.argument_list()?;
let binary = arglist.binary(0)?;
let size = binary.size();
let ptr = binary.raw_data();
let buffer = unsafe { std::slice::from_raw_parts(ptr as *const u8, size) };
Some(UnpackedMessage { message_type, data: buffer })
}
}

251
desktop/ui/src/lib.rs Normal file
View File

@@ -0,0 +1,251 @@
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 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>),
Failure(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

@@ -0,0 +1,78 @@
#[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")]
fn should_enable_hardware_acceleration() -> bool {
#[cfg(target_os = "linux")]
{
// Check if running on Wayland or X11
let has_wayland = std::env::var("WAYLAND_DISPLAY")
.ok()
.filter(|var| !var.is_empty())
.or_else(|| std::env::var("WAYLAND_SOCKET").ok())
.filter(|var| !var.is_empty())
.is_some();
let has_x11 = std::env::var("DISPLAY").ok().filter(|var| !var.is_empty()).is_some();
if !has_wayland && !has_x11 {
tracing::warn!("No display server detected, disabling hardware acceleration");
return false;
}
// Check for NVIDIA proprietary driver (known to have issues)
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
if has_wayland {
tracing::info!("Wayland detected, enabling hardware acceleration");
true
} else if has_x11 {
tracing::info!("X11 detected, enabling hardware acceleration");
true
} else {
false
}
}
#[cfg(target_os = "windows")]
{
// Windows generally has good D3D11 support
tracing::info!("Windows detected, enabling hardware acceleration");
true
}
#[cfg(target_os = "macos")]
{
// macOS has good Metal/IOSurface support
tracing::info!("macOS detected, enabling hardware acceleration");
true
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
tracing::warn!("Unsupported platform for hardware acceleration");
false
}
}

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 main_pid = std::process::id() as libc::pid_t;
// SAFETY: the closure runs in the forked child before exec and only makes async-signal-safe calls
unsafe {
command.pre_exec(move || {
// Tie the host lifetime to the main process
if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) != 0 {
return Err(std::io::Error::last_os_error());
}
if libc::getppid() != main_pid {
return Err(std::io::Error::from_raw_os_error(libc::ESRCH));
}
// 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!("Parent process is gone, exiting...");
std::process::exit(0);
}
std::thread::sleep(Duration::from_millis(500));
}
});
if let Err(e) = result {
tracing::error!("Failed to spawn 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,115 @@
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),
}
}
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,51 @@
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,
content: Option<crate::frames::import::ContentRect>,
},
ShutdownComplete,
}

View File

@@ -0,0 +1,401 @@
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(any(not(feature = "accelerated_paint"), target_os = "windows"), 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_tx, hello_rx) = mpsc::channel();
let accept_thread = std::thread::Builder::new().name("cef-host-accept".to_string()).spawn(move || {
let _ = hello_tx.send(server.accept());
});
if let Err(e) = accept_thread {
let _ = child.kill();
let _ = child.wait();
return Err(UiError::Bootstrap(format!("failed to spawn the host accept thread: {e}")));
}
let deadline = Instant::now() + HOST_HELLO_TIMEOUT;
let (event_receiver, hello) = loop {
match hello_rx.recv_timeout(Duration::from_millis(100)) {
Ok(Ok(accepted)) => break accepted,
Ok(Err(e)) => {
let _ = child.kill();
let _ = child.wait();
return Err(UiError::Handshake(format!("failed to accept the host connection: {e}")));
}
Err(RecvTimeoutError::Timeout) => {
if let Ok(Some(status)) = child.try_wait() {
return Err(UiError::HostExited(status.to_string()));
}
if Instant::now() >= deadline {
let _ = child.kill();
let _ = child.wait();
return Err(UiError::HandshakeTimeout);
}
}
Err(RecvTimeoutError::Disconnected) => {
let _ = child.kill();
let _ = child.wait();
return Err(UiError::Handshake("the accept thread disappeared".to_string()));
}
}
};
let EventMessage::Hello {
pid,
control_sender,
acceleration: host_acceleration,
} = hello
else {
let _ = child.kill();
let _ = child.wait();
return Err(UiError::Handshake("the first message from the host was not Hello".to_string()));
};
tracing::info!("CEF host process connected (pid {pid})");
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))
.map_err(|e| UiError::Bootstrap(format!("failed to spawn the frame receiver thread: {e}")))?;
}
{
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))
.map_err(|e| UiError::Bootstrap(format!("failed to spawn the host event receiver thread: {e}")))?;
}
{
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,
}
}
})
.map_err(|e| UiError::Bootstrap(format!("failed to spawn the host supervisor thread: {e}")))?;
}
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::Failure(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,
content,
} => {
let frame = PendingFrame::Accelerated(plane::WireFrame::new(seq, handle, width, height, format, content));
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(());
}
}
}

111
desktop/ui/src/resources.rs Normal file
View File

@@ -0,0 +1,111 @@
use std::fs::File;
#[cfg(feature = "embedded_resources")]
use std::io;
use std::io::Read;
use std::path::{Component, 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> {
if path.components().any(|component| matches!(component, Component::ParentDir)) {
tracing::error!("Rejected resource path with a parent directory component: {path:?}");
return None;
}
let resources = if cfg!(feature = "embedded_resources") {
WebResources::Embedded
} else {
match std::env::var("GRAPHITE_RESOURCES") {
Ok(dir) => WebResources::External(dir.into()),
Err(_) => {
tracing::error!("GRAPHITE_RESOURCES must point to the frontend assets when embedded resources are disabled");
return None;
}
}
};
let path = if path.as_os_str().is_empty() { PathBuf::from("index.html") } else { path };
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
}
}
}

View File

@@ -0,0 +1,6 @@
pub unsafe fn pointer_to_string(pointer: *mut cef::sys::_cef_string_utf16_t) -> String {
let str = unsafe { (*pointer).str_ };
let len = unsafe { (*pointer).length };
let slice = unsafe { std::slice::from_raw_parts(str, len) };
String::from_utf16(slice).unwrap()
}

69
desktop/ui/src/view.rs Normal file
View File

@@ -0,0 +1,69 @@
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 }
}
pub(super) fn current(&mut self) -> ViewInfo {
for update in self.receiver.try_iter() {
self.view_info.apply_update(update);
}
self.view_info
}
}