Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -458,8 +458,9 @@ strip = "none"
# Never vendor dependencies into this repository. Always fork the crate on
# GitHub and make changes on the fork's `patch` branch. Reference an exact Git
# revision here and document the branch beside each dependency.
# Fork branch: patch (includes Wayland/X11 fixes, continuous XI2 units, and Windows precision touchpad panning).
winit = { git = "https://github.com/eval-exec/winit", rev = "2290f91c602f8efdf102972457a25e8b6e36f90c" }
# Fork branch: patch (includes inherited fixes and explicit Wayland connections
# with failed-construction retry; deferred daemon startup never mutates display env).
winit = { git = "https://github.com/thanosapollo/winit", rev = "6884804b87b93c228b97ba683ddcc7d0acb4d023" }
Comment on lines +461 to +463

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The winit change is proposed upstream in eval-exec/winit#1. Once it lands there I'll repoint the dependency to an eval-exec/winit revision.

# Fork branch: neomacs-present-timing.
# Native presentation IDs/timestamps; see the Vulkan timing seam in this fork.
wgpu-hal = { git = "https://github.com/eval-exec/wgpu", rev = "9717f9fe277beec2c98df550662a626f8fc0248c" }
Expand Down
11 changes: 11 additions & 0 deletions crates/neomacs-display-runtime/src/display_identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,19 @@ pub struct DisplayIdentityResolver {
enum WaylandConnectionName {
InheritedSocket,
Environment(Option<String>),
Explicit(String),
}

impl DisplayIdentityResolver {
/// Provenance for the socket passed to the explicit Wayland constructor.
/// `resolve` still checks the opened native display handle, not the request.
pub fn explicit_wayland(name: String) -> Self {
Self {
wayland: WaylandConnectionName::Explicit(name),
x11: None,
}
}

pub fn capture_environment() -> Self {
Self {
wayland: if std::env::var_os("WAYLAND_SOCKET").is_some() {
Expand Down Expand Up @@ -56,6 +66,7 @@ impl DisplayIdentityResolver {
GraphicalBackend::Wayland => match self.wayland {
WaylandConnectionName::InheritedSocket => None,
WaylandConnectionName::Environment(name) => name,
WaylandConnectionName::Explicit(name) => Some(name),
},
GraphicalBackend::X11 => self.x11,
GraphicalBackend::Cocoa => Some(system_name.to_owned()),
Expand Down
55 changes: 50 additions & 5 deletions crates/neomacs-display-runtime/src/font_defaults/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub(super) struct Subscription {
context: gio::glib::MainContext,
stop: Arc<AtomicBool>,
worker: Option<JoinHandle<()>>,
bypass: Option<Arc<AtomicBool>>,
}

impl Drop for Subscription {
Expand All @@ -25,10 +26,20 @@ impl Drop for Subscription {
// GLib guarantees that an early wakeup makes the next iteration
// return without blocking, including the check-before-wait race.
self.context.wakeup();
if let Some(worker) = self.worker.take()
&& worker.join().is_err()
{
tracing::error!("desktop font subscription worker panicked");
if let Some(worker) = self.worker.take() {
if let Some(bypass) = &self.bypass {
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(100);
while !worker.is_finished() && std::time::Instant::now() < deadline {
std::thread::sleep(std::time::Duration::from_millis(5));
}
if !worker.is_finished() {
bypass.store(true, Ordering::Release);
return; // Native worker owns its context; never free it here.
}
}
if worker.join().is_err() {
tracing::error!("desktop font subscription worker panicked");
}
}
}
}
Expand Down Expand Up @@ -59,6 +70,19 @@ fn read_fonts(schema: &SettingsSchema, settings: &Settings) -> SystemFonts {
}

pub(super) fn observe() -> io::Result<super::FontDefaultsObserver> {
observe_inner(None)
}

pub(super) fn observe_controlled(
cancelled: &dyn Fn() -> bool,
bypass: Arc<AtomicBool>,
) -> io::Result<super::FontDefaultsObserver> {
observe_inner(Some((cancelled, bypass)))
}

fn observe_inner(
control: Option<(&dyn Fn() -> bool, Arc<AtomicBool>)>,
) -> io::Result<super::FontDefaultsObserver> {
let context = gio::glib::MainContext::new();
let stop = Arc::new(AtomicBool::new(false));
let worker_context = context.clone();
Expand All @@ -68,6 +92,9 @@ pub(super) fn observe() -> io::Result<super::FontDefaultsObserver> {
let worker = std::thread::Builder::new()
.name("desktop-font-settings".into())
.spawn(move || {
if worker_stop.load(Ordering::Acquire) {
return;
}
worker_context
.with_thread_default(|| {
let Some(schema) = SettingsSchemaSource::default()
Expand Down Expand Up @@ -111,8 +138,26 @@ pub(super) fn observe() -> io::Result<super::FontDefaultsObserver> {
context,
stop,
worker: Some(worker),
bypass: control.as_ref().map(|(_, bypass)| bypass.clone()),
};
let initial = if let Some((cancelled, _)) = control {
loop {
if cancelled() {
return Err(io::Error::new(
io::ErrorKind::Interrupted,
"Native font observation cancelled",
));
}
match initial_rx.recv_timeout(std::time::Duration::from_millis(20)) {
Ok(initial) => break initial,
Err(crossbeam_channel::RecvTimeoutError::Timeout) => {}
Err(error) => return Err(io::Error::other(error)),
}
}
} else {
initial_rx.recv().map_err(io::Error::other)?
};
match initial_rx.recv().map_err(io::Error::other)? {
match initial {
Some(initial) => Ok(super::FontDefaultsObserver {
initial: super::GuiFontDefaults::Desktop(initial),
changes,
Expand Down
10 changes: 10 additions & 0 deletions crates/neomacs-display-runtime/src/font_defaults/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ impl FontDefaultsObserver {
}
}

/// Deferred Linux discovery bounds the wait and retains foreign-worker exit
/// disposition when a native backend cannot acknowledge cancellation.
#[cfg(target_os = "linux")]
pub fn observe_font_defaults_controlled(
cancelled: &dyn Fn() -> bool,
bypass: std::sync::Arc<std::sync::atomic::AtomicBool>,
) -> std::io::Result<FontDefaultsObserver> {
linux::observe_controlled(cancelled, bypass)
}

/// Capture preferences before opening fonts. AppKit discovery stays on the
/// calling main thread; Linux owns discovery and monitoring on one GIO thread.
pub fn observe_font_defaults(backend: GraphicalBackend) -> std::io::Result<FontDefaultsObserver> {
Expand Down
1 change: 1 addition & 0 deletions crates/neomacs-display-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub mod font_defaults;
pub mod gui_resources;
pub mod macos_bundle_runtime;
mod menus;
pub mod native_window_wait;
mod presentation;
mod presentation_feedback;
pub mod redisplay;
Expand Down
118 changes: 118 additions & 0 deletions crates/neomacs-display-runtime/src/native_window_wait.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//! Exact lease for synchronous native window construction. The controller may
//! interrupt only the retained connection's duplicated socket, never native objects.
use std::collections::HashMap;
use std::sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
};
use std::time::Instant;

#[derive(Default)]
struct State {
leases: HashMap<u64, (Arc<AtomicBool>, Instant)>,
active: Option<u64>,
terminal: bool,
}
#[derive(Default)]
pub struct NativeWindowWaits(Mutex<State>);
impl NativeWindowWaits {
pub fn register(&self, frame: u64, live: Arc<AtomicBool>, deadline: Instant) {
self.0
.lock()
.unwrap()
.leases
.insert(frame, (live, deadline));
}
pub fn remove(&self, frame: u64) {
self.0.lock().unwrap().leases.remove(&frame);
}
pub fn terminal(&self) -> bool {
self.0.lock().unwrap().terminal
}
/// Claim terminal connection disposition before the exact socket shutdown.
/// Finishing the synchronous call is serialized with this claim.
pub fn interrupt_required(&self) -> bool {
let mut state = self.0.lock().unwrap();
if state
.active
.and_then(|frame| state.leases.get(&frame))
.is_some_and(|(live, deadline)| {
!live.load(Ordering::Acquire) || Instant::now() >= *deadline
})
{
state.terminal = true;
}
state.terminal
}
pub fn run<T>(
&self,
frame: u64,
work: impl FnOnce() -> Result<T, String>,
) -> Result<T, String> {
{
let mut state = self.0.lock().unwrap();
if state.terminal
|| state.leases.get(&frame).is_some_and(|(live, deadline)| {
!live.load(Ordering::Acquire) || Instant::now() >= *deadline
})
{
return Err("Native frame preparation cancelled".into());
}
state.active = Some(frame);
}
let result = work();
let mut state = self.0.lock().unwrap();
state.active = None;
if state.terminal
|| state.leases.get(&frame).is_some_and(|(live, deadline)| {
!live.load(Ordering::Acquire) || Instant::now() >= *deadline
})
{
return Err("Native frame preparation cancelled".into());
}
result
}
}

#[cfg(test)]
mod tests {
use super::*;
#[test]
fn active_revocation_terminalizes_only_its_connection() {
let waits = Arc::new(NativeWindowWaits::default());
let live = Arc::new(AtomicBool::new(true));
waits.register(
1,
live.clone(),
Instant::now() + std::time::Duration::from_secs(15),
);
let result = waits.run(1, || {
live.store(false, Ordering::Release);
assert!(waits.interrupt_required());
Ok(42)
});
assert!(result.is_err());
assert!(waits.terminal());
let fresh = Arc::new(AtomicBool::new(true));
waits.register(
2,
fresh,
Instant::now() + std::time::Duration::from_secs(15),
);
assert!(waits.run(2, || Ok(42)).is_err());
}
#[test]
fn completed_constructor_loses_interrupt_authority() {
let waits = NativeWindowWaits::default();
let live = Arc::new(AtomicBool::new(true));
waits.register(
1,
live.clone(),
Instant::now() + std::time::Duration::from_secs(15),
);
assert_eq!(waits.run(1, || Ok(42)), Ok(42));
live.store(false, Ordering::Release);
assert!(!waits.interrupt_required());
assert!(!waits.terminal());
}
}
Loading
Loading