Compare commits
3 Commits
4193457fc4
...
bfda24ad0a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfda24ad0a | ||
|
|
67400f1499 | ||
|
|
861bf63621 |
63
Cargo.lock
generated
63
Cargo.lock
generated
@@ -1306,10 +1306,38 @@ name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"libc",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-graphics"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-surface",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-video"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-io-surface",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1323,6 +1351,19 @@ name = "objc2-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-io-surface"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
@@ -1348,8 +1389,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-core-video",
|
||||
"objc2-foundation",
|
||||
"objc2-metal",
|
||||
]
|
||||
@@ -1822,6 +1867,7 @@ dependencies = [
|
||||
"ruin-runtime",
|
||||
"ruin_reactivity",
|
||||
"ruin_ui",
|
||||
"ruin_ui_platform_macos",
|
||||
"ruin_ui_platform_wayland",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@@ -1850,6 +1896,23 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruin_ui_platform_macos"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle",
|
||||
"raw-window-metal",
|
||||
"ruin-runtime",
|
||||
"ruin_ui",
|
||||
"ruin_ui_renderer_wgpu",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruin_ui_platform_wayland"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -282,6 +282,14 @@ impl Reactor {
|
||||
self.inner.ensure_flush_scheduled();
|
||||
}
|
||||
|
||||
/// Flushes currently queued reactive jobs immediately on the calling thread.
|
||||
///
|
||||
/// This is useful when host integrations need synchronous propagation
|
||||
/// (for example, during native resize loops).
|
||||
pub fn flush_now(&self) {
|
||||
Rc::clone(&self.inner).flush_jobs();
|
||||
}
|
||||
|
||||
pub(crate) fn allocate_node(&self) -> NodeId {
|
||||
let raw = self.inner.next_node.get();
|
||||
self.inner.next_node.set(raw.wrapping_add(1));
|
||||
|
||||
@@ -8,9 +8,14 @@ ruin_reactivity = { path = "../reactivity" }
|
||||
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
||||
ruin_app_proc_macros = { package = "ruin-app-proc-macros", path = "../ruin_app_proc_macros" }
|
||||
ruin_ui = { path = "../ui" }
|
||||
ruin_ui_platform_wayland = { path = "../ui_platform_wayland" }
|
||||
tracing = "0.1"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
ruin_ui_platform_wayland = { path = "../ui_platform_wayland" }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
ruin_ui_platform_macos = { path = "../ui_platform_macos" }
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber = { version = "0.3", default-features = false, features = [
|
||||
"env-filter",
|
||||
|
||||
18
lib/ruin_app/build.rs
Normal file
18
lib/ruin_app/build.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=macos/Info.plist");
|
||||
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
if target_os != "macos" {
|
||||
return;
|
||||
}
|
||||
|
||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("manifest dir"));
|
||||
let plist = manifest_dir.join("macos/Info.plist");
|
||||
let arg = format!("-Wl,-sectcreate,__TEXT,__info_plist,{}", plist.display());
|
||||
|
||||
// Ensure all ruin_app executables/examples opt into Retina rendering.
|
||||
println!("cargo:rustc-link-arg-examples={arg}");
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
use ruin_app::prelude::*;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
#[ruin_runtime::async_main]
|
||||
async fn main() -> ruin_app::Result<()> {
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
|
||||
)
|
||||
.with_target(true)
|
||||
.without_time()
|
||||
.try_init();
|
||||
|
||||
let title = "RUIN Counter";
|
||||
|
||||
App::new()
|
||||
|
||||
@@ -4,8 +4,7 @@ use tracing_subscriber::util::SubscriberInitExt;
|
||||
use tracing_subscriber::{EnvFilter, fmt};
|
||||
|
||||
fn install_tracing() {
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("warn"));
|
||||
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn"));
|
||||
let fmt_layer = fmt::layer()
|
||||
.with_target(true)
|
||||
.with_thread_ids(true)
|
||||
@@ -91,21 +90,12 @@ fn TaskBoard() -> impl IntoView {
|
||||
}
|
||||
});
|
||||
|
||||
let all_label = if matches!(filter.get(), TaskFilter::All) {
|
||||
"● All"
|
||||
} else {
|
||||
"○ All"
|
||||
};
|
||||
let open_label = if matches!(filter.get(), TaskFilter::OpenOnly) {
|
||||
"● Open"
|
||||
} else {
|
||||
"○ Open"
|
||||
};
|
||||
let done_label = if matches!(filter.get(), TaskFilter::CompletedOnly) {
|
||||
"● Completed"
|
||||
} else {
|
||||
"○ Completed"
|
||||
};
|
||||
let all_label = filter_label(matches!(filter.get(), TaskFilter::All), "All");
|
||||
let open_label = filter_label(matches!(filter.get(), TaskFilter::OpenOnly), "Open");
|
||||
let done_label = filter_label(
|
||||
matches!(filter.get(), TaskFilter::CompletedOnly),
|
||||
"Completed",
|
||||
);
|
||||
let task_rows = visible_ids.with(|ids| {
|
||||
ids.iter()
|
||||
.map(|task_id| {
|
||||
@@ -467,6 +457,14 @@ fn move_task(tasks: &Signal<Vec<Task>>, task_id: u64, direction: MoveDirection)
|
||||
});
|
||||
}
|
||||
|
||||
fn filter_label(active: bool, label: &str) -> String {
|
||||
if active {
|
||||
format!("[x] {label}")
|
||||
} else {
|
||||
format!("[ ] {label}")
|
||||
}
|
||||
}
|
||||
|
||||
fn seed_tasks() -> Vec<Task> {
|
||||
vec![
|
||||
Task {
|
||||
|
||||
@@ -259,7 +259,12 @@ fn CardFrame(title: View, toolbar: ChildViews, children: ChildViews) -> impl Int
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn InlineDialog(open: bool, title: View, actions: ChildViews, children: ChildViews) -> impl IntoView {
|
||||
fn InlineDialog(
|
||||
open: bool,
|
||||
title: View,
|
||||
actions: ChildViews,
|
||||
children: ChildViews,
|
||||
) -> impl IntoView {
|
||||
if open {
|
||||
let actions_row = view! {
|
||||
row(gap = 10.0) {
|
||||
|
||||
16
lib/ruin_app/macos/Info.plist
Normal file
16
lib/ruin_app/macos/Info.plist
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>RUIN</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>dev.ruin.example</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -11,12 +11,12 @@ use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::future::Future;
|
||||
use std::iter;
|
||||
use std::time::Instant;
|
||||
use std::marker::PhantomData;
|
||||
use std::rc::Rc;
|
||||
use std::time::Instant;
|
||||
|
||||
use ruin_reactivity::effect;
|
||||
use ruin_runtime::queue_future;
|
||||
use ruin_reactivity::{current as current_reactor, effect};
|
||||
use ruin_runtime::{queue_future, spawn_worker};
|
||||
use ruin_ui::{
|
||||
Border, Color, CursorIcon, DisplayItem, Edges, Element, ElementId, HitTarget, InteractionTree,
|
||||
KeyboardEvent, KeyboardEventKind, KeyboardKey, LayoutCache, LayoutSnapshot, PlatformEvent,
|
||||
@@ -25,6 +25,9 @@ use ruin_ui::{
|
||||
TextSystem, TextWrap, UiSize, WindowController, WindowSpec, WindowUpdate,
|
||||
layout_snapshot_with_cache,
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use ruin_ui_platform_macos::{run_macos_app_event_loop, start_macos_ui};
|
||||
#[cfg(target_os = "linux")]
|
||||
use ruin_ui_platform_wayland::start_wayland_ui;
|
||||
|
||||
pub use ResourceState::{Pending, Ready};
|
||||
@@ -32,6 +35,16 @@ pub use ruin_app_proc_macros::{component, context_provider, view};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn start_platform_ui() -> ruin_ui::UiRuntime {
|
||||
start_wayland_ui()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn start_platform_ui() -> ruin_ui::UiRuntime {
|
||||
start_macos_ui()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Window {
|
||||
spec: WindowSpec,
|
||||
@@ -85,8 +98,7 @@ impl App {
|
||||
pub fn mount<M: Mountable>(self, root: M) -> MountedApp<M> {
|
||||
MountedApp {
|
||||
window: self.window,
|
||||
root: Rc::new(root),
|
||||
render_state: Rc::new(RenderState::default()),
|
||||
root,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,19 +149,18 @@ impl Mountable for Element {
|
||||
|
||||
pub struct MountedApp<M: Mountable> {
|
||||
window: Window,
|
||||
root: Rc<M>,
|
||||
render_state: Rc<RenderState>,
|
||||
root: M,
|
||||
}
|
||||
|
||||
impl<M: Mountable> MountedApp<M> {
|
||||
pub async fn run(self) -> Result<()> {
|
||||
let MountedApp {
|
||||
window: app_window,
|
||||
root,
|
||||
render_state,
|
||||
} = self;
|
||||
|
||||
let mut ui = start_wayland_ui();
|
||||
async fn run_with_ui_runtime(
|
||||
window: Window,
|
||||
root: M,
|
||||
mut ui: ruin_ui::UiRuntime,
|
||||
) -> Result<()> {
|
||||
let root = Rc::new(root);
|
||||
let render_state = Rc::new(RenderState::default());
|
||||
let app_window = window;
|
||||
let window = ui.create_window(app_window.spec.clone())?;
|
||||
let initial_viewport = app_window
|
||||
.spec
|
||||
@@ -157,6 +168,7 @@ impl<M: Mountable> MountedApp<M> {
|
||||
.unwrap_or(UiSize::new(960.0, 640.0));
|
||||
|
||||
let viewport = ruin_reactivity::cell(initial_viewport);
|
||||
let configure_serial = ruin_reactivity::cell(0_u64);
|
||||
let scene_version = StdCell::new(0_u64);
|
||||
let text_system = Rc::new(RefCell::new(TextSystem::new()));
|
||||
let layout_cache = Rc::new(RefCell::new(LayoutCache::new()));
|
||||
@@ -170,6 +182,7 @@ impl<M: Mountable> MountedApp<M> {
|
||||
let _scene_effect = effect({
|
||||
let window = window.clone();
|
||||
let viewport = viewport.clone();
|
||||
let configure_serial = configure_serial.clone();
|
||||
let text_system = Rc::clone(&text_system);
|
||||
let layout_cache = Rc::clone(&layout_cache);
|
||||
let interaction_tree = Rc::clone(&interaction_tree);
|
||||
@@ -181,6 +194,7 @@ impl<M: Mountable> MountedApp<M> {
|
||||
let text_selection = Rc::clone(&input_state.text_selection);
|
||||
move || {
|
||||
let viewport = viewport.get();
|
||||
let _ = configure_serial.get();
|
||||
let version = scene_version.get().wrapping_add(1);
|
||||
scene_version.set(version);
|
||||
let _ = text_selection.version.get();
|
||||
@@ -253,6 +267,8 @@ impl<M: Mountable> MountedApp<M> {
|
||||
"app received Configured, queuing layout effect"
|
||||
);
|
||||
let _ = viewport.set(configuration.actual_inner_size);
|
||||
configure_serial.update(|value| *value = value.wrapping_add(1));
|
||||
current_reactor().flush_now();
|
||||
}
|
||||
PlatformEvent::Pointer { window_id, event } if window_id == window.id() => {
|
||||
Self::handle_pointer_event(
|
||||
@@ -263,6 +279,7 @@ impl<M: Mountable> MountedApp<M> {
|
||||
&mut input_state,
|
||||
event,
|
||||
)?;
|
||||
current_reactor().flush_now();
|
||||
}
|
||||
PlatformEvent::Keyboard { window_id, event } if window_id == window.id() => {
|
||||
Self::handle_keyboard_event(
|
||||
@@ -272,11 +289,22 @@ impl<M: Mountable> MountedApp<M> {
|
||||
&input_state,
|
||||
event,
|
||||
)?;
|
||||
current_reactor().flush_now();
|
||||
}
|
||||
PlatformEvent::CloseRequested { window_id } if window_id == window.id() => {
|
||||
tracing::debug!(
|
||||
target: "ruin_app::lifecycle",
|
||||
?window_id,
|
||||
"app received CloseRequested, issuing open(false)"
|
||||
);
|
||||
let _ = window.update(WindowUpdate::new().open(false));
|
||||
}
|
||||
PlatformEvent::Closed { window_id } if window_id == window.id() => {
|
||||
tracing::debug!(
|
||||
target: "ruin_app::lifecycle",
|
||||
?window_id,
|
||||
"app received Closed, shutting down ui runtime"
|
||||
);
|
||||
ui.shutdown()?;
|
||||
return Ok(());
|
||||
}
|
||||
@@ -288,6 +316,14 @@ impl<M: Mountable> MountedApp<M> {
|
||||
ui.shutdown()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: Mountable> MountedApp<M> {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub async fn run(self) -> Result<()> {
|
||||
let ui = start_platform_ui();
|
||||
Self::run_with_ui_runtime(self.window, self.root, ui).await
|
||||
}
|
||||
|
||||
fn handle_pointer_event(
|
||||
window: &WindowController,
|
||||
@@ -378,7 +414,7 @@ impl<M: Mountable> MountedApp<M> {
|
||||
}
|
||||
|
||||
fn handle_text_selection_event(
|
||||
window: &WindowController,
|
||||
_window: &WindowController,
|
||||
interaction_tree: &InteractionTree,
|
||||
event: &RoutedPointerEvent,
|
||||
text_selection: &TextSelectionState,
|
||||
@@ -439,13 +475,47 @@ impl<M: Mountable> MountedApp<M> {
|
||||
|
||||
if selection_changed {
|
||||
text_selection.version.update(|value| *value += 1);
|
||||
sync_primary_selection(window, interaction_tree, *text_selection.selection.borrow())?;
|
||||
#[cfg(target_os = "linux")]
|
||||
sync_primary_selection(
|
||||
_window,
|
||||
interaction_tree,
|
||||
*text_selection.selection.borrow(),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl<M: Mountable + Send + 'static> MountedApp<M> {
|
||||
pub async fn run(self) -> Result<()> {
|
||||
let ui = start_platform_ui();
|
||||
let (done_tx, done_rx) = std::sync::mpsc::channel::<std::result::Result<(), String>>();
|
||||
let window = self.window;
|
||||
let root = self.root;
|
||||
let _worker = spawn_worker(
|
||||
move || {
|
||||
queue_future(async move {
|
||||
let result = Self::run_with_ui_runtime(window, root, ui)
|
||||
.await
|
||||
.map_err(|err| err.to_string());
|
||||
let _ = done_tx.send(result);
|
||||
});
|
||||
},
|
||||
|| {},
|
||||
);
|
||||
|
||||
run_macos_app_event_loop();
|
||||
|
||||
match done_rx.recv() {
|
||||
Ok(Ok(())) => Ok(()),
|
||||
Ok(Err(err)) => Err(err.into()),
|
||||
Err(_) => Err("ui worker terminated before completion".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct View {
|
||||
element: Element,
|
||||
@@ -1848,6 +1918,7 @@ fn apply_text_selection_overlay(
|
||||
scene.items = next_items;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn sync_primary_selection(
|
||||
window: &WindowController,
|
||||
interaction_tree: &InteractionTree,
|
||||
@@ -1983,8 +2054,9 @@ pub mod prelude {
|
||||
Pending, Ready, Resource, ResourceState, Result, ScrollBoxBuilder, ScrollBoxWidget,
|
||||
Shortcut, ShortcutScope, Signal, TextBuilder, TextChildren, TextRole, TextValue, View,
|
||||
WidgetRef, Window, block, button, colors, column, component, context_provider, provide,
|
||||
row, scroll_box, surfaces, text, use_context, use_effect, use_memo, use_resource, use_shortcut,
|
||||
use_shortcut_with_context, use_signal, use_widget_ref, use_window_title, view,
|
||||
row, scroll_box, surfaces, text, use_context, use_effect, use_memo, use_resource,
|
||||
use_shortcut, use_shortcut_with_context, use_signal, use_widget_ref, use_window_title,
|
||||
view,
|
||||
};
|
||||
pub use ruin_ui::{
|
||||
Border, Color, CursorIcon, Edges, Element, ElementId, InteractionTree, PointerButton,
|
||||
@@ -2555,4 +2627,27 @@ mod tests {
|
||||
|
||||
assert!(debug.contains("offset_y: 96.0"), "{debug}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_signal_write_flushes_without_manual_reactor_flush() {
|
||||
let signal = Signal::new("before".to_string());
|
||||
let observed = Rc::new(RefCell::new(String::new()));
|
||||
let _effect = ruin_reactivity::effect({
|
||||
let signal = signal.clone();
|
||||
let observed = Rc::clone(&observed);
|
||||
move || {
|
||||
*observed.borrow_mut() = signal.get();
|
||||
}
|
||||
});
|
||||
|
||||
ruin_runtime::queue_future({
|
||||
let signal = signal.clone();
|
||||
async move {
|
||||
let _ = signal.set("after".to_string());
|
||||
}
|
||||
});
|
||||
ruin_runtime::run_until_stalled();
|
||||
|
||||
assert_eq!(observed.borrow().as_str(), "after");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,10 +60,7 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
||||
Some(prop) if prop.ident == "children" => {
|
||||
let prop = inputs.pop().expect("last input should exist");
|
||||
let kind = parse_children_contract_kind(&prop.ty)?;
|
||||
Some(ChildContract {
|
||||
ty: prop.ty,
|
||||
kind,
|
||||
})
|
||||
Some(ChildContract { ty: prop.ty, kind })
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
@@ -87,7 +84,8 @@ fn expand_component(mut function: ItemFn) -> Result<proc_macro2::TokenStream> {
|
||||
}
|
||||
Some(contract) => {
|
||||
let child_arg_ty = child_builder_arg_type_tokens(contract.kind);
|
||||
let child_value = wrap_special_value_tokens(contract.kind, &format_ident!("children"));
|
||||
let child_value =
|
||||
wrap_special_value_tokens(contract.kind, &format_ident!("children"));
|
||||
let child_field_ident = child_field_ident
|
||||
.as_ref()
|
||||
.expect("child field ident should exist");
|
||||
@@ -574,10 +572,7 @@ fn child_builder_arg_type_tokens(kind: SpecialValueKind) -> proc_macro2::TokenSt
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_special_value_tokens(
|
||||
kind: SpecialValueKind,
|
||||
ident: &Ident,
|
||||
) -> proc_macro2::TokenStream {
|
||||
fn wrap_special_value_tokens(kind: SpecialValueKind, ident: &Ident) -> proc_macro2::TokenStream {
|
||||
match kind {
|
||||
SpecialValueKind::Plain => quote! { #ident },
|
||||
SpecialValueKind::ChildViews => quote! { ::ruin_app::ChildViews::from_children(#ident) },
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::sync::{Arc, Mutex};
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use crate::op::completion::{CompletionFuture, CompletionHandle};
|
||||
use crate::sys::linux::channel::runtime_waiter;
|
||||
use crate::sys::current::channel::runtime_waiter;
|
||||
|
||||
/// Creates a bounded channel with room for at most `capacity` queued messages.
|
||||
///
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex};
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use crate::op::completion::{CompletionFuture, CompletionHandle};
|
||||
use crate::sys::linux::channel::runtime_waiter;
|
||||
use crate::sys::current::channel::runtime_waiter;
|
||||
|
||||
/// Creates a single-use channel for transferring one value from a [`Sender`] to a [`Receiver`].
|
||||
///
|
||||
|
||||
@@ -3,44 +3,9 @@
|
||||
use std::io;
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
use crate::op::completion::completion_for_current_thread;
|
||||
use crate::platform::linux_x86_64::runtime::with_current_driver;
|
||||
use crate::platform::linux_x86_64::uring::{IORING_OP_POLL_ADD, IoUringCqe};
|
||||
|
||||
/// Waits until `fd` becomes readable or reports an error/hangup condition.
|
||||
pub async fn wait_readable(fd: RawFd) -> io::Result<()> {
|
||||
submit_poll(fd, libc::POLLIN | libc::POLLERR | libc::POLLHUP).await
|
||||
}
|
||||
|
||||
async fn submit_poll(fd: RawFd, mask: i16) -> io::Result<()> {
|
||||
let (future, handle) = completion_for_current_thread::<io::Result<()>>();
|
||||
let callback_handle = handle.clone();
|
||||
let token = with_current_driver(|driver| {
|
||||
driver.submit_operation(
|
||||
move |sqe| {
|
||||
sqe.opcode = IORING_OP_POLL_ADD;
|
||||
sqe.fd = fd;
|
||||
sqe.len = 0;
|
||||
sqe.op_flags = mask as u32;
|
||||
},
|
||||
move |cqe| {
|
||||
callback_handle.complete(cqe_to_result(cqe));
|
||||
},
|
||||
)
|
||||
})?;
|
||||
|
||||
handle.set_cancel(move || {
|
||||
let _ = with_current_driver(|driver| driver.cancel_operation(token));
|
||||
});
|
||||
|
||||
future.await
|
||||
}
|
||||
|
||||
fn cqe_to_result(cqe: IoUringCqe) -> io::Result<()> {
|
||||
if cqe.res < 0 {
|
||||
return Err(io::Error::from_raw_os_error(-cqe.res));
|
||||
}
|
||||
Ok(())
|
||||
crate::sys::current::fd::wait_readable(fd).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -53,8 +18,8 @@ mod tests {
|
||||
#[test]
|
||||
fn wait_readable_resolves_for_pipe() {
|
||||
let mut fds = [0; 2];
|
||||
let result = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC | libc::O_NONBLOCK) };
|
||||
assert_eq!(result, 0, "pipe2 should succeed");
|
||||
let result = unsafe { libc::pipe(fds.as_mut_ptr()) };
|
||||
assert_eq!(result, 0, "pipe should succeed");
|
||||
let read_fd = fds[0];
|
||||
let write_fd = fds[1];
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::op::fs::{
|
||||
FileType as RawFileType, FsOp, MetadataTarget, OpenOptions as OpOpenOptions,
|
||||
RawDirEntry as OpDirEntry, RawMetadata,
|
||||
};
|
||||
use crate::sys::linux::fs as sys_fs;
|
||||
use crate::sys::current::fs as sys_fs;
|
||||
|
||||
struct FileInner {
|
||||
fd: OwnedFd,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Runtime, driver, async I/O, and channel primitives for RUIN.
|
||||
//!
|
||||
//! The crate is centered around a single-threaded event loop with explicit worker threads,
|
||||
//! JavaScript-style microtask/macrotask scheduling, and Linux `io_uring`-backed I/O.
|
||||
//! JavaScript-style microtask/macrotask scheduling and platform-specific async I/O backends.
|
||||
//!
|
||||
//! Most users will start with:
|
||||
//!
|
||||
@@ -11,17 +11,22 @@
|
||||
//!
|
||||
//! # Platform support
|
||||
//!
|
||||
//! `ruin-runtime` currently targets Linux on `x86_64`.
|
||||
//! `ruin-runtime` currently targets:
|
||||
//! - Linux `x86_64`
|
||||
//! - macOS `aarch64`
|
||||
//!
|
||||
//! RUIN runtime foundations.
|
||||
//!
|
||||
//! This crate provides a Linux x86_64 runtime substrate: the mesh allocator, the driver, and a
|
||||
//! single-threaded runtime loop with worker-thread task forwarding.
|
||||
//! This crate provides a platform runtime substrate with a single-threaded runtime loop and
|
||||
//! worker-thread task forwarding.
|
||||
|
||||
#![feature(thread_local)]
|
||||
|
||||
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
|
||||
compile_error!("ruin-runtime currently supports only Linux x86_64.");
|
||||
#[cfg(not(any(
|
||||
all(target_os = "linux", target_arch = "x86_64"),
|
||||
all(target_os = "macos", target_arch = "aarch64")
|
||||
)))]
|
||||
compile_error!("ruin-runtime currently supports Linux x86_64 and macOS aarch64.");
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
@@ -56,11 +61,24 @@ pub use ruin_runtime_proc_macros::async_main;
|
||||
/// thread before calling [`run`].
|
||||
pub use ruin_runtime_proc_macros::main;
|
||||
|
||||
/// Driver primitives re-exported from the Linux x86_64 backend.
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
pub use platform::linux_x86_64::driver::{
|
||||
/// Driver primitives re-exported from the active backend.
|
||||
#[cfg(any(
|
||||
all(target_os = "linux", target_arch = "x86_64"),
|
||||
all(target_os = "macos", target_arch = "aarch64")
|
||||
))]
|
||||
pub use platform::current::driver::{
|
||||
Driver, ReadyEvents, ThreadNotifier, create, create_driver, monotonic_now,
|
||||
};
|
||||
/// Runtime/event-loop primitives re-exported from the active backend.
|
||||
#[cfg(any(
|
||||
all(target_os = "linux", target_arch = "x86_64"),
|
||||
all(target_os = "macos", target_arch = "aarch64")
|
||||
))]
|
||||
pub use platform::current::runtime::{
|
||||
IntervalHandle, JoinHandle, ThreadHandle, TimeoutHandle, WorkerHandle, clear_interval,
|
||||
clear_timeout, current_thread_handle, queue_future, queue_microtask, queue_task, run,
|
||||
run_ready_tasks, run_until_stalled, set_interval, set_timeout, spawn_worker, yield_now,
|
||||
};
|
||||
/// Public mesh-allocator surface re-exported from the Linux x86_64 backend.
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
pub use platform::linux_x86_64::mesh_alloc::{
|
||||
@@ -77,22 +95,17 @@ pub use platform::linux_x86_64::mesh_alloc::{
|
||||
/// Additional allocator helpers re-exported from the Linux x86_64 backend.
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
pub use platform::linux_x86_64::mesh_alloc::{FreelistId, bitmaps_meshable};
|
||||
/// Runtime/event-loop primitives re-exported from the Linux x86_64 backend.
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
pub use platform::linux_x86_64::runtime::{
|
||||
IntervalHandle, JoinHandle, ThreadHandle, TimeoutHandle, WorkerHandle, clear_interval,
|
||||
clear_timeout, current_thread_handle, queue_future, queue_microtask, queue_task, run,
|
||||
set_interval, set_timeout, spawn_worker, yield_now,
|
||||
};
|
||||
|
||||
/// Returns the default global mesh allocator configuration for this crate.
|
||||
///
|
||||
/// This is useful when embedding the allocator in a `#[global_allocator]` static.
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
pub const fn default_global_allocator() -> GlobalMeshAllocator {
|
||||
GlobalMeshAllocator::with_default_config()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
mod tests {
|
||||
use super::{MeshAllocator, page_size};
|
||||
|
||||
|
||||
@@ -72,10 +72,10 @@ impl TcpStream {
|
||||
where
|
||||
A: ToSocketAddrs + Send + 'static,
|
||||
{
|
||||
let addrs = crate::sys::linux::net::resolve_addrs(addr).await?;
|
||||
let addrs = crate::sys::current::net::resolve_addrs(addr).await?;
|
||||
let mut last_error = None;
|
||||
for addr in addrs {
|
||||
match crate::sys::linux::net::connect_stream(addr).await {
|
||||
match crate::sys::current::net::connect_stream(addr).await {
|
||||
Ok(fd) => return Ok(Self::from_owned_fd(fd)),
|
||||
Err(error) => last_error = Some(error),
|
||||
}
|
||||
@@ -92,7 +92,7 @@ impl TcpStream {
|
||||
/// Connects to `addr`, failing if the deadline elapses first.
|
||||
pub async fn connect_timeout(addr: &SocketAddr, timeout: Duration) -> io::Result<Self> {
|
||||
validate_timeout(timeout)?;
|
||||
crate::sys::linux::net::connect_stream_timeout(*addr, timeout)
|
||||
crate::sys::current::net::connect_stream_timeout(*addr, timeout)
|
||||
.await
|
||||
.map(Self::from_owned_fd)
|
||||
}
|
||||
@@ -101,10 +101,10 @@ impl TcpStream {
|
||||
pub async fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let data = match self.read_timeout_value() {
|
||||
Some(timeout) => {
|
||||
crate::sys::linux::net::recv_timeout(self.raw_fd(), buf.len(), 0, timeout).await?
|
||||
crate::sys::current::net::recv_timeout(self.raw_fd(), buf.len(), 0, timeout).await?
|
||||
}
|
||||
None => {
|
||||
crate::sys::linux::net::recv(NetOp::Recv {
|
||||
crate::sys::current::net::recv(NetOp::Recv {
|
||||
fd: self.raw_fd(),
|
||||
len: buf.len(),
|
||||
flags: 0,
|
||||
@@ -136,10 +136,11 @@ impl TcpStream {
|
||||
pub async fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self.write_timeout_value() {
|
||||
Some(timeout) => {
|
||||
crate::sys::linux::net::send_timeout(self.raw_fd(), buf.to_vec(), 0, timeout).await
|
||||
crate::sys::current::net::send_timeout(self.raw_fd(), buf.to_vec(), 0, timeout)
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
crate::sys::linux::net::send(NetOp::Send {
|
||||
crate::sys::current::net::send(NetOp::Send {
|
||||
fd: self.raw_fd(),
|
||||
data: buf.to_vec(),
|
||||
flags: 0,
|
||||
@@ -166,7 +167,7 @@ impl TcpStream {
|
||||
|
||||
/// Shuts down the read, write, or both halves of the connection.
|
||||
pub async fn shutdown(&self, how: Shutdown) -> io::Result<()> {
|
||||
crate::sys::linux::net::shutdown(NetOp::Shutdown {
|
||||
crate::sys::current::net::shutdown(NetOp::Shutdown {
|
||||
fd: self.raw_fd(),
|
||||
how,
|
||||
})
|
||||
@@ -175,39 +176,39 @@ impl TcpStream {
|
||||
|
||||
/// Duplicates the underlying stream socket.
|
||||
pub async fn try_clone(&self) -> io::Result<Self> {
|
||||
crate::sys::linux::net::duplicate(self.raw_fd())
|
||||
crate::sys::current::net::duplicate(self.raw_fd())
|
||||
.await
|
||||
.map(Self::from_owned_fd)
|
||||
}
|
||||
|
||||
/// Returns the local socket address of this stream.
|
||||
pub fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
crate::sys::linux::net::local_addr(self.raw_fd())
|
||||
crate::sys::current::net::local_addr(self.raw_fd())
|
||||
}
|
||||
|
||||
/// Returns the remote peer address of this stream.
|
||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
crate::sys::linux::net::peer_addr(self.raw_fd())
|
||||
crate::sys::current::net::peer_addr(self.raw_fd())
|
||||
}
|
||||
|
||||
/// Reads the current `TCP_NODELAY` setting.
|
||||
pub fn nodelay(&self) -> io::Result<bool> {
|
||||
crate::sys::linux::net::nodelay(self.raw_fd())
|
||||
crate::sys::current::net::nodelay(self.raw_fd())
|
||||
}
|
||||
|
||||
/// Enables or disables `TCP_NODELAY`.
|
||||
pub fn set_nodelay(&self, enabled: bool) -> io::Result<()> {
|
||||
crate::sys::linux::net::set_nodelay(self.raw_fd(), enabled)
|
||||
crate::sys::current::net::set_nodelay(self.raw_fd(), enabled)
|
||||
}
|
||||
|
||||
/// Reads the socket's IP time-to-live value.
|
||||
pub fn ttl(&self) -> io::Result<u32> {
|
||||
crate::sys::linux::net::ttl(self.raw_fd())
|
||||
crate::sys::current::net::ttl(self.raw_fd())
|
||||
}
|
||||
|
||||
/// Sets the socket's IP time-to-live value.
|
||||
pub fn set_ttl(&self, ttl: u32) -> io::Result<()> {
|
||||
crate::sys::linux::net::set_ttl(self.raw_fd(), ttl)
|
||||
crate::sys::current::net::set_ttl(self.raw_fd(), ttl)
|
||||
}
|
||||
|
||||
/// Returns the read timeout used by async read operations on this handle.
|
||||
@@ -269,10 +270,10 @@ impl TcpListener {
|
||||
where
|
||||
A: ToSocketAddrs + Send + 'static,
|
||||
{
|
||||
let addrs = crate::sys::linux::net::resolve_addrs(addr).await?;
|
||||
let addrs = crate::sys::current::net::resolve_addrs(addr).await?;
|
||||
let mut last_error = None;
|
||||
for addr in addrs {
|
||||
match crate::sys::linux::net::bind_listener(addr, None).await {
|
||||
match crate::sys::current::net::bind_listener(addr, None).await {
|
||||
Ok(fd) => return Ok(Self::from_owned_fd(fd)),
|
||||
Err(error) => last_error = Some(error),
|
||||
}
|
||||
@@ -288,7 +289,8 @@ impl TcpListener {
|
||||
|
||||
/// Accepts an incoming connection.
|
||||
pub async fn accept(&self) -> io::Result<(TcpStream, SocketAddr)> {
|
||||
let accepted = crate::sys::linux::net::accept(NetOp::Accept { fd: self.raw_fd() }).await?;
|
||||
let accepted =
|
||||
crate::sys::current::net::accept(NetOp::Accept { fd: self.raw_fd() }).await?;
|
||||
|
||||
let stream = TcpStream::from_owned_fd(unsafe { OwnedFd::from_raw_fd(accepted.fd) });
|
||||
Ok((stream, accepted.peer_addr))
|
||||
@@ -296,17 +298,17 @@ impl TcpListener {
|
||||
|
||||
/// Returns the local socket address of this listener.
|
||||
pub fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
crate::sys::linux::net::local_addr(self.raw_fd())
|
||||
crate::sys::current::net::local_addr(self.raw_fd())
|
||||
}
|
||||
|
||||
/// Reads the listener socket's IP time-to-live value.
|
||||
pub fn ttl(&self) -> io::Result<u32> {
|
||||
crate::sys::linux::net::ttl(self.raw_fd())
|
||||
crate::sys::current::net::ttl(self.raw_fd())
|
||||
}
|
||||
|
||||
/// Sets the listener socket's IP time-to-live value.
|
||||
pub fn set_ttl(&self, ttl: u32) -> io::Result<()> {
|
||||
crate::sys::linux::net::set_ttl(self.raw_fd(), ttl)
|
||||
crate::sys::current::net::set_ttl(self.raw_fd(), ttl)
|
||||
}
|
||||
|
||||
fn from_owned_fd(fd: OwnedFd) -> Self {
|
||||
@@ -326,10 +328,10 @@ impl UdpSocket {
|
||||
where
|
||||
A: ToSocketAddrs + Send + 'static,
|
||||
{
|
||||
let addrs = crate::sys::linux::net::resolve_addrs(addr).await?;
|
||||
let addrs = crate::sys::current::net::resolve_addrs(addr).await?;
|
||||
let mut last_error = None;
|
||||
for addr in addrs {
|
||||
match crate::sys::linux::net::bind_datagram(addr).await {
|
||||
match crate::sys::current::net::bind_datagram(addr).await {
|
||||
Ok(fd) => return Ok(Self::from_owned_fd(fd)),
|
||||
Err(error) => last_error = Some(error),
|
||||
}
|
||||
@@ -351,10 +353,10 @@ impl UdpSocket {
|
||||
where
|
||||
A: ToSocketAddrs + Send + 'static,
|
||||
{
|
||||
let addrs = crate::sys::linux::net::resolve_addrs(addr).await?;
|
||||
let addrs = crate::sys::current::net::resolve_addrs(addr).await?;
|
||||
let mut last_error = None;
|
||||
for addr in addrs {
|
||||
match crate::sys::linux::net::connect(NetOp::Connect {
|
||||
match crate::sys::current::net::connect(NetOp::Connect {
|
||||
fd: self.raw_fd(),
|
||||
addr,
|
||||
})
|
||||
@@ -377,10 +379,11 @@ impl UdpSocket {
|
||||
pub async fn send(&self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self.write_timeout_value() {
|
||||
Some(timeout) => {
|
||||
crate::sys::linux::net::send_timeout(self.raw_fd(), buf.to_vec(), 0, timeout).await
|
||||
crate::sys::current::net::send_timeout(self.raw_fd(), buf.to_vec(), 0, timeout)
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
crate::sys::linux::net::send(NetOp::Send {
|
||||
crate::sys::current::net::send(NetOp::Send {
|
||||
fd: self.raw_fd(),
|
||||
data: buf.to_vec(),
|
||||
flags: 0,
|
||||
@@ -394,10 +397,10 @@ impl UdpSocket {
|
||||
pub async fn recv(&self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let data = match self.read_timeout_value() {
|
||||
Some(timeout) => {
|
||||
crate::sys::linux::net::recv_timeout(self.raw_fd(), buf.len(), 0, timeout).await?
|
||||
crate::sys::current::net::recv_timeout(self.raw_fd(), buf.len(), 0, timeout).await?
|
||||
}
|
||||
None => {
|
||||
crate::sys::linux::net::recv(NetOp::Recv {
|
||||
crate::sys::current::net::recv(NetOp::Recv {
|
||||
fd: self.raw_fd(),
|
||||
len: buf.len(),
|
||||
flags: 0,
|
||||
@@ -414,7 +417,7 @@ impl UdpSocket {
|
||||
pub async fn peek(&self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let data = match self.read_timeout_value() {
|
||||
Some(timeout) => {
|
||||
crate::sys::linux::net::recv_timeout(
|
||||
crate::sys::current::net::recv_timeout(
|
||||
self.raw_fd(),
|
||||
buf.len(),
|
||||
libc::MSG_PEEK,
|
||||
@@ -423,7 +426,7 @@ impl UdpSocket {
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
crate::sys::linux::net::recv(NetOp::Recv {
|
||||
crate::sys::current::net::recv(NetOp::Recv {
|
||||
fd: self.raw_fd(),
|
||||
len: buf.len(),
|
||||
flags: libc::MSG_PEEK,
|
||||
@@ -441,13 +444,13 @@ impl UdpSocket {
|
||||
where
|
||||
A: ToSocketAddrs + Send + 'static,
|
||||
{
|
||||
let addrs = crate::sys::linux::net::resolve_addrs(addr).await?;
|
||||
let addrs = crate::sys::current::net::resolve_addrs(addr).await?;
|
||||
let mut last_error = None;
|
||||
let timeout = self.write_timeout_value();
|
||||
for addr in addrs {
|
||||
let result = match timeout {
|
||||
Some(timeout) => {
|
||||
crate::sys::linux::net::send_to_timeout(
|
||||
crate::sys::current::net::send_to_timeout(
|
||||
self.raw_fd(),
|
||||
buf.to_vec(),
|
||||
addr,
|
||||
@@ -457,7 +460,7 @@ impl UdpSocket {
|
||||
.await
|
||||
}
|
||||
None => {
|
||||
crate::sys::linux::net::send_to(NetOp::SendTo {
|
||||
crate::sys::current::net::send_to(NetOp::SendTo {
|
||||
fd: self.raw_fd(),
|
||||
target: addr,
|
||||
data: buf.to_vec(),
|
||||
@@ -484,11 +487,11 @@ impl UdpSocket {
|
||||
pub async fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
||||
let datagram = match self.read_timeout_value() {
|
||||
Some(timeout) => {
|
||||
crate::sys::linux::net::recv_from_timeout(self.raw_fd(), buf.len(), 0, timeout)
|
||||
crate::sys::current::net::recv_from_timeout(self.raw_fd(), buf.len(), 0, timeout)
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
crate::sys::linux::net::recv_from(NetOp::RecvFrom {
|
||||
crate::sys::current::net::recv_from(NetOp::RecvFrom {
|
||||
fd: self.raw_fd(),
|
||||
len: buf.len(),
|
||||
flags: 0,
|
||||
@@ -505,7 +508,7 @@ impl UdpSocket {
|
||||
pub async fn peek_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
|
||||
let datagram = match self.read_timeout_value() {
|
||||
Some(timeout) => {
|
||||
crate::sys::linux::net::recv_from_timeout(
|
||||
crate::sys::current::net::recv_from_timeout(
|
||||
self.raw_fd(),
|
||||
buf.len(),
|
||||
libc::MSG_PEEK,
|
||||
@@ -514,7 +517,7 @@ impl UdpSocket {
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
crate::sys::linux::net::recv_from(NetOp::RecvFrom {
|
||||
crate::sys::current::net::recv_from(NetOp::RecvFrom {
|
||||
fd: self.raw_fd(),
|
||||
len: buf.len(),
|
||||
flags: libc::MSG_PEEK,
|
||||
@@ -529,39 +532,39 @@ impl UdpSocket {
|
||||
|
||||
/// Duplicates the underlying UDP socket.
|
||||
pub async fn try_clone(&self) -> io::Result<Self> {
|
||||
crate::sys::linux::net::duplicate(self.raw_fd())
|
||||
crate::sys::current::net::duplicate(self.raw_fd())
|
||||
.await
|
||||
.map(Self::from_owned_fd)
|
||||
}
|
||||
|
||||
/// Returns the local socket address of this socket.
|
||||
pub fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
crate::sys::linux::net::local_addr(self.raw_fd())
|
||||
crate::sys::current::net::local_addr(self.raw_fd())
|
||||
}
|
||||
|
||||
/// Returns the connected peer address, if the socket has been connected.
|
||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
crate::sys::linux::net::peer_addr(self.raw_fd())
|
||||
crate::sys::current::net::peer_addr(self.raw_fd())
|
||||
}
|
||||
|
||||
/// Reads the `SO_BROADCAST` setting.
|
||||
pub fn broadcast(&self) -> io::Result<bool> {
|
||||
crate::sys::linux::net::broadcast(self.raw_fd())
|
||||
crate::sys::current::net::broadcast(self.raw_fd())
|
||||
}
|
||||
|
||||
/// Enables or disables `SO_BROADCAST`.
|
||||
pub fn set_broadcast(&self, enabled: bool) -> io::Result<()> {
|
||||
crate::sys::linux::net::set_broadcast(self.raw_fd(), enabled)
|
||||
crate::sys::current::net::set_broadcast(self.raw_fd(), enabled)
|
||||
}
|
||||
|
||||
/// Reads the socket's IP time-to-live value.
|
||||
pub fn ttl(&self) -> io::Result<u32> {
|
||||
crate::sys::linux::net::ttl(self.raw_fd())
|
||||
crate::sys::current::net::ttl(self.raw_fd())
|
||||
}
|
||||
|
||||
/// Sets the socket's IP time-to-live value.
|
||||
pub fn set_ttl(&self, ttl: u32) -> io::Result<()> {
|
||||
crate::sys::linux::net::set_ttl(self.raw_fd(), ttl)
|
||||
crate::sys::current::net::set_ttl(self.raw_fd(), ttl)
|
||||
}
|
||||
|
||||
/// Returns the read timeout used by async receive operations on this handle.
|
||||
@@ -627,13 +630,13 @@ impl HyperRead for TcpStream {
|
||||
|
||||
if this.pending_read.is_none() {
|
||||
this.pending_read = Some(match this.read_timeout_value() {
|
||||
Some(timeout) => Box::pin(crate::sys::linux::net::recv_timeout(
|
||||
Some(timeout) => Box::pin(crate::sys::current::net::recv_timeout(
|
||||
this.raw_fd(),
|
||||
buf.remaining(),
|
||||
0,
|
||||
timeout,
|
||||
)),
|
||||
None => crate::sys::linux::net::recv_future(this.raw_fd(), buf.remaining()),
|
||||
None => crate::sys::current::net::recv_future(this.raw_fd(), buf.remaining()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -671,13 +674,13 @@ impl HyperWrite for TcpStream {
|
||||
|
||||
if this.pending_write.is_none() {
|
||||
this.pending_write = Some(match this.write_timeout_value() {
|
||||
Some(timeout) => Box::pin(crate::sys::linux::net::send_timeout(
|
||||
Some(timeout) => Box::pin(crate::sys::current::net::send_timeout(
|
||||
this.raw_fd(),
|
||||
buf.to_vec(),
|
||||
0,
|
||||
timeout,
|
||||
)),
|
||||
None => crate::sys::linux::net::send_future(this.raw_fd(), buf.to_vec()),
|
||||
None => crate::sys::current::net::send_future(this.raw_fd(), buf.to_vec()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -707,7 +710,7 @@ impl HyperWrite for TcpStream {
|
||||
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
|
||||
let this = self.get_mut();
|
||||
if this.pending_shutdown.is_none() {
|
||||
this.pending_shutdown = Some(crate::sys::linux::net::shutdown_future(
|
||||
this.pending_shutdown = Some(crate::sys::current::net::shutdown_future(
|
||||
this.raw_fd(),
|
||||
Shutdown::Write,
|
||||
));
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::task::{Context, Poll, Waker};
|
||||
|
||||
use crate::platform::linux_x86_64::runtime::{ThreadHandle, current_thread_handle};
|
||||
use crate::platform::current::runtime::{ThreadHandle, current_thread_handle};
|
||||
|
||||
type CancelCallback = Box<dyn FnOnce() + Send + 'static>;
|
||||
|
||||
|
||||
@@ -13,16 +13,12 @@ use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::driver::{Driver, ThreadNotifier, create_driver, monotonic_now};
|
||||
use crate::platform::runtime_shared::{
|
||||
IntervalCallback, LocalBoxFuture, LocalTask, LocalTaskQueue, MICROTASK_STARVATION_THRESHOLD,
|
||||
MacroTaskQueue, SendTask,
|
||||
};
|
||||
use crate::trace_targets;
|
||||
|
||||
type LocalTask = Box<dyn FnOnce() + 'static>;
|
||||
type SendTask = Box<dyn FnOnce() + Send + 'static>;
|
||||
type LocalBoxFuture = Pin<Box<dyn Future<Output = ()> + 'static>>;
|
||||
|
||||
/// If the microtask queue runs more than this many tasks in a single turn
|
||||
/// without yielding to the macrotask queue, a warning is emitted.
|
||||
const MICROTASK_STARVATION_THRESHOLD: u64 = 1000;
|
||||
|
||||
struct MacroTask {
|
||||
task: LocalTask,
|
||||
/// Wall time at which this task entered the local queue. Populated only
|
||||
@@ -478,6 +474,99 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Drains ready work on the current runtime thread without blocking for future work.
|
||||
///
|
||||
/// Unlike [`run`], this returns as soon as there are no immediately runnable
|
||||
/// microtasks or macrotasks left. It is intended for host integrations that
|
||||
/// need to re-enter the scheduler while an outer platform loop remains active.
|
||||
pub fn run_until_stalled() {
|
||||
let _ = current_thread();
|
||||
|
||||
loop {
|
||||
drain_all();
|
||||
|
||||
let mut microtasks_run: u64 = 0;
|
||||
while let Some(task) = pop_microtask() {
|
||||
task();
|
||||
microtasks_run += 1;
|
||||
drain_all();
|
||||
}
|
||||
if microtasks_run >= MICROTASK_STARVATION_THRESHOLD {
|
||||
tracing::warn!(
|
||||
target: trace_targets::SCHEDULER,
|
||||
event = "microtask_starvation",
|
||||
count = microtasks_run,
|
||||
"microtask queue ran {microtasks_run} tasks in a single turn; macrotask handlers may be starved",
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(task) = pop_macrotask() {
|
||||
task();
|
||||
continue;
|
||||
}
|
||||
|
||||
drain_all();
|
||||
|
||||
if has_ready_work() {
|
||||
continue;
|
||||
}
|
||||
|
||||
current_thread()
|
||||
.shared
|
||||
.closing
|
||||
.store(false, Ordering::Release);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// Drains already-queued work on the current runtime thread without polling the
|
||||
/// driver for timers or I/O readiness.
|
||||
///
|
||||
/// This is intended for host integrations that need to flush application work
|
||||
/// from inside a host callback without re-entering timer callbacks.
|
||||
pub fn run_ready_tasks() {
|
||||
let _ = current_thread();
|
||||
|
||||
loop {
|
||||
drain_remote_tasks();
|
||||
drain_completed_workers();
|
||||
|
||||
let mut microtasks_run: u64 = 0;
|
||||
while let Some(task) = pop_microtask() {
|
||||
task();
|
||||
microtasks_run += 1;
|
||||
drain_remote_tasks();
|
||||
drain_completed_workers();
|
||||
}
|
||||
if microtasks_run >= MICROTASK_STARVATION_THRESHOLD {
|
||||
tracing::warn!(
|
||||
target: trace_targets::SCHEDULER,
|
||||
event = "microtask_starvation",
|
||||
count = microtasks_run,
|
||||
"microtask queue ran {microtasks_run} tasks in a single turn; macrotask handlers may be starved",
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(task) = pop_macrotask() {
|
||||
task();
|
||||
continue;
|
||||
}
|
||||
|
||||
drain_remote_tasks();
|
||||
drain_completed_workers();
|
||||
|
||||
if has_ready_work() {
|
||||
continue;
|
||||
}
|
||||
|
||||
current_thread()
|
||||
.shared
|
||||
.closing
|
||||
.store(false, Ordering::Release);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fn drain_all() {
|
||||
drain_driver_events();
|
||||
drain_remote_tasks();
|
||||
@@ -575,12 +664,12 @@ struct ThreadState {
|
||||
driver: Driver,
|
||||
shared: Arc<ThreadShared>,
|
||||
worker_completion: Option<Arc<WorkerCompletion>>,
|
||||
local_microtasks: RefCell<VecDeque<LocalTask>>,
|
||||
local_macrotasks: RefCell<VecDeque<MacroTask>>,
|
||||
local_microtasks: RefCell<LocalTaskQueue>,
|
||||
local_macrotasks: RefCell<MacroTaskQueue<MacroTask>>,
|
||||
timers: RefCell<TimerHeap>,
|
||||
/// Zero-delay intervals bypasses the timer heap entirely. Each entry
|
||||
/// re-enqueues itself as a macrotask on every turn.
|
||||
immediate_intervals: RefCell<HashMap<usize, Rc<RefCell<Box<dyn FnMut()>>>>>,
|
||||
immediate_intervals: RefCell<HashMap<usize, IntervalCallback>>,
|
||||
next_timer_id: Cell<usize>,
|
||||
children: RefCell<Vec<ChildWorker>>,
|
||||
}
|
||||
@@ -1178,7 +1267,11 @@ fn schedule_immediate_interval(owner: *const ThreadState, id: usize) {
|
||||
};
|
||||
(callback.borrow_mut())();
|
||||
// Re-enqueue for the next turn if still live.
|
||||
if current_thread().immediate_intervals.borrow().contains_key(&id) {
|
||||
if current_thread()
|
||||
.immediate_intervals
|
||||
.borrow()
|
||||
.contains_key(&id)
|
||||
{
|
||||
schedule_immediate_interval(owner, id);
|
||||
}
|
||||
}));
|
||||
@@ -1207,12 +1300,8 @@ fn dispatch_expired_timers() {
|
||||
.deadline
|
||||
.checked_add(interval)
|
||||
.unwrap_or(Duration::MAX);
|
||||
let next_timer = TimerNode::interval(
|
||||
timer.id,
|
||||
next_deadline,
|
||||
interval,
|
||||
Rc::clone(&callback),
|
||||
);
|
||||
let next_timer =
|
||||
TimerNode::interval(timer.id, next_deadline, interval, Rc::clone(&callback));
|
||||
current_thread().timers.borrow_mut().insert(next_timer);
|
||||
|
||||
push_local_macrotask(Box::new(move || {
|
||||
@@ -1394,8 +1483,7 @@ mod tests {
|
||||
// turns by awaiting a sleep between checks.
|
||||
let count = Rc::new(Cell::new(0usize));
|
||||
let count_clone = Rc::clone(&count);
|
||||
let handle_slot: Rc<RefCell<Option<super::IntervalHandle>>> =
|
||||
Rc::new(RefCell::new(None));
|
||||
let handle_slot: Rc<RefCell<Option<super::IntervalHandle>>> = Rc::new(RefCell::new(None));
|
||||
let handle_slot_clone = Rc::clone(&handle_slot);
|
||||
|
||||
let handle = set_interval(Duration::ZERO, move || {
|
||||
|
||||
494
lib/runtime/src/platform/macos_aarch64/driver.rs
Normal file
494
lib/runtime/src/platform/macos_aarch64/driver.rs
Normal file
@@ -0,0 +1,494 @@
|
||||
//! Public runtime driver primitives for macOS.
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::os::fd::RawFd;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::op::completion::CompletionHandle;
|
||||
|
||||
type FdCompletion = CompletionHandle<io::Result<()>>;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub(crate) struct FdReadinessToken(u64);
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub(crate) enum FdInterest {
|
||||
Readable,
|
||||
Writable,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct FdKey {
|
||||
fd: RawFd,
|
||||
interest: FdInterest,
|
||||
}
|
||||
|
||||
struct FdWaiter {
|
||||
token: FdReadinessToken,
|
||||
completion: FdCompletion,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct NotifierInner {
|
||||
write_fd: RawFd,
|
||||
closed: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl NotifierInner {
|
||||
fn notify(&self) -> io::Result<()> {
|
||||
if self.closed.load(Ordering::Acquire) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"target runtime driver is closed",
|
||||
));
|
||||
}
|
||||
|
||||
let byte = 1u8;
|
||||
let written = unsafe {
|
||||
libc::write(
|
||||
self.write_fd,
|
||||
&byte as *const u8 as *const libc::c_void,
|
||||
std::mem::size_of::<u8>(),
|
||||
)
|
||||
};
|
||||
if written < 0 {
|
||||
let error = io::Error::last_os_error();
|
||||
if error.kind() == io::ErrorKind::WouldBlock {
|
||||
return Ok(());
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
/// Cross-thread notifier for a runtime thread's driver.
|
||||
pub struct ThreadNotifier {
|
||||
inner: NotifierInner,
|
||||
}
|
||||
|
||||
impl ThreadNotifier {
|
||||
/// Sends a wake notification to the target runtime thread.
|
||||
pub fn notify(&self) -> io::Result<()> {
|
||||
self.inner.notify()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
|
||||
/// Readiness information returned by [`Driver::poll`].
|
||||
pub struct ReadyEvents {
|
||||
/// One or more timer expirations are pending.
|
||||
pub timer: bool,
|
||||
/// One or more cross-thread wake notifications are pending.
|
||||
pub wake: bool,
|
||||
}
|
||||
|
||||
/// Low-level macOS runtime driver backed by `kqueue` and a wake pipe.
|
||||
pub struct Driver {
|
||||
kqueue_fd: RawFd,
|
||||
wake_read_fd: RawFd,
|
||||
wake_write_fd: RawFd,
|
||||
closed: Arc<AtomicBool>,
|
||||
timer_deadline: Cell<Option<Duration>>,
|
||||
pending_wakes: Cell<u64>,
|
||||
pending_timers: Cell<u64>,
|
||||
next_fd_token: Cell<u64>,
|
||||
fd_waiters: RefCell<HashMap<FdKey, Vec<FdWaiter>>>,
|
||||
}
|
||||
|
||||
/// Creates a new driver and its paired [`ThreadNotifier`].
|
||||
pub fn create() -> io::Result<(Driver, ThreadNotifier)> {
|
||||
create_driver()
|
||||
}
|
||||
|
||||
/// Creates a new driver and its paired [`ThreadNotifier`].
|
||||
pub fn create_driver() -> io::Result<(Driver, ThreadNotifier)> {
|
||||
let kqueue_fd = cvt(unsafe { libc::kqueue() })?;
|
||||
|
||||
let mut pipe_fds = [0; 2];
|
||||
cvt(unsafe { libc::pipe(pipe_fds.as_mut_ptr()) })?;
|
||||
let wake_read_fd = pipe_fds[0];
|
||||
let wake_write_fd = pipe_fds[1];
|
||||
|
||||
set_nonblocking(wake_read_fd)?;
|
||||
set_nonblocking(wake_write_fd)?;
|
||||
|
||||
let event = libc::kevent {
|
||||
ident: wake_read_fd as usize,
|
||||
filter: libc::EVFILT_READ,
|
||||
flags: libc::EV_ADD | libc::EV_ENABLE,
|
||||
fflags: 0,
|
||||
data: 0,
|
||||
udata: std::ptr::null_mut(),
|
||||
};
|
||||
|
||||
let submitted = unsafe {
|
||||
libc::kevent(
|
||||
kqueue_fd,
|
||||
&event,
|
||||
1,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
if submitted < 0 {
|
||||
let error = io::Error::last_os_error();
|
||||
unsafe {
|
||||
libc::close(wake_read_fd);
|
||||
libc::close(wake_write_fd);
|
||||
libc::close(kqueue_fd);
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let closed = Arc::new(AtomicBool::new(false));
|
||||
let driver = Driver {
|
||||
kqueue_fd,
|
||||
wake_read_fd,
|
||||
wake_write_fd,
|
||||
closed: Arc::clone(&closed),
|
||||
timer_deadline: Cell::new(None),
|
||||
pending_wakes: Cell::new(0),
|
||||
pending_timers: Cell::new(0),
|
||||
next_fd_token: Cell::new(1),
|
||||
fd_waiters: RefCell::new(HashMap::new()),
|
||||
};
|
||||
|
||||
let notifier = ThreadNotifier {
|
||||
inner: NotifierInner {
|
||||
write_fd: wake_write_fd,
|
||||
closed,
|
||||
},
|
||||
};
|
||||
|
||||
Ok((driver, notifier))
|
||||
}
|
||||
|
||||
impl Driver {
|
||||
pub(crate) fn bind_current_thread(&self) {}
|
||||
|
||||
pub(crate) fn unbind_current_thread(&self) {}
|
||||
|
||||
/// Polls the driver without blocking.
|
||||
pub fn poll(&self) -> io::Result<Option<ReadyEvents>> {
|
||||
let mut pending = ReadyEvents::default();
|
||||
if self.pending_wakes.get() > 0 {
|
||||
pending.wake = true;
|
||||
}
|
||||
if self.pending_timers.get() > 0 {
|
||||
pending.timer = true;
|
||||
}
|
||||
if pending.wake || pending.timer {
|
||||
return Ok(Some(pending));
|
||||
}
|
||||
self.process(Some(Duration::ZERO))
|
||||
}
|
||||
|
||||
/// Blocks until at least one event is available.
|
||||
pub fn wait(&self) -> io::Result<()> {
|
||||
let now = monotonic_now()?;
|
||||
let timeout = self
|
||||
.timer_deadline
|
||||
.get()
|
||||
.map(|deadline| deadline.saturating_sub(now));
|
||||
let _ = self.process(timeout)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the currently armed timer deadline.
|
||||
pub fn rearm_timer(&self, deadline: Option<Duration>) -> io::Result<()> {
|
||||
self.timer_deadline.set(deadline);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Drains the accumulated wake notification count.
|
||||
pub fn drain_wake(&self) -> io::Result<u64> {
|
||||
let wakes = self.pending_wakes.replace(0);
|
||||
if wakes == 0 {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::WouldBlock,
|
||||
"no wake events are pending",
|
||||
))
|
||||
} else {
|
||||
Ok(wakes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Drains the accumulated timer-expiration count.
|
||||
pub fn drain_timer(&self) -> io::Result<u64> {
|
||||
let timers = self.pending_timers.replace(0);
|
||||
if timers == 0 {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::WouldBlock,
|
||||
"no timer events are pending",
|
||||
))
|
||||
} else {
|
||||
Ok(timers)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn register_fd_readiness(
|
||||
&self,
|
||||
fd: RawFd,
|
||||
interest: FdInterest,
|
||||
completion: FdCompletion,
|
||||
) -> io::Result<FdReadinessToken> {
|
||||
let key = FdKey { fd, interest };
|
||||
let should_register = !self.fd_waiters.borrow().contains_key(&key);
|
||||
if should_register {
|
||||
self.update_fd_interest(key, libc::EV_ADD | libc::EV_ENABLE)?;
|
||||
}
|
||||
|
||||
let token = self.allocate_fd_token();
|
||||
self.fd_waiters
|
||||
.borrow_mut()
|
||||
.entry(key)
|
||||
.or_default()
|
||||
.push(FdWaiter { token, completion });
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_fd_readiness(&self, token: FdReadinessToken) {
|
||||
let mut empty_key = None;
|
||||
{
|
||||
let mut waiters = self.fd_waiters.borrow_mut();
|
||||
for (key, entries) in waiters.iter_mut() {
|
||||
if let Some(index) = entries.iter().position(|entry| entry.token == token) {
|
||||
entries.swap_remove(index);
|
||||
if entries.is_empty() {
|
||||
empty_key = Some(*key);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(key) = empty_key {
|
||||
waiters.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(key) = empty_key {
|
||||
let _ = self.update_fd_interest(key, libc::EV_DELETE);
|
||||
}
|
||||
}
|
||||
|
||||
fn process(&self, timeout: Option<Duration>) -> io::Result<Option<ReadyEvents>> {
|
||||
let mut ready = ReadyEvents::default();
|
||||
|
||||
let mut events = [unsafe { std::mem::zeroed::<libc::kevent>() }; 16];
|
||||
let timeout_spec = timeout_to_timespec(timeout);
|
||||
let timeout_ptr = timeout_spec
|
||||
.as_ref()
|
||||
.map_or(std::ptr::null(), |value| value as *const libc::timespec);
|
||||
|
||||
let result = unsafe {
|
||||
libc::kevent(
|
||||
self.kqueue_fd,
|
||||
std::ptr::null(),
|
||||
0,
|
||||
events.as_mut_ptr(),
|
||||
events.len() as i32,
|
||||
timeout_ptr,
|
||||
)
|
||||
};
|
||||
if result < 0 {
|
||||
let error = io::Error::last_os_error();
|
||||
if error.kind() != io::ErrorKind::Interrupted {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
let mut saw_any = false;
|
||||
let count = result.max(0) as usize;
|
||||
if count > 0 {
|
||||
saw_any = true;
|
||||
for event in events.iter().take(count) {
|
||||
if event.ident as RawFd == self.wake_read_fd {
|
||||
ready.wake = true;
|
||||
let wakes = drain_wake_pipe(self.wake_read_fd)?;
|
||||
self.pending_wakes
|
||||
.set(self.pending_wakes.get().saturating_add(wakes));
|
||||
} else if let Some(interest) = interest_from_filter(event.filter) {
|
||||
self.complete_fd_waiters(event.ident as RawFd, interest, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(deadline) = self.timer_deadline.get()
|
||||
&& monotonic_now()? >= deadline
|
||||
{
|
||||
ready.timer = true;
|
||||
saw_any = true;
|
||||
self.timer_deadline.set(None);
|
||||
self.pending_timers
|
||||
.set(self.pending_timers.get().saturating_add(1));
|
||||
}
|
||||
|
||||
if saw_any { Ok(Some(ready)) } else { Ok(None) }
|
||||
}
|
||||
|
||||
fn allocate_fd_token(&self) -> FdReadinessToken {
|
||||
let token = self.next_fd_token.get();
|
||||
self.next_fd_token.set(
|
||||
token
|
||||
.checked_add(1)
|
||||
.expect("fd readiness token space exhausted"),
|
||||
);
|
||||
FdReadinessToken(token)
|
||||
}
|
||||
|
||||
fn update_fd_interest(&self, key: FdKey, flags: u16) -> io::Result<()> {
|
||||
let event = libc::kevent {
|
||||
ident: key.fd as usize,
|
||||
filter: filter_for_interest(key.interest),
|
||||
flags,
|
||||
fflags: 0,
|
||||
data: 0,
|
||||
udata: std::ptr::null_mut(),
|
||||
};
|
||||
let submitted = unsafe {
|
||||
libc::kevent(
|
||||
self.kqueue_fd,
|
||||
&event,
|
||||
1,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
if submitted < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_fd_waiters(&self, fd: RawFd, interest: FdInterest, event: &libc::kevent) {
|
||||
let key = FdKey { fd, interest };
|
||||
let waiters = self.fd_waiters.borrow_mut().remove(&key);
|
||||
let Some(waiters) = waiters else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = self.update_fd_interest(key, libc::EV_DELETE);
|
||||
let result = fd_event_result(event);
|
||||
for waiter in waiters {
|
||||
waiter.completion.complete(match &result {
|
||||
Ok(()) => Ok(()),
|
||||
Err(error) => Err(io::Error::new(error.kind(), error.to_string())),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Driver {
|
||||
fn drop(&mut self) {
|
||||
self.closed.store(true, Ordering::Release);
|
||||
unsafe {
|
||||
libc::close(self.wake_read_fd);
|
||||
libc::close(self.wake_write_fd);
|
||||
libc::close(self.kqueue_fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current monotonic clock reading.
|
||||
pub fn monotonic_now() -> io::Result<Duration> {
|
||||
let mut now = std::mem::MaybeUninit::<libc::timespec>::uninit();
|
||||
let result = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, now.as_mut_ptr()) };
|
||||
if result < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
let now = unsafe { now.assume_init() };
|
||||
Ok(Duration::new(now.tv_sec as u64, now.tv_nsec as u32))
|
||||
}
|
||||
|
||||
fn cvt(value: libc::c_int) -> io::Result<libc::c_int> {
|
||||
if value < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_for_interest(interest: FdInterest) -> i16 {
|
||||
match interest {
|
||||
FdInterest::Readable => libc::EVFILT_READ,
|
||||
FdInterest::Writable => libc::EVFILT_WRITE,
|
||||
}
|
||||
}
|
||||
|
||||
fn interest_from_filter(filter: i16) -> Option<FdInterest> {
|
||||
if filter == libc::EVFILT_READ {
|
||||
Some(FdInterest::Readable)
|
||||
} else if filter == libc::EVFILT_WRITE {
|
||||
Some(FdInterest::Writable)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn fd_event_result(event: &libc::kevent) -> io::Result<()> {
|
||||
if event.flags & libc::EV_ERROR != 0 && event.data != 0 {
|
||||
Err(io::Error::from_raw_os_error(event.data as i32))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn set_nonblocking(fd: RawFd) -> io::Result<()> {
|
||||
let flags = cvt(unsafe { libc::fcntl(fd, libc::F_GETFL) })?;
|
||||
cvt(unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn timeout_to_timespec(timeout: Option<Duration>) -> Option<libc::timespec> {
|
||||
timeout.map(|value| libc::timespec {
|
||||
tv_sec: value.as_secs() as libc::time_t,
|
||||
tv_nsec: value.subsec_nanos() as libc::c_long,
|
||||
})
|
||||
}
|
||||
|
||||
fn drain_wake_pipe(fd: RawFd) -> io::Result<u64> {
|
||||
let mut wakes = 0u64;
|
||||
let mut buf = [0u8; 256];
|
||||
|
||||
loop {
|
||||
let read = unsafe {
|
||||
libc::read(
|
||||
fd,
|
||||
buf.as_mut_ptr() as *mut libc::c_void,
|
||||
buf.len() as libc::size_t,
|
||||
)
|
||||
};
|
||||
|
||||
if read > 0 {
|
||||
wakes = wakes.saturating_add(read as u64);
|
||||
continue;
|
||||
}
|
||||
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let error = io::Error::last_os_error();
|
||||
if error.kind() == io::ErrorKind::WouldBlock {
|
||||
break;
|
||||
}
|
||||
if error.kind() == io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
Ok(wakes.max(1))
|
||||
}
|
||||
2
lib/runtime/src/platform/macos_aarch64/mod.rs
Normal file
2
lib/runtime/src/platform/macos_aarch64/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod driver;
|
||||
pub mod runtime;
|
||||
1508
lib/runtime/src/platform/macos_aarch64/runtime.rs
Normal file
1508
lib/runtime/src/platform/macos_aarch64/runtime.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,11 @@
|
||||
pub(crate) mod runtime_shared;
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
pub mod linux_x86_64;
|
||||
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||
pub mod macos_aarch64;
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
pub use linux_x86_64 as current;
|
||||
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||
pub use macos_aarch64 as current;
|
||||
|
||||
17
lib/runtime/src/platform/runtime_shared.rs
Normal file
17
lib/runtime/src/platform/runtime_shared.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub(crate) type LocalTask = Box<dyn FnOnce() + 'static>;
|
||||
pub(crate) type SendTask = Box<dyn FnOnce() + Send + 'static>;
|
||||
pub(crate) type LocalBoxFuture = Pin<Box<dyn Future<Output = ()> + 'static>>;
|
||||
pub(crate) type IntervalCallback = Rc<RefCell<Box<dyn FnMut()>>>;
|
||||
|
||||
/// If the microtask queue runs more than this many tasks in a single turn
|
||||
/// without yielding to the macrotask queue, a warning is emitted.
|
||||
pub(crate) const MICROTASK_STARVATION_THRESHOLD: u64 = 1000;
|
||||
|
||||
pub(crate) type LocalTaskQueue = VecDeque<LocalTask>;
|
||||
pub(crate) type MacroTaskQueue<T> = VecDeque<T>;
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Linux channel wake helpers.
|
||||
|
||||
use crate::op::completion::{CompletionFuture, CompletionHandle, completion};
|
||||
use crate::platform::linux_x86_64::runtime::try_current_thread_handle;
|
||||
use crate::platform::current::runtime::try_current_thread_handle;
|
||||
|
||||
pub(crate) fn runtime_waiter<T: Send + 'static>() -> (CompletionFuture<T>, CompletionHandle<T>) {
|
||||
let owner = try_current_thread_handle()
|
||||
|
||||
44
lib/runtime/src/sys/linux/fd.rs
Normal file
44
lib/runtime/src/sys/linux/fd.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
//! Linux fd readiness backend.
|
||||
|
||||
use std::io;
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
use crate::op::completion::completion_for_current_thread;
|
||||
use crate::platform::current::runtime::with_current_driver;
|
||||
use crate::platform::linux_x86_64::uring::{IORING_OP_POLL_ADD, IoUringCqe};
|
||||
|
||||
/// Waits until `fd` becomes readable or reports an error/hangup condition.
|
||||
pub async fn wait_readable(fd: RawFd) -> io::Result<()> {
|
||||
submit_poll(fd, libc::POLLIN | libc::POLLERR | libc::POLLHUP).await
|
||||
}
|
||||
|
||||
async fn submit_poll(fd: RawFd, mask: i16) -> io::Result<()> {
|
||||
let (future, handle) = completion_for_current_thread::<io::Result<()>>();
|
||||
let callback_handle = handle.clone();
|
||||
let token = with_current_driver(|driver| {
|
||||
driver.submit_operation(
|
||||
move |sqe| {
|
||||
sqe.opcode = IORING_OP_POLL_ADD;
|
||||
sqe.fd = fd;
|
||||
sqe.len = 0;
|
||||
sqe.op_flags = mask as u32;
|
||||
},
|
||||
move |cqe| {
|
||||
callback_handle.complete(cqe_to_result(cqe));
|
||||
},
|
||||
)
|
||||
})?;
|
||||
|
||||
handle.set_cancel(move || {
|
||||
let _ = with_current_driver(|driver| driver.cancel_operation(token));
|
||||
});
|
||||
|
||||
future.await
|
||||
}
|
||||
|
||||
fn cqe_to_result(cqe: IoUringCqe) -> io::Result<()> {
|
||||
if cqe.res < 0 {
|
||||
return Err(io::Error::from_raw_os_error(-cqe.res));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Linux backend modules.
|
||||
|
||||
pub mod channel;
|
||||
pub mod fd;
|
||||
pub mod fs;
|
||||
pub mod net;
|
||||
|
||||
@@ -803,13 +803,9 @@ where
|
||||
let (future, handle) = completion_for_current_thread::<io::Result<T>>();
|
||||
let callback_handle = handle.clone();
|
||||
let token = with_current_driver(|driver| {
|
||||
driver.submit_operation_with_linked_timeout(
|
||||
fill,
|
||||
timeout,
|
||||
move |cqe| {
|
||||
driver.submit_operation_with_linked_timeout(fill, timeout, move |cqe| {
|
||||
callback_handle.complete(map(cqe));
|
||||
},
|
||||
)
|
||||
})
|
||||
})?;
|
||||
|
||||
handle.set_cancel(move || {
|
||||
@@ -1079,7 +1075,6 @@ fn send_sync(fd: RawFd, data: Vec<u8>, flags: i32) -> io::Result<usize> {
|
||||
cvt_long(written).map(|written| written as usize)
|
||||
}
|
||||
|
||||
|
||||
fn recv_sync(fd: RawFd, len: usize, flags: i32) -> io::Result<Vec<u8>> {
|
||||
let mut buffer = vec![0; len];
|
||||
let read = unsafe {
|
||||
@@ -1127,7 +1122,6 @@ fn close_sync(fd: RawFd) -> io::Result<()> {
|
||||
cvt(unsafe { libc::close(fd) }).map(|_| ())
|
||||
}
|
||||
|
||||
|
||||
/// Wrapper making `Box<libc::iovec>` sendable across the async CQE boundary.
|
||||
///
|
||||
/// Safety: `iov_base` points into a `Vec<u8>` that is owned by the same
|
||||
|
||||
10
lib/runtime/src/sys/macos/channel.rs
Normal file
10
lib/runtime/src/sys/macos/channel.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! macOS channel wake helpers.
|
||||
|
||||
use crate::op::completion::{CompletionFuture, CompletionHandle, completion};
|
||||
use crate::platform::current::runtime::try_current_thread_handle;
|
||||
|
||||
pub(crate) fn runtime_waiter<T: Send + 'static>() -> (CompletionFuture<T>, CompletionHandle<T>) {
|
||||
let owner = try_current_thread_handle()
|
||||
.expect("async channel operations must be polled on a runtime thread");
|
||||
completion(owner)
|
||||
}
|
||||
49
lib/runtime/src/sys/macos/fd.rs
Normal file
49
lib/runtime/src/sys/macos/fd.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
//! macOS fd readiness backend.
|
||||
|
||||
use std::io;
|
||||
use std::os::fd::RawFd;
|
||||
|
||||
use crate::op::completion::completion_for_current_thread;
|
||||
use crate::platform::current::driver::FdInterest;
|
||||
use crate::platform::current::runtime::{
|
||||
cancel_fd_readiness, current_thread_handle, with_current_driver,
|
||||
};
|
||||
|
||||
/// Waits until `fd` becomes readable or reports an error/hangup condition.
|
||||
pub async fn wait_readable(fd: RawFd) -> io::Result<()> {
|
||||
wait_fd_readiness(fd, FdInterest::Readable).await
|
||||
}
|
||||
|
||||
/// Waits until `fd` becomes writable or reports an error/hangup condition.
|
||||
pub async fn wait_writable(fd: RawFd) -> io::Result<()> {
|
||||
wait_fd_readiness(fd, FdInterest::Writable).await
|
||||
}
|
||||
|
||||
async fn wait_fd_readiness(fd: RawFd, interest: FdInterest) -> io::Result<()> {
|
||||
let (future, handle) = completion_for_current_thread::<io::Result<()>>();
|
||||
let owner = current_thread_handle();
|
||||
let token =
|
||||
with_current_driver(|driver| driver.register_fd_readiness(fd, interest, handle.clone()));
|
||||
match token {
|
||||
Ok(token) => {
|
||||
handle.set_cancel({
|
||||
let handle = handle.clone();
|
||||
move || {
|
||||
let queued_handle = handle.clone();
|
||||
let queued = owner.queue_task(move || {
|
||||
cancel_fd_readiness(token);
|
||||
queued_handle.finish(None);
|
||||
});
|
||||
if !queued {
|
||||
handle.finish(None);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
handle.complete(Err(error));
|
||||
}
|
||||
}
|
||||
|
||||
future.await
|
||||
}
|
||||
493
lib/runtime/src/sys/macos/fs.rs
Normal file
493
lib/runtime/src/sys/macos/fs.rs
Normal file
@@ -0,0 +1,493 @@
|
||||
//! macOS filesystem backend.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::CString;
|
||||
use std::future::poll_fn;
|
||||
use std::io;
|
||||
use std::os::fd::{FromRawFd, OwnedFd};
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::task::{Context, Poll, Waker};
|
||||
use std::thread;
|
||||
|
||||
use crate::op::completion::completion_for_current_thread;
|
||||
use crate::op::fs::{FileType, FsOp, MetadataTarget, RawDirEntry, RawMetadata};
|
||||
use crate::platform::current::runtime::{ThreadHandle, current_thread_handle};
|
||||
|
||||
type BlockingTask = Box<dyn FnOnce() + Send + 'static>;
|
||||
|
||||
static BLOCKING_POOL: OnceLock<io::Result<BlockingPool>> = OnceLock::new();
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ExecutionPath {
|
||||
Offload,
|
||||
}
|
||||
|
||||
pub fn execution_path(_op: &FsOp) -> ExecutionPath {
|
||||
ExecutionPath::Offload
|
||||
}
|
||||
|
||||
pub async fn open(op: FsOp) -> io::Result<OwnedFd> {
|
||||
let FsOp::Open { path, options } = op else {
|
||||
unreachable!("open backend called with non-open op");
|
||||
};
|
||||
|
||||
offload(move || {
|
||||
let mut open = std::fs::OpenOptions::new();
|
||||
open.read(options.read)
|
||||
.write(options.write)
|
||||
.append(options.append)
|
||||
.truncate(options.truncate)
|
||||
.create(options.create)
|
||||
.create_new(options.create_new)
|
||||
.mode(0o666);
|
||||
let file = open.open(path)?;
|
||||
Ok(unsafe { OwnedFd::from_raw_fd(std::os::fd::IntoRawFd::into_raw_fd(file)) })
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn read(op: FsOp) -> io::Result<Vec<u8>> {
|
||||
let FsOp::Read { fd, offset, len } = op else {
|
||||
unreachable!("read backend called with non-read op");
|
||||
};
|
||||
|
||||
offload(move || {
|
||||
let mut buffer = vec![0; len];
|
||||
let read = match offset {
|
||||
Some(offset) => unsafe {
|
||||
libc::pread(
|
||||
fd,
|
||||
buffer.as_mut_ptr().cast::<libc::c_void>(),
|
||||
len,
|
||||
offset as libc::off_t,
|
||||
)
|
||||
},
|
||||
None => unsafe { libc::read(fd, buffer.as_mut_ptr().cast::<libc::c_void>(), len) },
|
||||
};
|
||||
if read < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
buffer.truncate(read as usize);
|
||||
Ok(buffer)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn write(op: FsOp) -> io::Result<usize> {
|
||||
let FsOp::Write { fd, offset, data } = op else {
|
||||
unreachable!("write backend called with non-write op");
|
||||
};
|
||||
|
||||
offload(move || {
|
||||
let written = match offset {
|
||||
Some(offset) => unsafe {
|
||||
libc::pwrite(
|
||||
fd,
|
||||
data.as_ptr().cast::<libc::c_void>(),
|
||||
data.len(),
|
||||
offset as libc::off_t,
|
||||
)
|
||||
},
|
||||
None => unsafe { libc::write(fd, data.as_ptr().cast::<libc::c_void>(), data.len()) },
|
||||
};
|
||||
if written < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(written as usize)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn metadata(op: FsOp) -> io::Result<RawMetadata> {
|
||||
let FsOp::Metadata {
|
||||
target,
|
||||
follow_symlinks,
|
||||
} = op
|
||||
else {
|
||||
unreachable!("metadata backend called with non-metadata op");
|
||||
};
|
||||
|
||||
offload(move || {
|
||||
let metadata = match target {
|
||||
MetadataTarget::Path(path) => {
|
||||
if follow_symlinks {
|
||||
std::fs::metadata(path)
|
||||
} else {
|
||||
std::fs::symlink_metadata(path)
|
||||
}
|
||||
}
|
||||
MetadataTarget::File(fd) => {
|
||||
let mut stat = unsafe { std::mem::zeroed::<libc::stat>() };
|
||||
let result = unsafe { libc::fstat(fd, &mut stat) };
|
||||
if result < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
return Ok(raw_metadata_from_stat(&stat));
|
||||
}
|
||||
}?;
|
||||
Ok(raw_metadata_from_std(&metadata))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn sync_all(op: FsOp) -> io::Result<()> {
|
||||
let FsOp::SyncAll { fd } = op else {
|
||||
unreachable!("sync_all backend called with non-sync_all op");
|
||||
};
|
||||
|
||||
offload(move || cvt(unsafe { libc::fsync(fd) }).map(|_| ())).await
|
||||
}
|
||||
|
||||
pub async fn sync_data(op: FsOp) -> io::Result<()> {
|
||||
let FsOp::SyncData { fd } = op else {
|
||||
unreachable!("sync_data backend called with non-sync_data op");
|
||||
};
|
||||
|
||||
offload(move || cvt(unsafe { libc::fcntl(fd, libc::F_FULLFSYNC) }).map(|_| ())).await
|
||||
}
|
||||
|
||||
pub async fn set_len(op: FsOp) -> io::Result<()> {
|
||||
let FsOp::SetLen { fd, len } = op else {
|
||||
unreachable!("set_len backend called with non-set_len op");
|
||||
};
|
||||
|
||||
offload(move || cvt(unsafe { libc::ftruncate(fd, len as libc::off_t) }).map(|_| ())).await
|
||||
}
|
||||
|
||||
pub async fn try_clone(op: FsOp) -> io::Result<OwnedFd> {
|
||||
let FsOp::Duplicate { fd } = op else {
|
||||
unreachable!("try_clone backend called with non-duplicate op");
|
||||
};
|
||||
|
||||
offload(move || {
|
||||
let duplicated = cvt(unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) })?;
|
||||
Ok(unsafe { OwnedFd::from_raw_fd(duplicated) })
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_dir(op: FsOp) -> io::Result<()> {
|
||||
let FsOp::CreateDir { path, mode } = op else {
|
||||
unreachable!("create_dir backend called with non-create_dir op");
|
||||
};
|
||||
|
||||
offload(move || {
|
||||
let c_path = path_to_c_string(path)?;
|
||||
cvt(unsafe { libc::mkdir(c_path.as_ptr(), mode as libc::mode_t) }).map(|_| ())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn remove_file(op: FsOp) -> io::Result<()> {
|
||||
let FsOp::RemoveFile { path } = op else {
|
||||
unreachable!("remove_file backend called with non-remove_file op");
|
||||
};
|
||||
|
||||
offload(move || std::fs::remove_file(path)).await
|
||||
}
|
||||
|
||||
pub async fn remove_dir(op: FsOp) -> io::Result<()> {
|
||||
let FsOp::RemoveDir { path } = op else {
|
||||
unreachable!("remove_dir backend called with non-remove_dir op");
|
||||
};
|
||||
|
||||
offload(move || std::fs::remove_dir(path)).await
|
||||
}
|
||||
|
||||
pub async fn rename(op: FsOp) -> io::Result<()> {
|
||||
let FsOp::Rename { from, to } = op else {
|
||||
unreachable!("rename backend called with non-rename op");
|
||||
};
|
||||
|
||||
offload(move || std::fs::rename(from, to)).await
|
||||
}
|
||||
|
||||
pub async fn close(op: FsOp) -> io::Result<()> {
|
||||
let FsOp::Close { fd } = op else {
|
||||
unreachable!("close backend called with non-close op");
|
||||
};
|
||||
|
||||
offload(move || cvt(unsafe { libc::close(fd) }).map(|_| ())).await
|
||||
}
|
||||
|
||||
pub fn read_dir(op: FsOp) -> io::Result<ReadDirStream> {
|
||||
let FsOp::ReadDir { path } = op else {
|
||||
unreachable!("read_dir backend called with non-read_dir op");
|
||||
};
|
||||
|
||||
ReadDirStream::new(path)
|
||||
}
|
||||
|
||||
pub struct ReadDirStream {
|
||||
state: Arc<ReadDirState>,
|
||||
}
|
||||
|
||||
impl ReadDirStream {
|
||||
fn new(path: PathBuf) -> io::Result<Self> {
|
||||
let state = Arc::new(ReadDirState::new(current_thread_handle()));
|
||||
let producer = Arc::clone(&state);
|
||||
|
||||
if let Err(error) = spawn_blocking(Box::new(move || produce_dir_entries(path, producer))) {
|
||||
state.release_pending();
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
Ok(Self { state })
|
||||
}
|
||||
|
||||
pub async fn next_entry(&mut self) -> io::Result<Option<RawDirEntry>> {
|
||||
poll_fn(|cx| self.state.poll_next(cx)).await
|
||||
}
|
||||
}
|
||||
|
||||
struct ReadDirState {
|
||||
owner: ThreadHandle,
|
||||
queue: Mutex<VecDeque<io::Result<RawDirEntry>>>,
|
||||
done: AtomicBool,
|
||||
pending: AtomicBool,
|
||||
wake_queued: AtomicBool,
|
||||
waker: Mutex<Option<Waker>>,
|
||||
}
|
||||
|
||||
impl ReadDirState {
|
||||
fn new(owner: ThreadHandle) -> Self {
|
||||
owner.begin_async_operation();
|
||||
Self {
|
||||
owner,
|
||||
queue: Mutex::new(VecDeque::new()),
|
||||
done: AtomicBool::new(false),
|
||||
pending: AtomicBool::new(true),
|
||||
wake_queued: AtomicBool::new(false),
|
||||
waker: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn push(self: &Arc<Self>, entry: io::Result<RawDirEntry>) {
|
||||
self.queue.lock().unwrap().push_back(entry);
|
||||
self.notify();
|
||||
}
|
||||
|
||||
fn finish(self: &Arc<Self>) {
|
||||
self.done.store(true, Ordering::Release);
|
||||
self.release_pending();
|
||||
self.notify();
|
||||
}
|
||||
|
||||
fn release_pending(&self) {
|
||||
if self.pending.swap(false, Ordering::AcqRel) {
|
||||
self.owner.finish_async_operation();
|
||||
}
|
||||
}
|
||||
|
||||
fn notify(self: &Arc<Self>) {
|
||||
if self.wake_queued.swap(true, Ordering::AcqRel) {
|
||||
return;
|
||||
}
|
||||
|
||||
let state = Arc::clone(self);
|
||||
if !self.owner.queue_task(move || {
|
||||
state.wake_queued.store(false, Ordering::Release);
|
||||
if let Some(waker) = state.waker.lock().unwrap().take() {
|
||||
waker.wake();
|
||||
}
|
||||
}) {
|
||||
self.wake_queued.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_next(&self, cx: &mut Context<'_>) -> Poll<io::Result<Option<RawDirEntry>>> {
|
||||
if let Some(entry) = self.queue.lock().unwrap().pop_front() {
|
||||
return Poll::Ready(entry.map(Some));
|
||||
}
|
||||
|
||||
if self.done.load(Ordering::Acquire) {
|
||||
return Poll::Ready(Ok(None));
|
||||
}
|
||||
|
||||
*self.waker.lock().unwrap() = Some(cx.waker().clone());
|
||||
|
||||
if let Some(entry) = self.queue.lock().unwrap().pop_front() {
|
||||
let _ = self.waker.lock().unwrap().take();
|
||||
return Poll::Ready(entry.map(Some));
|
||||
}
|
||||
|
||||
if self.done.load(Ordering::Acquire) {
|
||||
let _ = self.waker.lock().unwrap().take();
|
||||
return Poll::Ready(Ok(None));
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ReadDirStream {
|
||||
fn drop(&mut self) {
|
||||
self.state.release_pending();
|
||||
}
|
||||
}
|
||||
|
||||
fn produce_dir_entries(path: PathBuf, state: Arc<ReadDirState>) {
|
||||
match std::fs::read_dir(path) {
|
||||
Ok(entries) => {
|
||||
for entry in entries {
|
||||
match entry {
|
||||
Ok(entry) => {
|
||||
let file_name = entry.file_name();
|
||||
state.push(Ok(RawDirEntry {
|
||||
path: entry.path(),
|
||||
file_name,
|
||||
}));
|
||||
}
|
||||
Err(error) => state.push(Err(error)),
|
||||
}
|
||||
}
|
||||
state.finish();
|
||||
}
|
||||
Err(error) => {
|
||||
state.push(Err(error));
|
||||
state.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn offload<T: Send + 'static>(
|
||||
work: impl FnOnce() -> io::Result<T> + Send + 'static,
|
||||
) -> io::Result<T> {
|
||||
let (future, handle) = completion_for_current_thread::<io::Result<T>>();
|
||||
let handle_for_task = handle.clone();
|
||||
if let Err(error) = spawn_blocking(Box::new(move || handle_for_task.complete(work()))) {
|
||||
handle.complete(Err(error));
|
||||
}
|
||||
future.await
|
||||
}
|
||||
|
||||
struct BlockingPool {
|
||||
sender: mpsc::Sender<BlockingTask>,
|
||||
}
|
||||
|
||||
impl BlockingPool {
|
||||
fn spawn(&self, task: BlockingTask) -> io::Result<()> {
|
||||
self.sender.send(task).map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"filesystem blocking worker pool has stopped",
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_blocking(task: BlockingTask) -> io::Result<()> {
|
||||
blocking_pool()?.spawn(task)
|
||||
}
|
||||
|
||||
fn blocking_pool() -> io::Result<&'static BlockingPool> {
|
||||
match BLOCKING_POOL.get_or_init(create_blocking_pool) {
|
||||
Ok(pool) => Ok(pool),
|
||||
Err(error) => Err(io::Error::new(error.kind(), error.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_blocking_pool() -> io::Result<BlockingPool> {
|
||||
let (sender, receiver) = mpsc::channel::<BlockingTask>();
|
||||
let receiver = Arc::new(Mutex::new(receiver));
|
||||
let worker_count = std::thread::available_parallelism()
|
||||
.map(usize::from)
|
||||
.unwrap_or(2)
|
||||
.clamp(2, 4);
|
||||
let mut spawned = 0usize;
|
||||
let mut last_error = None;
|
||||
|
||||
for index in 0..worker_count {
|
||||
let receiver = Arc::clone(&receiver);
|
||||
match thread::Builder::new()
|
||||
.name(format!("ruin-runtime-fs-offload-{index}"))
|
||||
.spawn(move || {
|
||||
loop {
|
||||
let task = receiver.lock().unwrap().recv();
|
||||
match task {
|
||||
Ok(task) => task(),
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Ok(_) => spawned += 1,
|
||||
Err(error) => last_error = Some(error),
|
||||
}
|
||||
}
|
||||
|
||||
if spawned == 0 {
|
||||
return Err(io::Error::other(
|
||||
last_error.expect("at least one worker spawn should have been attempted"),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(BlockingPool { sender })
|
||||
}
|
||||
|
||||
fn path_to_c_string(path: PathBuf) -> io::Result<CString> {
|
||||
CString::new(path.as_os_str().as_bytes()).map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"path contains interior NUL bytes",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn raw_metadata_from_std(metadata: &std::fs::Metadata) -> RawMetadata {
|
||||
let file_type = metadata.file_type();
|
||||
let kind = if file_type.is_file() {
|
||||
FileType::File
|
||||
} else if file_type.is_dir() {
|
||||
FileType::Directory
|
||||
} else if file_type.is_symlink() {
|
||||
FileType::Symlink
|
||||
} else if file_type.is_block_device() {
|
||||
FileType::BlockDevice
|
||||
} else if file_type.is_char_device() {
|
||||
FileType::CharacterDevice
|
||||
} else if file_type.is_fifo() {
|
||||
FileType::Fifo
|
||||
} else if file_type.is_socket() {
|
||||
FileType::Socket
|
||||
} else {
|
||||
FileType::Unknown
|
||||
};
|
||||
|
||||
RawMetadata {
|
||||
file_type: kind,
|
||||
mode: (metadata.mode() & 0o7777) as u16,
|
||||
len: metadata.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_metadata_from_stat(stat: &libc::stat) -> RawMetadata {
|
||||
let kind = match stat.st_mode & libc::S_IFMT {
|
||||
libc::S_IFREG => FileType::File,
|
||||
libc::S_IFDIR => FileType::Directory,
|
||||
libc::S_IFLNK => FileType::Symlink,
|
||||
libc::S_IFBLK => FileType::BlockDevice,
|
||||
libc::S_IFCHR => FileType::CharacterDevice,
|
||||
libc::S_IFIFO => FileType::Fifo,
|
||||
libc::S_IFSOCK => FileType::Socket,
|
||||
_ => FileType::Unknown,
|
||||
};
|
||||
|
||||
RawMetadata {
|
||||
file_type: kind,
|
||||
mode: stat.st_mode & 0o7777,
|
||||
len: stat.st_size as u64,
|
||||
}
|
||||
}
|
||||
|
||||
fn cvt(value: libc::c_int) -> io::Result<libc::c_int> {
|
||||
if value < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
6
lib/runtime/src/sys/macos/mod.rs
Normal file
6
lib/runtime/src/sys/macos/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
//! macOS backend modules.
|
||||
|
||||
pub mod channel;
|
||||
pub mod fd;
|
||||
pub mod fs;
|
||||
pub mod net;
|
||||
823
lib/runtime/src/sys/macos/net.rs
Normal file
823
lib/runtime/src/sys/macos/net.rs
Normal file
@@ -0,0 +1,823 @@
|
||||
//! macOS networking backend.
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::future::Future;
|
||||
use std::io;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::net::{
|
||||
Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs,
|
||||
};
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::pin::Pin;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::op::completion::completion_for_current_thread;
|
||||
use crate::op::net::{AcceptedSocket, NetOp, ReceivedDatagram};
|
||||
|
||||
const DEFAULT_LISTENER_BACKLOG: i32 = 1024;
|
||||
|
||||
type RecvFuture = Pin<Box<dyn Future<Output = io::Result<Vec<u8>>> + 'static>>;
|
||||
type SendFuture = Pin<Box<dyn Future<Output = io::Result<usize>> + 'static>>;
|
||||
type ShutdownFuture = Pin<Box<dyn Future<Output = io::Result<()>> + 'static>>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ExecutionPath {
|
||||
Kqueue,
|
||||
Offload,
|
||||
}
|
||||
|
||||
pub fn execution_path(op: &NetOp) -> ExecutionPath {
|
||||
match op {
|
||||
NetOp::Socket { .. }
|
||||
| NetOp::Connect { .. }
|
||||
| NetOp::Bind { .. }
|
||||
| NetOp::Listen { .. }
|
||||
| NetOp::Accept { .. }
|
||||
| NetOp::Send { .. }
|
||||
| NetOp::SendTo { .. }
|
||||
| NetOp::Recv { .. }
|
||||
| NetOp::RecvFrom { .. }
|
||||
| NetOp::Shutdown { .. }
|
||||
| NetOp::Close { .. } => ExecutionPath::Kqueue,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_addrs<A>(addr: A) -> io::Result<Vec<SocketAddr>>
|
||||
where
|
||||
A: ToSocketAddrs + Send + 'static,
|
||||
{
|
||||
offload(move || {
|
||||
let addrs = addr.to_socket_addrs()?.collect::<Vec<_>>();
|
||||
if addrs.is_empty() {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"address resolved to no socket addresses",
|
||||
))
|
||||
} else {
|
||||
Ok(addrs)
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn socket(op: NetOp) -> io::Result<OwnedFd> {
|
||||
let NetOp::Socket {
|
||||
domain,
|
||||
socket_type,
|
||||
protocol,
|
||||
flags,
|
||||
} = op
|
||||
else {
|
||||
unreachable!("socket backend called with non-socket op");
|
||||
};
|
||||
|
||||
socket_sync(domain, socket_type, protocol, flags)
|
||||
}
|
||||
|
||||
pub async fn connect(op: NetOp) -> io::Result<()> {
|
||||
let NetOp::Connect { fd, addr } = op else {
|
||||
unreachable!("connect backend called with non-connect op");
|
||||
};
|
||||
|
||||
connect_async(fd, RawSocketAddr::from_socket_addr(addr)).await
|
||||
}
|
||||
|
||||
pub async fn bind(op: NetOp) -> io::Result<()> {
|
||||
let NetOp::Bind { fd, addr } = op else {
|
||||
unreachable!("bind backend called with non-bind op");
|
||||
};
|
||||
|
||||
bind_sync(fd, RawSocketAddr::from_socket_addr(addr))
|
||||
}
|
||||
|
||||
pub async fn listen(op: NetOp) -> io::Result<()> {
|
||||
let NetOp::Listen { fd, backlog } = op else {
|
||||
unreachable!("listen backend called with non-listen op");
|
||||
};
|
||||
|
||||
listen_sync(fd, backlog)
|
||||
}
|
||||
|
||||
pub async fn accept(op: NetOp) -> io::Result<AcceptedSocket> {
|
||||
let NetOp::Accept { fd } = op else {
|
||||
unreachable!("accept backend called with non-accept op");
|
||||
};
|
||||
|
||||
accept_async(fd).await
|
||||
}
|
||||
|
||||
pub async fn send(op: NetOp) -> io::Result<usize> {
|
||||
let NetOp::Send { fd, data, flags } = op else {
|
||||
unreachable!("send backend called with non-send op");
|
||||
};
|
||||
|
||||
send_async(fd, data, flags).await
|
||||
}
|
||||
|
||||
pub async fn send_to(op: NetOp) -> io::Result<usize> {
|
||||
let NetOp::SendTo {
|
||||
fd,
|
||||
target,
|
||||
data,
|
||||
flags,
|
||||
} = op
|
||||
else {
|
||||
unreachable!("send_to backend called with non-send_to op");
|
||||
};
|
||||
|
||||
send_to_async(fd, target, data, flags).await
|
||||
}
|
||||
|
||||
pub async fn recv(op: NetOp) -> io::Result<Vec<u8>> {
|
||||
let NetOp::Recv { fd, len, flags } = op else {
|
||||
unreachable!("recv backend called with non-recv op");
|
||||
};
|
||||
|
||||
recv_async(fd, len, flags).await
|
||||
}
|
||||
|
||||
pub async fn recv_from(op: NetOp) -> io::Result<ReceivedDatagram> {
|
||||
let NetOp::RecvFrom { fd, len, flags } = op else {
|
||||
unreachable!("recv_from backend called with non-recv_from op");
|
||||
};
|
||||
|
||||
recv_from_async(fd, len, flags).await
|
||||
}
|
||||
|
||||
pub async fn shutdown(op: NetOp) -> io::Result<()> {
|
||||
let NetOp::Shutdown { fd, how } = op else {
|
||||
unreachable!("shutdown backend called with non-shutdown op");
|
||||
};
|
||||
|
||||
shutdown_sync(fd, how)
|
||||
}
|
||||
|
||||
pub async fn close(op: NetOp) -> io::Result<()> {
|
||||
let NetOp::Close { fd } = op else {
|
||||
unreachable!("close backend called with non-close op");
|
||||
};
|
||||
|
||||
close_sync(fd)
|
||||
}
|
||||
|
||||
pub async fn connect_stream(addr: SocketAddr) -> io::Result<OwnedFd> {
|
||||
match connect_stream_inner(addr).await {
|
||||
Err(error) if should_try_ipv4_loopback(addr, &error) => {
|
||||
connect_stream_inner(localhost_v4(addr)).await
|
||||
}
|
||||
result => result,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn bind_listener(addr: SocketAddr, backlog: Option<i32>) -> io::Result<OwnedFd> {
|
||||
match bind_listener_inner(addr, backlog).await {
|
||||
Err(error) if should_try_ipv4_loopback(addr, &error) => {
|
||||
bind_listener_inner(localhost_v4(addr), backlog).await
|
||||
}
|
||||
result => result,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn bind_datagram(addr: SocketAddr) -> io::Result<OwnedFd> {
|
||||
match bind_datagram_inner(addr).await {
|
||||
Err(error) if should_try_ipv4_loopback(addr, &error) => {
|
||||
bind_datagram_inner(localhost_v4(addr)).await
|
||||
}
|
||||
result => result,
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_stream_inner(addr: SocketAddr) -> io::Result<OwnedFd> {
|
||||
let stream = socket(NetOp::Socket {
|
||||
domain: socket_domain(addr),
|
||||
socket_type: libc::SOCK_STREAM,
|
||||
protocol: 0,
|
||||
flags: 0,
|
||||
})
|
||||
.await?;
|
||||
|
||||
connect(NetOp::Connect {
|
||||
fd: stream.as_raw_fd(),
|
||||
addr,
|
||||
})
|
||||
.await?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
async fn bind_listener_inner(addr: SocketAddr, backlog: Option<i32>) -> io::Result<OwnedFd> {
|
||||
let listener = socket(NetOp::Socket {
|
||||
domain: socket_domain(addr),
|
||||
socket_type: libc::SOCK_STREAM,
|
||||
protocol: 0,
|
||||
flags: 0,
|
||||
})
|
||||
.await?;
|
||||
|
||||
set_reuse_addr(listener.as_raw_fd(), true)?;
|
||||
|
||||
bind(NetOp::Bind {
|
||||
fd: listener.as_raw_fd(),
|
||||
addr,
|
||||
})
|
||||
.await?;
|
||||
listen(NetOp::Listen {
|
||||
fd: listener.as_raw_fd(),
|
||||
backlog: backlog.unwrap_or(DEFAULT_LISTENER_BACKLOG),
|
||||
})
|
||||
.await?;
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
async fn bind_datagram_inner(addr: SocketAddr) -> io::Result<OwnedFd> {
|
||||
let socket = socket(NetOp::Socket {
|
||||
domain: socket_domain(addr),
|
||||
socket_type: libc::SOCK_DGRAM,
|
||||
protocol: 0,
|
||||
flags: 0,
|
||||
})
|
||||
.await?;
|
||||
|
||||
bind(NetOp::Bind {
|
||||
fd: socket.as_raw_fd(),
|
||||
addr,
|
||||
})
|
||||
.await?;
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
pub async fn duplicate(fd: RawFd) -> io::Result<OwnedFd> {
|
||||
let duplicated = cvt(unsafe { libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, 0) })?;
|
||||
set_nonblocking(duplicated)?;
|
||||
Ok(unsafe { OwnedFd::from_raw_fd(duplicated) })
|
||||
}
|
||||
|
||||
pub async fn recv_timeout(
|
||||
fd: RawFd,
|
||||
len: usize,
|
||||
flags: i32,
|
||||
timeout: Duration,
|
||||
) -> io::Result<Vec<u8>> {
|
||||
io_timeout(timeout, recv_async(fd, len, flags)).await
|
||||
}
|
||||
|
||||
pub async fn send_timeout(
|
||||
fd: RawFd,
|
||||
data: Vec<u8>,
|
||||
flags: i32,
|
||||
timeout: Duration,
|
||||
) -> io::Result<usize> {
|
||||
io_timeout(timeout, send_async(fd, data, flags)).await
|
||||
}
|
||||
|
||||
pub async fn recv_from_timeout(
|
||||
fd: RawFd,
|
||||
len: usize,
|
||||
flags: i32,
|
||||
timeout: Duration,
|
||||
) -> io::Result<ReceivedDatagram> {
|
||||
io_timeout(timeout, recv_from_async(fd, len, flags)).await
|
||||
}
|
||||
|
||||
pub async fn send_to_timeout(
|
||||
fd: RawFd,
|
||||
data: Vec<u8>,
|
||||
target: SocketAddr,
|
||||
flags: i32,
|
||||
timeout: Duration,
|
||||
) -> io::Result<usize> {
|
||||
io_timeout(timeout, send_to_async(fd, target, data, flags)).await
|
||||
}
|
||||
|
||||
pub async fn connect_stream_timeout(addr: SocketAddr, timeout: Duration) -> io::Result<OwnedFd> {
|
||||
let fd = socket_sync(socket_domain(addr), libc::SOCK_STREAM, 0, 0)?;
|
||||
if let Err(error) = io_timeout(
|
||||
timeout,
|
||||
connect_async(fd.as_raw_fd(), RawSocketAddr::from_socket_addr(addr)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
drop(fd);
|
||||
return Err(error);
|
||||
}
|
||||
Ok(fd)
|
||||
}
|
||||
|
||||
pub fn local_addr(fd: RawFd) -> io::Result<SocketAddr> {
|
||||
socket_addr_with(libc::getsockname, fd)
|
||||
}
|
||||
|
||||
pub fn peer_addr(fd: RawFd) -> io::Result<SocketAddr> {
|
||||
socket_addr_with(libc::getpeername, fd)
|
||||
}
|
||||
|
||||
pub fn nodelay(fd: RawFd) -> io::Result<bool> {
|
||||
let mut value = 0;
|
||||
let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
|
||||
cvt(unsafe {
|
||||
libc::getsockopt(
|
||||
fd,
|
||||
libc::IPPROTO_TCP,
|
||||
libc::TCP_NODELAY,
|
||||
&mut value as *mut libc::c_int as *mut c_void,
|
||||
&mut len,
|
||||
)
|
||||
})?;
|
||||
Ok(value != 0)
|
||||
}
|
||||
|
||||
pub fn broadcast(fd: RawFd) -> io::Result<bool> {
|
||||
getsockopt_int(fd, libc::SOL_SOCKET, libc::SO_BROADCAST).map(|value| value != 0)
|
||||
}
|
||||
|
||||
pub fn set_broadcast(fd: RawFd, enabled: bool) -> io::Result<()> {
|
||||
setsockopt_int(fd, libc::SOL_SOCKET, libc::SO_BROADCAST, enabled.into())
|
||||
}
|
||||
|
||||
pub fn ttl(fd: RawFd) -> io::Result<u32> {
|
||||
match socket_family(fd)? {
|
||||
libc::AF_INET => {
|
||||
getsockopt_int(fd, libc::IPPROTO_IP, libc::IP_TTL).map(|value| value as u32)
|
||||
}
|
||||
libc::AF_INET6 => getsockopt_int(fd, libc::IPPROTO_IPV6, libc::IPV6_UNICAST_HOPS)
|
||||
.map(|value| value as u32),
|
||||
family => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("unsupported socket family {family} for TTL"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_ttl(fd: RawFd, ttl: u32) -> io::Result<()> {
|
||||
let ttl = i32::try_from(ttl)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "TTL exceeds i32 range"))?;
|
||||
match socket_family(fd)? {
|
||||
libc::AF_INET => setsockopt_int(fd, libc::IPPROTO_IP, libc::IP_TTL, ttl),
|
||||
libc::AF_INET6 => setsockopt_int(fd, libc::IPPROTO_IPV6, libc::IPV6_UNICAST_HOPS, ttl),
|
||||
family => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("unsupported socket family {family} for TTL"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_nodelay(fd: RawFd, enabled: bool) -> io::Result<()> {
|
||||
let value: libc::c_int = enabled.into();
|
||||
cvt(unsafe {
|
||||
libc::setsockopt(
|
||||
fd,
|
||||
libc::IPPROTO_TCP,
|
||||
libc::TCP_NODELAY,
|
||||
&value as *const libc::c_int as *const c_void,
|
||||
std::mem::size_of_val(&value) as libc::socklen_t,
|
||||
)
|
||||
})
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub fn recv_future(fd: RawFd, len: usize) -> RecvFuture {
|
||||
Box::pin(recv(NetOp::Recv { fd, len, flags: 0 }))
|
||||
}
|
||||
|
||||
pub fn send_future(fd: RawFd, data: Vec<u8>) -> SendFuture {
|
||||
Box::pin(send(NetOp::Send { fd, data, flags: 0 }))
|
||||
}
|
||||
|
||||
pub fn shutdown_future(fd: RawFd, how: Shutdown) -> ShutdownFuture {
|
||||
Box::pin(shutdown(NetOp::Shutdown { fd, how }))
|
||||
}
|
||||
|
||||
async fn offload<T: Send + 'static>(
|
||||
work: impl FnOnce() -> io::Result<T> + Send + 'static,
|
||||
) -> io::Result<T> {
|
||||
let (future, handle) = completion_for_current_thread::<io::Result<T>>();
|
||||
let handle_for_thread = handle.clone();
|
||||
if let Err(error) = thread::Builder::new()
|
||||
.name("ruin-runtime-net-offload".into())
|
||||
.spawn(move || handle_for_thread.complete(work()))
|
||||
{
|
||||
handle.complete(Err(io::Error::other(error)));
|
||||
}
|
||||
future.await
|
||||
}
|
||||
|
||||
async fn io_timeout<T>(
|
||||
timeout: Duration,
|
||||
future: impl Future<Output = io::Result<T>>,
|
||||
) -> io::Result<T> {
|
||||
crate::time::timeout(timeout, future)
|
||||
.await
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "operation timed out"))?
|
||||
}
|
||||
|
||||
fn socket_domain(addr: SocketAddr) -> i32 {
|
||||
match addr {
|
||||
SocketAddr::V4(_) => libc::AF_INET,
|
||||
SocketAddr::V6(_) => libc::AF_INET6,
|
||||
}
|
||||
}
|
||||
|
||||
fn shutdown_how(how: Shutdown) -> i32 {
|
||||
match how {
|
||||
Shutdown::Read => libc::SHUT_RD,
|
||||
Shutdown::Write => libc::SHUT_WR,
|
||||
Shutdown::Both => libc::SHUT_RDWR,
|
||||
}
|
||||
}
|
||||
|
||||
fn socket_addr_with(
|
||||
op: unsafe extern "C" fn(RawFd, *mut libc::sockaddr, *mut libc::socklen_t) -> libc::c_int,
|
||||
fd: RawFd,
|
||||
) -> io::Result<SocketAddr> {
|
||||
let mut storage = MaybeUninit::<libc::sockaddr_storage>::zeroed();
|
||||
let mut len = std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
|
||||
cvt(unsafe { op(fd, storage.as_mut_ptr().cast::<libc::sockaddr>(), &mut len) })?;
|
||||
let storage = unsafe { storage.assume_init() };
|
||||
socket_addr_from_storage(&storage, len)
|
||||
}
|
||||
|
||||
fn set_reuse_addr(fd: RawFd, enabled: bool) -> io::Result<()> {
|
||||
setsockopt_int(fd, libc::SOL_SOCKET, libc::SO_REUSEADDR, enabled.into())
|
||||
}
|
||||
|
||||
fn socket_family(fd: RawFd) -> io::Result<i32> {
|
||||
let mut storage = MaybeUninit::<libc::sockaddr_storage>::zeroed();
|
||||
let mut len = std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
|
||||
cvt(unsafe { libc::getsockname(fd, storage.as_mut_ptr().cast::<libc::sockaddr>(), &mut len) })?;
|
||||
let storage = unsafe { storage.assume_init() };
|
||||
Ok(storage.ss_family as i32)
|
||||
}
|
||||
|
||||
fn getsockopt_int(fd: RawFd, level: i32, name: i32) -> io::Result<i32> {
|
||||
let mut value = 0;
|
||||
let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
|
||||
cvt(unsafe {
|
||||
libc::getsockopt(
|
||||
fd,
|
||||
level,
|
||||
name,
|
||||
&mut value as *mut libc::c_int as *mut c_void,
|
||||
&mut len,
|
||||
)
|
||||
})?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn setsockopt_int(fd: RawFd, level: i32, name: i32, value: i32) -> io::Result<()> {
|
||||
cvt(unsafe {
|
||||
libc::setsockopt(
|
||||
fd,
|
||||
level,
|
||||
name,
|
||||
&value as *const libc::c_int as *const c_void,
|
||||
std::mem::size_of_val(&value) as libc::socklen_t,
|
||||
)
|
||||
})
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
fn socket_addr_from_storage(
|
||||
storage: &libc::sockaddr_storage,
|
||||
len: libc::socklen_t,
|
||||
) -> io::Result<SocketAddr> {
|
||||
match storage.ss_family as i32 {
|
||||
libc::AF_INET => {
|
||||
if len < std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"sockaddr_in length is truncated",
|
||||
));
|
||||
}
|
||||
let addr = unsafe { *(storage as *const _ as *const libc::sockaddr_in) };
|
||||
Ok(SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr)),
|
||||
u16::from_be(addr.sin_port),
|
||||
)))
|
||||
}
|
||||
libc::AF_INET6 => {
|
||||
if len < std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"sockaddr_in6 length is truncated",
|
||||
));
|
||||
}
|
||||
let addr = unsafe { *(storage as *const _ as *const libc::sockaddr_in6) };
|
||||
Ok(SocketAddr::V6(SocketAddrV6::new(
|
||||
Ipv6Addr::from(addr.sin6_addr.s6_addr),
|
||||
u16::from_be(addr.sin6_port),
|
||||
addr.sin6_flowinfo,
|
||||
addr.sin6_scope_id,
|
||||
)))
|
||||
}
|
||||
family => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("unsupported socket family {family}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct RawSocketAddr {
|
||||
storage: libc::sockaddr_storage,
|
||||
len: libc::socklen_t,
|
||||
}
|
||||
|
||||
impl RawSocketAddr {
|
||||
fn from_socket_addr(addr: SocketAddr) -> Self {
|
||||
match addr {
|
||||
SocketAddr::V4(addr) => {
|
||||
let sockaddr = libc::sockaddr_in {
|
||||
sin_len: std::mem::size_of::<libc::sockaddr_in>() as u8,
|
||||
sin_family: libc::AF_INET as libc::sa_family_t,
|
||||
sin_port: addr.port().to_be(),
|
||||
sin_addr: libc::in_addr {
|
||||
s_addr: u32::from_ne_bytes(addr.ip().octets()),
|
||||
},
|
||||
sin_zero: [0; 8],
|
||||
};
|
||||
let mut storage =
|
||||
unsafe { MaybeUninit::<libc::sockaddr_storage>::zeroed().assume_init() };
|
||||
unsafe {
|
||||
std::ptr::write(
|
||||
&mut storage as *mut libc::sockaddr_storage as *mut libc::sockaddr_in,
|
||||
sockaddr,
|
||||
);
|
||||
}
|
||||
Self {
|
||||
storage,
|
||||
len: std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
|
||||
}
|
||||
}
|
||||
SocketAddr::V6(addr) => {
|
||||
let sockaddr = libc::sockaddr_in6 {
|
||||
sin6_len: std::mem::size_of::<libc::sockaddr_in6>() as u8,
|
||||
sin6_family: libc::AF_INET6 as libc::sa_family_t,
|
||||
sin6_port: addr.port().to_be(),
|
||||
sin6_flowinfo: addr.flowinfo(),
|
||||
sin6_addr: libc::in6_addr {
|
||||
s6_addr: addr.ip().octets(),
|
||||
},
|
||||
sin6_scope_id: addr.scope_id(),
|
||||
};
|
||||
let mut storage =
|
||||
unsafe { MaybeUninit::<libc::sockaddr_storage>::zeroed().assume_init() };
|
||||
unsafe {
|
||||
std::ptr::write(
|
||||
&mut storage as *mut libc::sockaddr_storage as *mut libc::sockaddr_in6,
|
||||
sockaddr,
|
||||
);
|
||||
}
|
||||
Self {
|
||||
storage,
|
||||
len: std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn as_ptr(&self) -> *const libc::sockaddr {
|
||||
&self.storage as *const libc::sockaddr_storage as *const libc::sockaddr
|
||||
}
|
||||
|
||||
fn len(&self) -> libc::socklen_t {
|
||||
self.len
|
||||
}
|
||||
}
|
||||
|
||||
fn socket_sync(domain: i32, socket_type: i32, protocol: i32, _flags: u32) -> io::Result<OwnedFd> {
|
||||
let fd = cvt(unsafe { libc::socket(domain, socket_type, protocol) })?;
|
||||
set_cloexec(fd)?;
|
||||
set_nonblocking(fd)?;
|
||||
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
|
||||
}
|
||||
|
||||
async fn connect_async(fd: RawFd, addr: RawSocketAddr) -> io::Result<()> {
|
||||
loop {
|
||||
let result = unsafe { libc::connect(fd, addr.as_ptr(), addr.len()) };
|
||||
if result == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let error = io::Error::last_os_error();
|
||||
match error.raw_os_error() {
|
||||
Some(libc::EINTR) => continue,
|
||||
Some(libc::EINPROGRESS) | Some(libc::EALREADY) => {
|
||||
crate::sys::current::fd::wait_writable(fd).await?;
|
||||
return socket_error(fd);
|
||||
}
|
||||
Some(libc::EISCONN) => return Ok(()),
|
||||
_ => return Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_sync(fd: RawFd, addr: RawSocketAddr) -> io::Result<()> {
|
||||
cvt(unsafe { libc::bind(fd, addr.as_ptr(), addr.len()) }).map(|_| ())
|
||||
}
|
||||
|
||||
fn listen_sync(fd: RawFd, backlog: i32) -> io::Result<()> {
|
||||
cvt(unsafe { libc::listen(fd, backlog) }).map(|_| ())
|
||||
}
|
||||
|
||||
fn accept_sync(fd: RawFd) -> io::Result<AcceptedSocket> {
|
||||
let mut storage = MaybeUninit::<libc::sockaddr_storage>::zeroed();
|
||||
let mut len = std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
|
||||
let accepted_fd =
|
||||
cvt(unsafe { libc::accept(fd, storage.as_mut_ptr().cast::<libc::sockaddr>(), &mut len) })?;
|
||||
set_cloexec(accepted_fd)?;
|
||||
let storage = unsafe { storage.assume_init() };
|
||||
let peer_addr = socket_addr_from_storage(&storage, len)?;
|
||||
Ok(AcceptedSocket {
|
||||
fd: accepted_fd,
|
||||
peer_addr,
|
||||
})
|
||||
}
|
||||
|
||||
async fn accept_async(fd: RawFd) -> io::Result<AcceptedSocket> {
|
||||
loop {
|
||||
match accept_sync(fd) {
|
||||
Ok(socket) => {
|
||||
set_nonblocking(socket.fd)?;
|
||||
return Ok(socket);
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||
crate::sys::current::fd::wait_readable(fd).await?;
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_async(fd: RawFd, data: Vec<u8>, flags: i32) -> io::Result<usize> {
|
||||
loop {
|
||||
match send_slice_sync(fd, &data, flags) {
|
||||
Ok(written) => return Ok(written),
|
||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||
crate::sys::current::fd::wait_writable(fd).await?;
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_slice_sync(fd: RawFd, data: &[u8], flags: i32) -> io::Result<usize> {
|
||||
let written = unsafe { libc::send(fd, data.as_ptr().cast::<c_void>(), data.len(), flags) };
|
||||
cvt_long(written).map(|written| written as usize)
|
||||
}
|
||||
|
||||
async fn send_to_async(
|
||||
fd: RawFd,
|
||||
target: SocketAddr,
|
||||
data: Vec<u8>,
|
||||
flags: i32,
|
||||
) -> io::Result<usize> {
|
||||
loop {
|
||||
match send_to_slice_sync(fd, target, &data, flags) {
|
||||
Ok(written) => return Ok(written),
|
||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||
crate::sys::current::fd::wait_writable(fd).await?;
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_to_slice_sync(fd: RawFd, target: SocketAddr, data: &[u8], flags: i32) -> io::Result<usize> {
|
||||
let addr = RawSocketAddr::from_socket_addr(target);
|
||||
let written = unsafe {
|
||||
libc::sendto(
|
||||
fd,
|
||||
data.as_ptr().cast::<c_void>(),
|
||||
data.len(),
|
||||
flags,
|
||||
addr.as_ptr(),
|
||||
addr.len(),
|
||||
)
|
||||
};
|
||||
cvt_long(written).map(|written| written as usize)
|
||||
}
|
||||
|
||||
async fn recv_async(fd: RawFd, len: usize, flags: i32) -> io::Result<Vec<u8>> {
|
||||
loop {
|
||||
match recv_sync(fd, len, flags) {
|
||||
Ok(data) => return Ok(data),
|
||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||
crate::sys::current::fd::wait_readable(fd).await?;
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn recv_sync(fd: RawFd, len: usize, flags: i32) -> io::Result<Vec<u8>> {
|
||||
let mut data = vec![0u8; len];
|
||||
let read = unsafe { libc::recv(fd, data.as_mut_ptr().cast::<c_void>(), len, flags) };
|
||||
let read = cvt_long(read)? as usize;
|
||||
data.truncate(read);
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
async fn recv_from_async(fd: RawFd, len: usize, flags: i32) -> io::Result<ReceivedDatagram> {
|
||||
loop {
|
||||
match recv_from_sync(fd, len, flags) {
|
||||
Ok(datagram) => return Ok(datagram),
|
||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||
crate::sys::current::fd::wait_readable(fd).await?;
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn recv_from_sync(fd: RawFd, len: usize, flags: i32) -> io::Result<ReceivedDatagram> {
|
||||
let mut data = vec![0u8; len];
|
||||
let mut storage = MaybeUninit::<libc::sockaddr_storage>::zeroed();
|
||||
let mut addr_len = std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
|
||||
let read = unsafe {
|
||||
libc::recvfrom(
|
||||
fd,
|
||||
data.as_mut_ptr().cast::<c_void>(),
|
||||
len,
|
||||
flags,
|
||||
storage.as_mut_ptr().cast::<libc::sockaddr>(),
|
||||
&mut addr_len,
|
||||
)
|
||||
};
|
||||
let read = cvt_long(read)? as usize;
|
||||
data.truncate(read);
|
||||
let storage = unsafe { storage.assume_init() };
|
||||
let peer_addr = socket_addr_from_storage(&storage, addr_len)?;
|
||||
Ok(ReceivedDatagram { data, peer_addr })
|
||||
}
|
||||
|
||||
fn shutdown_sync(fd: RawFd, how: Shutdown) -> io::Result<()> {
|
||||
cvt(unsafe { libc::shutdown(fd, shutdown_how(how)) }).map(|_| ())
|
||||
}
|
||||
|
||||
fn close_sync(fd: RawFd) -> io::Result<()> {
|
||||
cvt(unsafe { libc::close(fd) }).map(|_| ())
|
||||
}
|
||||
|
||||
fn set_cloexec(fd: RawFd) -> io::Result<()> {
|
||||
let flags = cvt(unsafe { libc::fcntl(fd, libc::F_GETFD) })?;
|
||||
cvt(unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_nonblocking(fd: RawFd) -> io::Result<()> {
|
||||
let flags = cvt(unsafe { libc::fcntl(fd, libc::F_GETFL) })?;
|
||||
cvt(unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) })?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_try_ipv4_loopback(addr: SocketAddr, error: &io::Error) -> bool {
|
||||
matches!(addr, SocketAddr::V6(v6) if v6.ip().is_loopback())
|
||||
&& matches!(
|
||||
error.raw_os_error(),
|
||||
Some(libc::EADDRNOTAVAIL | libc::EAFNOSUPPORT | libc::ENETUNREACH)
|
||||
)
|
||||
}
|
||||
|
||||
fn socket_error(fd: RawFd) -> io::Result<()> {
|
||||
let mut so_error: libc::c_int = 0;
|
||||
let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
|
||||
cvt(unsafe {
|
||||
libc::getsockopt(
|
||||
fd,
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_ERROR,
|
||||
&mut so_error as *mut libc::c_int as *mut c_void,
|
||||
&mut len,
|
||||
)
|
||||
})?;
|
||||
if so_error == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::from_raw_os_error(so_error))
|
||||
}
|
||||
}
|
||||
|
||||
fn localhost_v4(addr: SocketAddr) -> SocketAddr {
|
||||
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, addr.port()))
|
||||
}
|
||||
|
||||
fn cvt(value: libc::c_int) -> io::Result<libc::c_int> {
|
||||
if value < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn cvt_long(value: libc::ssize_t) -> io::Result<libc::ssize_t> {
|
||||
if value < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,10 @@
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
pub mod linux;
|
||||
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||
pub mod macos;
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
pub use linux as current;
|
||||
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||
pub use macos as current;
|
||||
|
||||
@@ -67,7 +67,9 @@ fn bench_scroll_list(c: &mut Criterion) {
|
||||
let viewport_height = 640.0;
|
||||
let n = 500;
|
||||
|
||||
let mut scroll_box = Element::scroll_box(0.0).width(400.0).height(viewport_height);
|
||||
let mut scroll_box = Element::scroll_box(0.0)
|
||||
.width(400.0)
|
||||
.height(viewport_height);
|
||||
for i in 0..n {
|
||||
scroll_box = scroll_box.child(
|
||||
Element::new()
|
||||
@@ -90,5 +92,10 @@ fn bench_scroll_list(c: &mut Criterion) {
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_static_tree, bench_single_change, bench_scroll_list);
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_static_tree,
|
||||
bench_single_change,
|
||||
bench_scroll_list
|
||||
);
|
||||
criterion_main!(benches);
|
||||
|
||||
@@ -107,6 +107,7 @@ fn log_platform_event(event: &PlatformEvent) {
|
||||
"clipboard text received"
|
||||
);
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
PlatformEvent::PrimarySelectionText { window_id, text } => {
|
||||
tracing::debug!(
|
||||
event = "primary_selection_text",
|
||||
|
||||
@@ -128,7 +128,12 @@ impl LayoutNode {
|
||||
/// Return a copy of this node (and all descendants) with all positions shifted by `offset`.
|
||||
pub fn translated(self, offset: Point) -> Self {
|
||||
fn translate_rect(r: Rect, o: Point) -> Rect {
|
||||
Rect::new(r.origin.x + o.x, r.origin.y + o.y, r.size.width, r.size.height)
|
||||
Rect::new(
|
||||
r.origin.x + o.x,
|
||||
r.origin.y + o.y,
|
||||
r.size.width,
|
||||
r.size.height,
|
||||
)
|
||||
}
|
||||
let rect = translate_rect(self.rect, offset);
|
||||
let clip_rect = self.clip_rect.map(|r| translate_rect(r, offset));
|
||||
@@ -143,7 +148,11 @@ impl LayoutNode {
|
||||
rect: translate_rect(img.rect, offset),
|
||||
..img
|
||||
});
|
||||
let children = self.children.into_iter().map(|c| c.translated(offset)).collect();
|
||||
let children = self
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|c| c.translated(offset))
|
||||
.collect();
|
||||
LayoutNode {
|
||||
rect,
|
||||
clip_rect,
|
||||
@@ -241,6 +250,7 @@ pub fn layout_snapshot_with_cache(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_element(
|
||||
element: &Element,
|
||||
rect: Rect,
|
||||
@@ -256,24 +266,18 @@ fn layout_element(
|
||||
perf_stats.nodes += 1;
|
||||
|
||||
// Viewport culling: skip fully off-screen elements inside a scroll box.
|
||||
if let Some(clip) = clip_rect {
|
||||
if !rects_overlap(rect, clip) {
|
||||
if let Some(clip) = clip_rect
|
||||
&& !rects_overlap(rect, clip)
|
||||
{
|
||||
perf_stats.viewport_culled += 1;
|
||||
return LayoutNode {
|
||||
path,
|
||||
element_id: element.id,
|
||||
return layout_interaction_skeleton(
|
||||
element,
|
||||
rect,
|
||||
corner_radius: 0.0,
|
||||
clip_rect: None,
|
||||
pointer_events: element.style.pointer_events,
|
||||
focusable: element.style.focusable,
|
||||
cursor: element.style.cursor.unwrap_or(CursorIcon::Default),
|
||||
scroll_metrics: None,
|
||||
prepared_image: None,
|
||||
prepared_text: None,
|
||||
children: Vec::new(),
|
||||
};
|
||||
}
|
||||
path,
|
||||
text_system,
|
||||
perf_stats,
|
||||
layout_cache,
|
||||
);
|
||||
}
|
||||
|
||||
// Incremental layout cache check.
|
||||
@@ -285,6 +289,7 @@ fn layout_element(
|
||||
subtree_hash,
|
||||
avail_width_bits: rect.size.width.round() as u32,
|
||||
avail_height_bits: rect.size.height.round() as u32,
|
||||
clip_rect_bits: cache_clip_rect_bits(rect, clip_rect),
|
||||
};
|
||||
if let Some(cached) = layout_cache.results.get(&cache_key) {
|
||||
let offset = rect.origin;
|
||||
@@ -368,7 +373,13 @@ fn layout_element(
|
||||
if pushed_clip {
|
||||
scene.pop_clip();
|
||||
}
|
||||
cache_layout(layout_cache, cache_key, &interaction, &scene.items[scene_start..], rect.origin);
|
||||
cache_layout(
|
||||
layout_cache,
|
||||
cache_key,
|
||||
&interaction,
|
||||
&scene.items[scene_start..],
|
||||
rect.origin,
|
||||
);
|
||||
return interaction;
|
||||
}
|
||||
|
||||
@@ -382,7 +393,13 @@ fn layout_element(
|
||||
if pushed_clip {
|
||||
scene.pop_clip();
|
||||
}
|
||||
cache_layout(layout_cache, cache_key, &interaction, &scene.items[scene_start..], rect.origin);
|
||||
cache_layout(
|
||||
layout_cache,
|
||||
cache_key,
|
||||
&interaction,
|
||||
&scene.items[scene_start..],
|
||||
rect.origin,
|
||||
);
|
||||
return interaction;
|
||||
}
|
||||
|
||||
@@ -508,7 +525,13 @@ fn layout_element(
|
||||
if pushed_clip {
|
||||
scene.pop_clip();
|
||||
}
|
||||
cache_layout(layout_cache, cache_key, &interaction, &scene.items[scene_start..], rect.origin);
|
||||
cache_layout(
|
||||
layout_cache,
|
||||
cache_key,
|
||||
&interaction,
|
||||
&scene.items[scene_start..],
|
||||
rect.origin,
|
||||
);
|
||||
return interaction;
|
||||
}
|
||||
|
||||
@@ -518,7 +541,13 @@ fn layout_element(
|
||||
if pushed_clip {
|
||||
scene.pop_clip();
|
||||
}
|
||||
cache_layout(layout_cache, cache_key, &interaction, &scene.items[scene_start..], rect.origin);
|
||||
cache_layout(
|
||||
layout_cache,
|
||||
cache_key,
|
||||
&interaction,
|
||||
&scene.items[scene_start..],
|
||||
rect.origin,
|
||||
);
|
||||
return interaction;
|
||||
}
|
||||
|
||||
@@ -527,7 +556,13 @@ fn layout_element(
|
||||
if pushed_clip {
|
||||
scene.pop_clip();
|
||||
}
|
||||
cache_layout(layout_cache, cache_key, &interaction, &scene.items[scene_start..], rect.origin);
|
||||
cache_layout(
|
||||
layout_cache,
|
||||
cache_key,
|
||||
&interaction,
|
||||
&scene.items[scene_start..],
|
||||
rect.origin,
|
||||
);
|
||||
return interaction;
|
||||
}
|
||||
interaction.children = layout_container_children(
|
||||
@@ -545,7 +580,13 @@ fn layout_element(
|
||||
scene.pop_clip();
|
||||
}
|
||||
|
||||
cache_layout(layout_cache, cache_key, &interaction, &scene.items[scene_start..], rect.origin);
|
||||
cache_layout(
|
||||
layout_cache,
|
||||
cache_key,
|
||||
&interaction,
|
||||
&scene.items[scene_start..],
|
||||
rect.origin,
|
||||
);
|
||||
interaction
|
||||
}
|
||||
|
||||
@@ -558,10 +599,13 @@ fn cache_layout(
|
||||
origin: Point,
|
||||
) {
|
||||
let neg = Point::new(-origin.x, -origin.y);
|
||||
cache.results.insert(key, CachedLayout {
|
||||
cache.results.insert(
|
||||
key,
|
||||
CachedLayout {
|
||||
interaction_node: interaction.clone().translated(neg),
|
||||
scene_items: scene_items.iter().map(|i| i.translated(neg)).collect(),
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn hit_path_node(node: &LayoutNode, point: crate::scene::Point) -> Option<Vec<HitTarget>> {
|
||||
@@ -735,6 +779,7 @@ struct MeasuredChild {
|
||||
is_flex: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_container_children(
|
||||
element: &Element,
|
||||
content: Rect,
|
||||
@@ -838,6 +883,210 @@ fn layout_container_children(
|
||||
children
|
||||
}
|
||||
|
||||
fn layout_interaction_skeleton(
|
||||
element: &Element,
|
||||
rect: Rect,
|
||||
path: LayoutPath,
|
||||
text_system: &mut TextSystem,
|
||||
perf_stats: &mut LayoutPerfStats,
|
||||
layout_cache: &mut LayoutCache,
|
||||
) -> LayoutNode {
|
||||
let cursor = element.style.cursor.unwrap_or_else(|| {
|
||||
if element.text_node().is_some() {
|
||||
CursorIcon::Text
|
||||
} else {
|
||||
CursorIcon::Default
|
||||
}
|
||||
});
|
||||
let mut interaction = LayoutNode {
|
||||
path,
|
||||
element_id: element.id,
|
||||
rect,
|
||||
corner_radius: uniform_corner_radius(&element.style, rect),
|
||||
clip_rect: None,
|
||||
pointer_events: element.style.pointer_events,
|
||||
focusable: element.style.focusable,
|
||||
cursor,
|
||||
scroll_metrics: None,
|
||||
prepared_image: None,
|
||||
prepared_text: None,
|
||||
children: Vec::new(),
|
||||
};
|
||||
|
||||
if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
|
||||
return interaction;
|
||||
}
|
||||
|
||||
if let Some(scroll_box) = element.scroll_box_node() {
|
||||
let content = inset_rect(rect, content_insets(&element.style));
|
||||
if content.size.width <= 0.0 || content.size.height <= 0.0 {
|
||||
return interaction;
|
||||
}
|
||||
let gutter_width = scroll_box
|
||||
.scrollbar
|
||||
.gutter_width
|
||||
.max(0.0)
|
||||
.min(content.size.width);
|
||||
let viewport_rect = Rect::new(
|
||||
content.origin.x,
|
||||
content.origin.y,
|
||||
(content.size.width - gutter_width).max(0.0),
|
||||
content.size.height,
|
||||
);
|
||||
interaction.clip_rect = Some(viewport_rect);
|
||||
let content_size = intrinsic_container_content_size(
|
||||
element,
|
||||
UiSize::new(
|
||||
viewport_rect.size.width.max(0.0),
|
||||
viewport_rect.size.height.max(0.0),
|
||||
),
|
||||
text_system,
|
||||
perf_stats,
|
||||
layout_cache,
|
||||
);
|
||||
let content_height = content_size.height.max(viewport_rect.size.height);
|
||||
let offset_y = scroll_box
|
||||
.offset_y
|
||||
.max(0.0)
|
||||
.min((content_height - viewport_rect.size.height).max(0.0));
|
||||
interaction.children = layout_interaction_skeleton_children(
|
||||
element,
|
||||
Rect::new(
|
||||
viewport_rect.origin.x,
|
||||
viewport_rect.origin.y - offset_y,
|
||||
viewport_rect.size.width,
|
||||
content_height,
|
||||
),
|
||||
&interaction.path,
|
||||
text_system,
|
||||
perf_stats,
|
||||
layout_cache,
|
||||
);
|
||||
let (scrollbar_track, scrollbar_thumb) = scrollbar_geometry(
|
||||
content,
|
||||
scroll_box.scrollbar,
|
||||
content_height,
|
||||
viewport_rect.size.height,
|
||||
offset_y,
|
||||
);
|
||||
interaction.scroll_metrics = Some(ScrollMetrics {
|
||||
viewport_rect,
|
||||
content_height,
|
||||
offset_y,
|
||||
max_offset_y: (content_height - viewport_rect.size.height).max(0.0),
|
||||
scrollbar_track,
|
||||
scrollbar_thumb,
|
||||
});
|
||||
return interaction;
|
||||
}
|
||||
|
||||
if element.text_node().is_some()
|
||||
|| element.image_node().is_some()
|
||||
|| element.children.is_empty()
|
||||
{
|
||||
return interaction;
|
||||
}
|
||||
|
||||
let content = inset_rect(rect, content_insets(&element.style));
|
||||
interaction.children = layout_interaction_skeleton_children(
|
||||
element,
|
||||
content,
|
||||
&interaction.path,
|
||||
text_system,
|
||||
perf_stats,
|
||||
layout_cache,
|
||||
);
|
||||
interaction
|
||||
}
|
||||
|
||||
fn layout_interaction_skeleton_children(
|
||||
element: &Element,
|
||||
content: Rect,
|
||||
path: &LayoutPath,
|
||||
text_system: &mut TextSystem,
|
||||
perf_stats: &mut LayoutPerfStats,
|
||||
layout_cache: &mut LayoutCache,
|
||||
) -> Vec<LayoutNode> {
|
||||
if element.children.is_empty() || content.size.width <= 0.0 || content.size.height <= 0.0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let gap_count = element.children.len().saturating_sub(1) as f32;
|
||||
let total_gap = element.style.gap * gap_count;
|
||||
let available_main =
|
||||
(main_axis_size(content.size, element.style.direction).max(0.0) - total_gap).max(0.0);
|
||||
let available_cross = cross_axis_size(content.size, element.style.direction).max(0.0);
|
||||
|
||||
let mut measured_children = Vec::with_capacity(element.children.len());
|
||||
let mut fixed_total = 0.0;
|
||||
let mut flex_total = 0.0;
|
||||
for child in &element.children {
|
||||
let cross = child_cross_size(child, element.style.direction)
|
||||
.unwrap_or(available_cross)
|
||||
.clamp(0.0, available_cross);
|
||||
let explicit_main =
|
||||
child_main_size(child, element.style.direction).map(|main| main.max(0.0));
|
||||
let is_flex = explicit_main.is_none() && child.style.flex_grow > 0.0;
|
||||
let measured_main = explicit_main.unwrap_or_else(|| {
|
||||
if is_flex {
|
||||
0.0
|
||||
} else {
|
||||
intrinsic_main_size(
|
||||
child,
|
||||
element.style.direction,
|
||||
cross,
|
||||
available_main,
|
||||
text_system,
|
||||
perf_stats,
|
||||
layout_cache,
|
||||
)
|
||||
}
|
||||
});
|
||||
if is_flex {
|
||||
flex_total += child_flex_weight(child);
|
||||
} else {
|
||||
fixed_total += measured_main;
|
||||
}
|
||||
measured_children.push(MeasuredChild {
|
||||
cross,
|
||||
main: measured_main,
|
||||
is_flex,
|
||||
});
|
||||
}
|
||||
|
||||
let remaining_main = (available_main - fixed_total).max(0.0);
|
||||
let mut cursor = main_axis_origin(content, element.style.direction);
|
||||
let mut children = Vec::with_capacity(element.children.len());
|
||||
for (index, (child, measured)) in element.children.iter().zip(measured_children).enumerate() {
|
||||
let child_main = if measured.is_flex {
|
||||
if flex_total <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
remaining_main * (child_flex_weight(child) / flex_total)
|
||||
}
|
||||
} else {
|
||||
measured.main
|
||||
};
|
||||
let child_rect = child_rect(
|
||||
content,
|
||||
element.style.direction,
|
||||
cursor,
|
||||
child_main.max(0.0),
|
||||
measured.cross,
|
||||
);
|
||||
children.push(layout_interaction_skeleton(
|
||||
child,
|
||||
child_rect,
|
||||
path.child(index),
|
||||
text_system,
|
||||
perf_stats,
|
||||
layout_cache,
|
||||
));
|
||||
cursor += child_main.max(0.0) + element.style.gap;
|
||||
}
|
||||
children
|
||||
}
|
||||
|
||||
fn intrinsic_main_size(
|
||||
child: &Element,
|
||||
direction: FlexDirection,
|
||||
@@ -986,7 +1235,14 @@ fn intrinsic_size(
|
||||
return cached;
|
||||
}
|
||||
let insets = content_insets(&element.style);
|
||||
let result = intrinsic_size_inner(element, available_size, insets, text_system, perf_stats, layout_cache);
|
||||
let result = intrinsic_size_inner(
|
||||
element,
|
||||
available_size,
|
||||
insets,
|
||||
text_system,
|
||||
perf_stats,
|
||||
layout_cache,
|
||||
);
|
||||
layout_cache.intrinsic_cache.insert(cache_key, result);
|
||||
result
|
||||
}
|
||||
@@ -1047,8 +1303,13 @@ fn intrinsic_size_inner(
|
||||
);
|
||||
}
|
||||
|
||||
let intrinsic_content =
|
||||
intrinsic_container_content_size(element, content_size, text_system, perf_stats, layout_cache);
|
||||
let intrinsic_content = intrinsic_container_content_size(
|
||||
element,
|
||||
content_size,
|
||||
text_system,
|
||||
perf_stats,
|
||||
layout_cache,
|
||||
);
|
||||
|
||||
UiSize::new(
|
||||
explicit_width
|
||||
@@ -1085,6 +1346,7 @@ struct LayoutCacheKey {
|
||||
subtree_hash: u64,
|
||||
avail_width_bits: u32,
|
||||
avail_height_bits: u32,
|
||||
clip_rect_bits: Option<(u32, u32, u32, u32)>,
|
||||
}
|
||||
|
||||
struct CachedLayout {
|
||||
@@ -1146,6 +1408,16 @@ fn rects_overlap(a: Rect, b: Rect) -> bool {
|
||||
&& a.origin.y + a.size.height > b.origin.y
|
||||
}
|
||||
|
||||
fn cache_clip_rect_bits(rect: Rect, clip_rect: Option<Rect>) -> Option<(u32, u32, u32, u32)> {
|
||||
let clip = clip_rect?;
|
||||
Some((
|
||||
(clip.origin.x - rect.origin.x).round().to_bits(),
|
||||
(clip.origin.y - rect.origin.y).round().to_bits(),
|
||||
clip.size.width.round().to_bits(),
|
||||
clip.size.height.round().to_bits(),
|
||||
))
|
||||
}
|
||||
|
||||
fn prepare_image(image: &ImageNode, rect: Rect, element_id: Option<ElementId>) -> PreparedImage {
|
||||
let source_size = image.resource.size();
|
||||
let source_aspect = if source_size.height > 0.0 {
|
||||
@@ -1537,11 +1809,13 @@ fn child_rect(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{LayoutCache, layout_scene, layout_snapshot, layout_snapshot_with_cache};
|
||||
use super::{
|
||||
LayoutCache, LayoutSnapshot, layout_scene, layout_snapshot, layout_snapshot_with_cache,
|
||||
};
|
||||
use crate::scene::{Color, DisplayItem, Point, Quad, Rect, UiSize};
|
||||
use crate::text::TextSystem;
|
||||
use crate::text::{TextStyle, TextWrap};
|
||||
use crate::tree::{Edges, Element, ElementId, FlexDirection};
|
||||
use crate::text::TextSystem;
|
||||
|
||||
#[test]
|
||||
fn row_layout_apportions_fixed_and_flex_children() {
|
||||
@@ -2156,10 +2430,12 @@ mod tests {
|
||||
let size = UiSize::new(400.0, 600.0);
|
||||
let snap1 = layout_snapshot_with_cache(1, size, &root, &mut text_system, &mut layout_cache);
|
||||
let snap2 = layout_snapshot_with_cache(2, size, &root, &mut text_system, &mut layout_cache);
|
||||
assert_eq!(snap1.scene.items, snap2.scene.items, "scene items should be identical");
|
||||
assert_eq!(
|
||||
snap1.interaction_tree.root,
|
||||
snap2.interaction_tree.root,
|
||||
snap1.scene.items, snap2.scene.items,
|
||||
"scene items should be identical"
|
||||
);
|
||||
assert_eq!(
|
||||
snap1.interaction_tree.root, snap2.interaction_tree.root,
|
||||
"interaction trees should be identical"
|
||||
);
|
||||
}
|
||||
@@ -2190,7 +2466,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn scroll_culling_skips_off_screen_children() {
|
||||
use crate::tree::ScrollbarStyle;
|
||||
let mut text_system = TextSystem::new();
|
||||
let mut layout_cache = LayoutCache::new();
|
||||
|
||||
@@ -2198,12 +2473,14 @@ mod tests {
|
||||
let item_height = 40.0;
|
||||
let viewport_height = 200.0;
|
||||
let n = 100;
|
||||
let mut scroll_box = Element::scroll_box(0.0).width(400.0).height(viewport_height);
|
||||
let mut scroll_box = Element::scroll_box(0.0)
|
||||
.width(400.0)
|
||||
.height(viewport_height);
|
||||
for i in 0..n {
|
||||
scroll_box = scroll_box.child(
|
||||
Element::new()
|
||||
.height(item_height)
|
||||
.child(Element::text(format!("item {i}"), text_style()))
|
||||
.child(Element::text(format!("item {i}"), text_style())),
|
||||
);
|
||||
}
|
||||
let root = Element::new().child(scroll_box);
|
||||
@@ -2214,11 +2491,158 @@ mod tests {
|
||||
let snap = layout_snapshot_with_cache(1, size, &root, &mut text_system, &mut layout_cache);
|
||||
|
||||
// Only text items within the 200px viewport should appear in the scene.
|
||||
let text_items = snap.scene.items.iter().filter(|item| {
|
||||
matches!(item, DisplayItem::Text(_))
|
||||
}).count();
|
||||
let text_items = snap
|
||||
.scene
|
||||
.items
|
||||
.iter()
|
||||
.filter(|item| matches!(item, DisplayItem::Text(_)))
|
||||
.count();
|
||||
// At most 6 text items should be visible (5 fit + 1 partial).
|
||||
assert!(text_items <= 6, "expected <= 6 text items in scene, got {text_items} (culling not working)");
|
||||
assert!(text_items >= 4, "expected >= 4 text items visible, got {text_items}");
|
||||
assert!(
|
||||
text_items <= 6,
|
||||
"expected <= 6 text items in scene, got {text_items} (culling not working)"
|
||||
);
|
||||
assert!(
|
||||
text_items >= 4,
|
||||
"expected >= 4 text items visible, got {text_items}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_scroll_container_recomputes_visible_children_after_offset_changes() {
|
||||
fn scroll_root(offset_y: f32) -> Element {
|
||||
let mut list = Element::column().gap(0.0);
|
||||
for i in 0..20 {
|
||||
list = list.child(
|
||||
Element::new()
|
||||
.height(40.0)
|
||||
.child(Element::text(format!("item {i:02}"), text_style())),
|
||||
);
|
||||
}
|
||||
Element::new().child(
|
||||
Element::scroll_box(offset_y)
|
||||
.width(400.0)
|
||||
.height(200.0)
|
||||
.child(list),
|
||||
)
|
||||
}
|
||||
|
||||
fn visible_texts(snapshot: &LayoutSnapshot) -> Vec<&str> {
|
||||
snapshot
|
||||
.scene
|
||||
.items
|
||||
.iter()
|
||||
.filter_map(|item| match item {
|
||||
DisplayItem::Text(text) => Some(text.text.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
let mut text_system = TextSystem::new();
|
||||
let mut layout_cache = LayoutCache::new();
|
||||
let size = UiSize::new(400.0, 200.0);
|
||||
|
||||
let first = layout_snapshot_with_cache(
|
||||
1,
|
||||
size,
|
||||
&scroll_root(0.0),
|
||||
&mut text_system,
|
||||
&mut layout_cache,
|
||||
);
|
||||
let second = layout_snapshot_with_cache(
|
||||
2,
|
||||
size,
|
||||
&scroll_root(240.0),
|
||||
&mut text_system,
|
||||
&mut layout_cache,
|
||||
);
|
||||
|
||||
let first_visible = visible_texts(&first);
|
||||
let second_visible = visible_texts(&second);
|
||||
assert!(first_visible.iter().any(|text| text.contains("item 00")));
|
||||
assert!(
|
||||
second_visible.iter().any(|text| text.contains("item 06")),
|
||||
"expected scrolled snapshot to include newly visible rows, got {second_visible:?}"
|
||||
);
|
||||
assert!(
|
||||
!second_visible.iter().any(|text| text.contains("item 00")),
|
||||
"expected initial rows to be culled after scrolling, got {second_visible:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn culled_container_keeps_descendant_geometry_for_scroll_into_view() {
|
||||
let target_id = ElementId::new(42);
|
||||
let root = Element::new().child(
|
||||
Element::scroll_box(0.0).width(400.0).height(120.0).child(
|
||||
Element::column()
|
||||
.child(
|
||||
Element::column()
|
||||
.height(160.0)
|
||||
.child(Element::text("visible group", text_style())),
|
||||
)
|
||||
.child(
|
||||
Element::column().height(160.0).child(
|
||||
Element::new()
|
||||
.id(target_id)
|
||||
.height(40.0)
|
||||
.child(Element::text("offscreen target", text_style())),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
let snapshot = layout_snapshot(1, UiSize::new(400.0, 120.0), &root);
|
||||
|
||||
assert!(
|
||||
snapshot
|
||||
.scene
|
||||
.items
|
||||
.iter()
|
||||
.all(|item| !matches!(item, DisplayItem::Text(text) if text.text.contains("offscreen target"))),
|
||||
"offscreen target text should still be scene-culled"
|
||||
);
|
||||
assert!(
|
||||
snapshot
|
||||
.interaction_tree
|
||||
.rect_for_element(target_id)
|
||||
.is_some(),
|
||||
"offscreen descendants must keep geometry for widget-ref scrolling"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_culling_keeps_visible_text_selectable_inside_viewport_clip() {
|
||||
let text_id = ElementId::new(77);
|
||||
let root = Element::new().child(
|
||||
Element::scroll_box(120.0).width(360.0).height(120.0).child(
|
||||
Element::paragraph(
|
||||
"line 01\nline 02\nline 03\nline 04\nline 05\nline 06\nline 07\nline 08\nline 09",
|
||||
text_style().with_line_height(20.0),
|
||||
)
|
||||
.id(text_id),
|
||||
),
|
||||
);
|
||||
|
||||
let snapshot = layout_snapshot(1, UiSize::new(360.0, 120.0), &root);
|
||||
let text = snapshot
|
||||
.interaction_tree
|
||||
.text_for_element(text_id)
|
||||
.expect("selectable text should remain in the interaction tree");
|
||||
let visible_hit = snapshot
|
||||
.interaction_tree
|
||||
.text_hit_test(Point::new(8.0, 8.0))
|
||||
.expect("visible scrolled text should be selectable");
|
||||
|
||||
assert_eq!(text.element_id, Some(text_id));
|
||||
assert_eq!(visible_hit.target.element_id, Some(text_id));
|
||||
assert!(
|
||||
snapshot
|
||||
.interaction_tree
|
||||
.text_hit_test(Point::new(8.0, 160.0))
|
||||
.is_none(),
|
||||
"text hit testing should still honor the scroll viewport clip"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
pub(crate) mod trace_targets {
|
||||
pub const PLATFORM: &str = "ruin_ui::platform";
|
||||
pub const SCENE: &str = "ruin_ui::scene";
|
||||
#[allow(dead_code)]
|
||||
pub const TEXT_PERF: &str = "ruin_ui::text_perf";
|
||||
}
|
||||
|
||||
@@ -35,7 +36,7 @@ pub use layout::{
|
||||
pub use layout::{layout_scene, layout_scene_with_text_system};
|
||||
pub use platform::{
|
||||
PlatformClosed, PlatformEndpoint, PlatformEvent, PlatformProxy, PlatformRequest,
|
||||
PlatformRuntime, start_headless,
|
||||
PlatformRuntime, set_command_wake_hook, start_headless,
|
||||
};
|
||||
pub use runtime::{EventStreamClosed, UiRuntime, WindowController};
|
||||
pub use scene::{
|
||||
|
||||
@@ -4,8 +4,8 @@ use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use ruin_runtime::channel::mpsc;
|
||||
use ruin_runtime::{WorkerHandle, queue_future, queue_microtask, spawn_worker};
|
||||
@@ -18,6 +18,13 @@ use crate::trace_targets;
|
||||
use crate::tree::CursorIcon;
|
||||
use crate::window::{WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate};
|
||||
|
||||
type CommandWakeHook = Arc<dyn Fn() + Send + Sync + 'static>;
|
||||
|
||||
fn command_wake_hook() -> &'static Mutex<Option<CommandWakeHook>> {
|
||||
static HOOK: OnceLock<Mutex<Option<CommandWakeHook>>> = OnceLock::new();
|
||||
HOOK.get_or_init(|| Mutex::new(None))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PlatformProxy {
|
||||
command_tx: mpsc::UnboundedSender<PlatformRequest>,
|
||||
@@ -32,7 +39,7 @@ pub struct PlatformProxy {
|
||||
pub struct PlatformRuntime {
|
||||
proxy: PlatformProxy,
|
||||
events: mpsc::Receiver<PlatformEvent>,
|
||||
_worker: WorkerHandle,
|
||||
worker: Option<WorkerHandle>,
|
||||
}
|
||||
|
||||
pub struct PlatformEndpoint {
|
||||
@@ -73,6 +80,7 @@ pub enum PlatformEvent {
|
||||
window_id: WindowId,
|
||||
text: String,
|
||||
},
|
||||
#[cfg(target_os = "linux")]
|
||||
PrimarySelectionText {
|
||||
window_id: WindowId,
|
||||
text: String,
|
||||
@@ -118,10 +126,12 @@ pub enum PlatformRequest {
|
||||
RequestClipboardText {
|
||||
window_id: WindowId,
|
||||
},
|
||||
#[cfg(target_os = "linux")]
|
||||
SetPrimarySelectionText {
|
||||
window_id: WindowId,
|
||||
text: String,
|
||||
},
|
||||
#[cfg(target_os = "linux")]
|
||||
RequestPrimarySelectionText {
|
||||
window_id: WindowId,
|
||||
},
|
||||
@@ -203,7 +213,30 @@ impl PlatformRuntime {
|
||||
Self {
|
||||
proxy,
|
||||
events: event_rx,
|
||||
_worker: worker,
|
||||
worker: Some(worker),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a platform runtime hosted by the current runtime thread.
|
||||
///
|
||||
/// This is useful for backends that must run on a specific host thread
|
||||
/// (for example, AppKit on macOS main thread).
|
||||
pub fn custom_local(start: impl FnOnce(PlatformEndpoint)) -> Self {
|
||||
let (command_tx, command_rx) = mpsc::unbounded_channel::<PlatformRequest>();
|
||||
let (event_tx, event_rx) = mpsc::unbounded_channel::<PlatformEvent>();
|
||||
let proxy = PlatformProxy {
|
||||
command_tx,
|
||||
next_window_id: Arc::new(AtomicU64::new(1)),
|
||||
};
|
||||
start(PlatformEndpoint {
|
||||
commands: command_rx,
|
||||
events: event_tx,
|
||||
});
|
||||
|
||||
Self {
|
||||
proxy,
|
||||
events: event_rx,
|
||||
worker: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +249,7 @@ impl PlatformRuntime {
|
||||
}
|
||||
|
||||
pub fn take_pending_events(&mut self) -> Vec<PlatformEvent> {
|
||||
let _ = self.worker.as_ref();
|
||||
let mut events = Vec::new();
|
||||
while let Ok(event) = self.events.try_recv() {
|
||||
events.push(event);
|
||||
@@ -247,6 +281,7 @@ impl PlatformProxy {
|
||||
self.send(PlatformRequest::ReplaceScene { window_id, scene })
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn set_primary_selection_text(
|
||||
&self,
|
||||
window_id: WindowId,
|
||||
@@ -258,6 +293,7 @@ impl PlatformProxy {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn request_primary_selection_text(
|
||||
&self,
|
||||
window_id: WindowId,
|
||||
@@ -317,7 +353,20 @@ impl PlatformProxy {
|
||||
}
|
||||
|
||||
fn send(&self, command: PlatformRequest) -> Result<(), PlatformClosed> {
|
||||
self.command_tx.send(command).map_err(|_| PlatformClosed)
|
||||
self.command_tx.send(command).map_err(|_| PlatformClosed)?;
|
||||
if let Ok(hook) = command_wake_hook().lock()
|
||||
&& let Some(hook) = hook.as_ref().cloned()
|
||||
{
|
||||
hook();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn set_command_wake_hook(hook: Option<CommandWakeHook>) {
|
||||
if let Ok(mut slot) = command_wake_hook().lock() {
|
||||
*slot = hook;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,7 +405,9 @@ pub fn start_headless() -> PlatformRuntime {
|
||||
}
|
||||
PlatformRequest::SetClipboardText { .. } => {}
|
||||
PlatformRequest::RequestClipboardText { .. } => {}
|
||||
#[cfg(target_os = "linux")]
|
||||
PlatformRequest::SetPrimarySelectionText { .. } => {}
|
||||
#[cfg(target_os = "linux")]
|
||||
PlatformRequest::RequestPrimarySelectionText { .. } => {}
|
||||
PlatformRequest::SetCursorIcon { .. } => {}
|
||||
PlatformRequest::EmitCloseRequested { window_id } => {
|
||||
|
||||
@@ -110,6 +110,7 @@ impl WindowController {
|
||||
}
|
||||
|
||||
/// Copies plain text to the platform primary-selection buffer for this window.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn set_primary_selection_text(
|
||||
&self,
|
||||
text: impl Into<String>,
|
||||
@@ -128,6 +129,7 @@ impl WindowController {
|
||||
}
|
||||
|
||||
/// Requests the current plain-text primary selection contents from the platform.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn request_primary_selection_text(&self) -> Result<(), PlatformClosed> {
|
||||
self.proxy.request_primary_selection_text(self.id)
|
||||
}
|
||||
|
||||
@@ -219,6 +219,7 @@ impl Deref for PreparedText {
|
||||
|
||||
impl PreparedText {
|
||||
/// Construct a `PreparedText` from shaped data. Called by `TextSystem::prepare_spans`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn from_layout(
|
||||
element_id: Option<ElementId>,
|
||||
text: String,
|
||||
@@ -623,10 +624,18 @@ impl DisplayItem {
|
||||
/// Return a copy of this item with all positions shifted by `offset`.
|
||||
pub fn translated(&self, offset: Point) -> Self {
|
||||
fn translate_rect(r: Rect, o: Point) -> Rect {
|
||||
Rect::new(r.origin.x + o.x, r.origin.y + o.y, r.size.width, r.size.height)
|
||||
Rect::new(
|
||||
r.origin.x + o.x,
|
||||
r.origin.y + o.y,
|
||||
r.size.width,
|
||||
r.size.height,
|
||||
)
|
||||
}
|
||||
match self {
|
||||
Self::Quad(q) => Self::Quad(Quad { rect: translate_rect(q.rect, offset), ..*q }),
|
||||
Self::Quad(q) => Self::Quad(Quad {
|
||||
rect: translate_rect(q.rect, offset),
|
||||
..*q
|
||||
}),
|
||||
Self::RoundedRect(r) => Self::RoundedRect(RoundedRect {
|
||||
rect: translate_rect(r.rect, offset),
|
||||
..*r
|
||||
@@ -838,8 +847,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn prepared_text_vertical_offset_moves_between_lines() {
|
||||
use std::sync::Arc;
|
||||
use super::{PreparedTextLine, TextLayoutData};
|
||||
use std::sync::Arc;
|
||||
|
||||
let mut text = PreparedText::monospace(
|
||||
"abcdwxyz",
|
||||
@@ -873,7 +882,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let orig_size = text.layout.size;
|
||||
text.layout = Arc::new(TextLayoutData { lines, glyphs, size: orig_size });
|
||||
text.layout = Arc::new(TextLayoutData {
|
||||
lines,
|
||||
glyphs,
|
||||
size: orig_size,
|
||||
});
|
||||
|
||||
assert_eq!(text.vertical_offset(2, 1), Some(6));
|
||||
assert_eq!(text.vertical_offset(6, -1), Some(2));
|
||||
@@ -892,8 +905,12 @@ mod tests {
|
||||
let target_line = &text.lines[1];
|
||||
// Lines store LOCAL coords; add text.origin to get absolute window coords for the query.
|
||||
let y = text.origin.y + target_line.rect.origin.y + target_line.rect.size.height * 0.5;
|
||||
let start = text.byte_offset_for_position(Point::new(text.origin.x + target_line.rect.origin.x, y));
|
||||
let end = text.byte_offset_for_position(Point::new(text.origin.x + target_line.rect.origin.x + 16.0, y));
|
||||
let start =
|
||||
text.byte_offset_for_position(Point::new(text.origin.x + target_line.rect.origin.x, y));
|
||||
let end = text.byte_offset_for_position(Point::new(
|
||||
text.origin.x + target_line.rect.origin.x + 16.0,
|
||||
y,
|
||||
));
|
||||
let rects = text.selection_rects(start, end);
|
||||
|
||||
assert_eq!(rects.len(), 1);
|
||||
|
||||
@@ -10,8 +10,9 @@ use cosmic_text::{
|
||||
};
|
||||
use fontconfig::Fontconfig;
|
||||
|
||||
use crate::{Color, GlyphInstance, Point, PreparedText, PreparedTextLine, Rect, TextLayoutData,
|
||||
UiSize};
|
||||
use crate::{
|
||||
Color, GlyphInstance, Point, PreparedText, PreparedTextLine, Rect, TextLayoutData, UiSize,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum TextAlign {
|
||||
@@ -443,7 +444,11 @@ impl TextSystem {
|
||||
// For no-wrap + start-aligned text, width doesn't affect shaping.
|
||||
// Pass None so cosmic-text doesn't truncate and the result is
|
||||
// consistent with the width-independent cache key.
|
||||
let effective_width = if width_affects_layout(style) { width } else { None };
|
||||
let effective_width = if width_affects_layout(style) {
|
||||
width
|
||||
} else {
|
||||
None
|
||||
};
|
||||
borrowed.set_size(effective_width, None);
|
||||
let default_attrs = default_attrs_for_style(style, default_family.as_deref());
|
||||
if uses_plain_text_fast_path {
|
||||
@@ -495,9 +500,7 @@ impl TextSystem {
|
||||
// is color-independent. Per-span colors (from glyph.color_opt)
|
||||
// are stored directly since they are part of the shape key via
|
||||
// the spans hash.
|
||||
color: glyph
|
||||
.color_opt
|
||||
.map_or(Color::SENTINEL, color_from_cosmic),
|
||||
color: glyph.color_opt.map_or(Color::SENTINEL, color_from_cosmic),
|
||||
cache_key: Some(physical.cache_key),
|
||||
text_start: line_offset + glyph.start,
|
||||
text_end: line_offset + glyph.end,
|
||||
@@ -930,7 +933,9 @@ impl std::hash::Hash for TextStyle {
|
||||
self.line_height.to_bits().hash(state);
|
||||
self.color.hash(state);
|
||||
self.font_family.hash(state);
|
||||
self.bounds.map(|b| (b.width.to_bits(), b.height.to_bits())).hash(state);
|
||||
self.bounds
|
||||
.map(|b| (b.width.to_bits(), b.height.to_bits()))
|
||||
.hash(state);
|
||||
self.wrap.hash(state);
|
||||
self.align.hash(state);
|
||||
self.max_lines.hash(state);
|
||||
@@ -1038,16 +1043,32 @@ mod tests {
|
||||
let blue = text_system.prepare("hello", origin, &style_blue);
|
||||
// Both PreparedTexts must have identical glyph shapes and sentinel colors —
|
||||
// the second call hits the shape cache and produces the same layout data.
|
||||
assert_eq!(red.glyphs.len(), blue.glyphs.len(), "glyph count must match");
|
||||
assert_eq!(
|
||||
red.glyphs.len(),
|
||||
blue.glyphs.len(),
|
||||
"glyph count must match"
|
||||
);
|
||||
for (r, b) in red.glyphs.iter().zip(blue.glyphs.iter()) {
|
||||
assert_eq!(r.position, b.position, "glyph positions must match");
|
||||
assert_eq!(r.cache_key, b.cache_key, "glyph cache keys must match");
|
||||
assert_eq!(r.color, Color::SENTINEL, "default-color glyphs must use SENTINEL");
|
||||
assert_eq!(b.color, Color::SENTINEL, "default-color glyphs must use SENTINEL");
|
||||
assert_eq!(
|
||||
r.color,
|
||||
Color::SENTINEL,
|
||||
"default-color glyphs must use SENTINEL"
|
||||
);
|
||||
assert_eq!(
|
||||
b.color,
|
||||
Color::SENTINEL,
|
||||
"default-color glyphs must use SENTINEL"
|
||||
);
|
||||
}
|
||||
// PreparedText::clone() is an Arc clone — O(1).
|
||||
let cloned = red.clone();
|
||||
assert_eq!(cloned.layout_ptr(), red.layout_ptr(), "clone must share the same Arc");
|
||||
assert_eq!(
|
||||
cloned.layout_ptr(),
|
||||
red.layout_ptr(),
|
||||
"clone must share the same Arc"
|
||||
);
|
||||
// But they should carry different default colors.
|
||||
assert_eq!(red.default_color, Color::rgb(0xFF, 0x00, 0x00));
|
||||
assert_eq!(blue.default_color, Color::rgb(0x00, 0x00, 0xFF));
|
||||
|
||||
17
lib/ui_platform_macos/Cargo.toml
Normal file
17
lib/ui_platform_macos/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "ruin_ui_platform_macos"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
objc2 = "0.6.4"
|
||||
objc2-core-foundation = "0.3.2"
|
||||
objc2-foundation = "0.3.2"
|
||||
objc2-quartz-core = "0.3.2"
|
||||
raw-window-handle = "0.6"
|
||||
raw-window-metal = "1.1.0"
|
||||
ruin_ui = { path = "../ui" }
|
||||
ruin_ui_renderer_wgpu = { path = "../ui_renderer_wgpu" }
|
||||
ruin_runtime = { package = "ruin-runtime", path = "../runtime" }
|
||||
tracing = "0.1"
|
||||
2083
lib/ui_platform_macos/src/lib.rs
Normal file
2083
lib/ui_platform_macos/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1309,7 +1309,9 @@ fn spawn_window_worker(
|
||||
"worker observed resized frame"
|
||||
);
|
||||
state_ref.pending_viewport = Some(current_viewport);
|
||||
state_ref.pending_viewport_since.get_or_insert_with(Instant::now);
|
||||
state_ref
|
||||
.pending_viewport_since
|
||||
.get_or_insert_with(Instant::now);
|
||||
// Emit configure BEFORE touching the GPU so the app
|
||||
// thread starts layout immediately in parallel.
|
||||
// The actual swapchain recreation is deferred to just
|
||||
@@ -1354,7 +1356,9 @@ fn spawn_window_worker(
|
||||
"scene size does not match current viewport; holding last buffer"
|
||||
);
|
||||
state_ref.pending_viewport = Some(current_viewport);
|
||||
state_ref.pending_viewport_since.get_or_insert_with(Instant::now);
|
||||
state_ref
|
||||
.pending_viewport_since
|
||||
.get_or_insert_with(Instant::now);
|
||||
state_ref.window.request_redraw();
|
||||
} else if !state_ref.window.presentation_ready() {
|
||||
// Correct scene is ready but the compositor
|
||||
@@ -1365,7 +1369,9 @@ fn spawn_window_worker(
|
||||
// before rendering. Deferring from frame.resized means
|
||||
// rapid configures pay one recreation instead of one
|
||||
// per event.
|
||||
if let Some((w, h)) = state_ref.pending_swapchain_size.take() {
|
||||
if let Some((w, h)) =
|
||||
state_ref.pending_swapchain_size.take()
|
||||
{
|
||||
let t_resize = std::time::Instant::now();
|
||||
state_ref.renderer.resize(w, h);
|
||||
let resize_gpu_us = t_resize.elapsed().as_micros();
|
||||
@@ -1382,7 +1388,8 @@ fn spawn_window_worker(
|
||||
let t_render = std::time::Instant::now();
|
||||
match state_ref.renderer.render(scene) {
|
||||
Ok(()) => {
|
||||
let render_gpu_us = t_render.elapsed().as_micros();
|
||||
let render_gpu_us =
|
||||
t_render.elapsed().as_micros();
|
||||
debug!(
|
||||
target: "ruin_ui_platform_wayland::perf",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
@@ -1464,10 +1471,10 @@ fn spawn_window_worker(
|
||||
{
|
||||
let delay =
|
||||
repeat.next_at.saturating_duration_since(Instant::now());
|
||||
state_ref.keyboard_repeat_timer = Some(set_timeout(
|
||||
delay,
|
||||
move || write_wakeup(pipe_write_fd),
|
||||
));
|
||||
state_ref.keyboard_repeat_timer =
|
||||
Some(set_timeout(delay, move || {
|
||||
write_wakeup(pipe_write_fd)
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,8 @@ struct PreparedGlyphBitmap {
|
||||
|
||||
struct RasterizedText {
|
||||
origin_offset: Point,
|
||||
size: UiSize,
|
||||
draw_size: UiSize,
|
||||
texture_size: UiSize,
|
||||
pixels: Vec<u8>,
|
||||
}
|
||||
|
||||
@@ -169,7 +170,8 @@ struct UploadedAtlasText {
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
struct TextTextureKey {
|
||||
text: String,
|
||||
bounds: Option<(u32, u32)>,
|
||||
bounds: Option<(i32, i32)>,
|
||||
clip: Option<(i32, i32, i32, i32)>,
|
||||
font_size_bits: u32,
|
||||
line_height_bits: u32,
|
||||
color: (u8, u8, u8, u8),
|
||||
@@ -330,6 +332,7 @@ pub struct WgpuSceneRenderer {
|
||||
image_cache_order: VecDeque<u64>,
|
||||
#[allow(dead_code)]
|
||||
glyph_atlas: GlyphAtlas,
|
||||
scale_factor: f32,
|
||||
}
|
||||
|
||||
const MAX_TEXT_CACHE_ENTRIES: usize = 64;
|
||||
@@ -366,8 +369,8 @@ impl WgpuSceneRenderer {
|
||||
format,
|
||||
width: width.max(1),
|
||||
height: height.max(1),
|
||||
present_mode: wgpu::PresentMode::AutoVsync,
|
||||
desired_maximum_frame_latency: 2,
|
||||
present_mode: wgpu::PresentMode::AutoNoVsync,
|
||||
desired_maximum_frame_latency: 1,
|
||||
alpha_mode: caps.alpha_modes[0],
|
||||
view_formats: vec![],
|
||||
};
|
||||
@@ -710,6 +713,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
image_cache: HashMap::new(),
|
||||
image_cache_order: VecDeque::new(),
|
||||
glyph_atlas,
|
||||
scale_factor: 1.0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -723,9 +727,33 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
}
|
||||
|
||||
pub fn set_scale_factor(&mut self, scale_factor: f32) {
|
||||
let next = scale_factor.max(1.0);
|
||||
if (self.scale_factor - next).abs() <= f32::EPSILON {
|
||||
return;
|
||||
}
|
||||
self.scale_factor = next;
|
||||
// Text cache is scale-dependent; flush on scale transitions (e.g. display move).
|
||||
self.text_cache.clear();
|
||||
self.text_cache_order.clear();
|
||||
self.glyph_atlas = create_glyph_atlas(
|
||||
&self.device,
|
||||
&self.text_bind_group_layout,
|
||||
&self.text_sampler,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn render(&mut self, scene: &SceneSnapshot) -> Result<(), RenderError> {
|
||||
self.render_with_logical_size(scene, scene.logical_size)
|
||||
}
|
||||
|
||||
pub fn render_with_logical_size(
|
||||
&mut self,
|
||||
scene: &SceneSnapshot,
|
||||
logical_size: UiSize,
|
||||
) -> Result<(), RenderError> {
|
||||
let render_start = std::time::Instant::now();
|
||||
let vertices = build_vertices(scene);
|
||||
let vertices = build_vertices(scene, logical_size);
|
||||
let frame = match self.surface.get_current_texture() {
|
||||
wgpu::CurrentSurfaceTexture::Success(frame)
|
||||
| wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
|
||||
@@ -749,7 +777,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
let mut uploaded_images = Vec::new();
|
||||
let mut uploaded_texts = Vec::new();
|
||||
let mut atlas_perf = AtlasTextPerfStats::default();
|
||||
let uploaded_atlas_text = self.prepare_uploaded_atlas_text(scene, &mut atlas_perf);
|
||||
let uploaded_atlas_text =
|
||||
self.prepare_uploaded_atlas_text(scene, logical_size, &mut atlas_perf);
|
||||
let mut clip_stack = Vec::new();
|
||||
let mut active_clip = ActiveClip::default();
|
||||
for item in &scene.items {
|
||||
@@ -760,17 +789,19 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
DisplayItem::PopClip => pop_clip_state(&mut clip_stack, &mut active_clip),
|
||||
DisplayItem::Image(image) => {
|
||||
if let Some(uploaded) =
|
||||
self.prepare_uploaded_image(image, scene.logical_size, active_clip)
|
||||
self.prepare_uploaded_image(image, logical_size, active_clip)
|
||||
{
|
||||
uploaded_images.push(uploaded);
|
||||
}
|
||||
}
|
||||
DisplayItem::Text(text) => {
|
||||
if text.glyphs.iter().all(|glyph| glyph.cache_key.is_some()) {
|
||||
if (self.scale_factor - 1.0).abs() <= f32::EPSILON
|
||||
&& text.glyphs.iter().all(|glyph| glyph.cache_key.is_some())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if let Some(uploaded) =
|
||||
self.prepare_uploaded_text(text, scene.logical_size, active_clip)
|
||||
self.prepare_uploaded_text(text, logical_size, active_clip)
|
||||
{
|
||||
uploaded_texts.push(uploaded);
|
||||
}
|
||||
@@ -854,21 +885,29 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rasterize_text(&mut self, text: &PreparedText) -> Option<RasterizedText> {
|
||||
if text.glyphs.iter().all(|glyph| glyph.cache_key.is_some()) {
|
||||
return self.rasterize_prepared_text(text);
|
||||
fn rasterize_text(
|
||||
&mut self,
|
||||
text: &PreparedText,
|
||||
raster_clip: Option<Rect>,
|
||||
) -> Option<RasterizedText> {
|
||||
if (self.scale_factor - 1.0).abs() <= f32::EPSILON
|
||||
&& text.glyphs.iter().all(|glyph| glyph.cache_key.is_some())
|
||||
{
|
||||
return self.rasterize_prepared_text(text, raster_clip);
|
||||
}
|
||||
|
||||
let glyphs = self.collect_glyph_bitmaps(text);
|
||||
let clip = match text.bounds {
|
||||
Some(bounds) => PixelRect {
|
||||
let clip = raster_clip.map(|clip| scale_rect_to_pixel_rect(clip, self.scale_factor));
|
||||
let glyphs = self.collect_glyph_bitmaps(text, clip);
|
||||
let clip = clip
|
||||
.or_else(|| {
|
||||
text.bounds.map(|bounds| PixelRect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: bounds.width.ceil() as i32,
|
||||
bottom: bounds.height.ceil() as i32,
|
||||
},
|
||||
None => glyph_union(&glyphs)?,
|
||||
};
|
||||
right: (bounds.width * self.scale_factor).ceil() as i32,
|
||||
bottom: (bounds.height * self.scale_factor).ceil() as i32,
|
||||
})
|
||||
})
|
||||
.or_else(|| glyph_union(&glyphs))?;
|
||||
if clip.width() == 0 || clip.height() == 0 {
|
||||
return None;
|
||||
}
|
||||
@@ -879,23 +918,36 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
}
|
||||
|
||||
Some(RasterizedText {
|
||||
origin_offset: Point::new(clip.left as f32, clip.top as f32),
|
||||
size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
||||
origin_offset: Point::new(
|
||||
clip.left as f32 / self.scale_factor,
|
||||
clip.top as f32 / self.scale_factor,
|
||||
),
|
||||
draw_size: UiSize::new(
|
||||
clip.width() as f32 / self.scale_factor,
|
||||
clip.height() as f32 / self.scale_factor,
|
||||
),
|
||||
texture_size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
||||
pixels,
|
||||
})
|
||||
}
|
||||
|
||||
fn rasterize_prepared_text(&mut self, text: &PreparedText) -> Option<RasterizedText> {
|
||||
fn rasterize_prepared_text(
|
||||
&mut self,
|
||||
text: &PreparedText,
|
||||
raster_clip: Option<Rect>,
|
||||
) -> Option<RasterizedText> {
|
||||
let glyphs = self.collect_prepared_glyphs(text);
|
||||
let clip = match text.bounds {
|
||||
Some(bounds) => PixelRect {
|
||||
let clip = raster_clip
|
||||
.map(rect_to_pixel_rect)
|
||||
.or_else(|| {
|
||||
text.bounds.map(|bounds| PixelRect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: bounds.width.ceil() as i32,
|
||||
bottom: bounds.height.ceil() as i32,
|
||||
},
|
||||
None => glyph_union_prepared(&glyphs)?,
|
||||
};
|
||||
})
|
||||
})
|
||||
.or_else(|| glyph_union_prepared(&glyphs))?;
|
||||
if clip.width() == 0 || clip.height() == 0 {
|
||||
return None;
|
||||
}
|
||||
@@ -914,7 +966,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
|
||||
Some(RasterizedText {
|
||||
origin_offset: Point::new(clip.left as f32, clip.top as f32),
|
||||
size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
||||
draw_size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
||||
texture_size: UiSize::new(clip.width() as f32, clip.height() as f32),
|
||||
pixels,
|
||||
})
|
||||
}
|
||||
@@ -955,13 +1008,20 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
glyphs
|
||||
}
|
||||
|
||||
fn collect_glyph_bitmaps(&mut self, text: &PreparedText) -> Vec<GlyphBitmap> {
|
||||
let mut buffer = Buffer::new_empty(Metrics::new(text.font_size, text.line_height));
|
||||
fn collect_glyph_bitmaps(
|
||||
&mut self,
|
||||
text: &PreparedText,
|
||||
raster_clip: Option<PixelRect>,
|
||||
) -> Vec<GlyphBitmap> {
|
||||
let mut buffer = Buffer::new_empty(Metrics::new(
|
||||
text.font_size * self.scale_factor,
|
||||
text.line_height * self.scale_factor,
|
||||
));
|
||||
{
|
||||
let mut borrowed = buffer.borrow_with(&mut self.font_system);
|
||||
borrowed.set_size(
|
||||
text.bounds.map(|bounds| bounds.width),
|
||||
text.bounds.map(|bounds| bounds.height),
|
||||
text.bounds.map(|bounds| bounds.width * self.scale_factor),
|
||||
text.bounds.map(|bounds| bounds.height * self.scale_factor),
|
||||
);
|
||||
borrowed.set_text(&text.text, &Attrs::new(), Shaping::Advanced, None);
|
||||
}
|
||||
@@ -982,15 +1042,21 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
if width == 0 || height == 0 {
|
||||
continue;
|
||||
}
|
||||
glyphs.push(GlyphBitmap {
|
||||
rect: PixelRect {
|
||||
let rect = PixelRect {
|
||||
left: physical.x + image.placement.left,
|
||||
top: physical.y - image.placement.top,
|
||||
right: physical.x + image.placement.left + width,
|
||||
bottom: physical.y - image.placement.top + height,
|
||||
},
|
||||
};
|
||||
if raster_clip.is_some_and(|clip| !pixel_rects_intersect(rect, clip)) {
|
||||
continue;
|
||||
}
|
||||
glyphs.push(GlyphBitmap {
|
||||
rect,
|
||||
content: image.content,
|
||||
color: glyph.color_opt.map_or(text.default_color, color_from_cosmic),
|
||||
color: glyph
|
||||
.color_opt
|
||||
.map_or(text.default_color, color_from_cosmic),
|
||||
data: image.data.clone(),
|
||||
});
|
||||
}
|
||||
@@ -1001,8 +1067,12 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
fn prepare_uploaded_atlas_text(
|
||||
&mut self,
|
||||
scene: &SceneSnapshot,
|
||||
logical_size: UiSize,
|
||||
perf: &mut AtlasTextPerfStats,
|
||||
) -> Option<UploadedAtlasText> {
|
||||
if (self.scale_factor - 1.0).abs() > f32::EPSILON {
|
||||
return None;
|
||||
}
|
||||
let mut vertices = Vec::new();
|
||||
let mut clip_stack = Vec::new();
|
||||
let mut active_clip = ActiveClip::default();
|
||||
@@ -1071,7 +1141,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
glyph_rect,
|
||||
atlas_glyph.atlas_rect,
|
||||
clip_rect,
|
||||
scene.logical_size,
|
||||
logical_size,
|
||||
resolved_color,
|
||||
active_clip,
|
||||
);
|
||||
@@ -1244,9 +1314,10 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
logical_size: UiSize,
|
||||
clip: ActiveClip,
|
||||
) -> Option<UploadedText> {
|
||||
let key = text_texture_key(text);
|
||||
let raster_clip = text_raster_clip(text, clip, logical_size)?;
|
||||
let key = text_texture_key(text, raster_clip, self.scale_factor);
|
||||
if !self.text_cache.contains_key(&key) {
|
||||
let rasterized = self.rasterize_text(text)?;
|
||||
let rasterized = self.rasterize_text(text, raster_clip)?;
|
||||
let cached = self.create_cached_text_texture(&rasterized);
|
||||
self.text_cache.insert(key.clone(), cached);
|
||||
}
|
||||
@@ -1314,8 +1385,8 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("ruin-ui-renderer-wgpu-texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: text.size.width as u32,
|
||||
height: text.size.height as u32,
|
||||
width: text.texture_size.width as u32,
|
||||
height: text.texture_size.height as u32,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
@@ -1335,12 +1406,12 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
&text.pixels,
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(text.size.width as u32 * 4),
|
||||
rows_per_image: Some(text.size.height as u32),
|
||||
bytes_per_row: Some(text.texture_size.width as u32 * 4),
|
||||
rows_per_image: Some(text.texture_size.height as u32),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: text.size.width as u32,
|
||||
height: text.size.height as u32,
|
||||
width: text.texture_size.width as u32,
|
||||
height: text.texture_size.height as u32,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
@@ -1363,7 +1434,7 @@ fn fs_main(input: VertexOut) -> @location(0) vec4<f32> {
|
||||
_texture: texture,
|
||||
bind_group,
|
||||
origin_offset: text.origin_offset,
|
||||
size: text.size,
|
||||
size: text.draw_size,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1470,7 +1541,11 @@ fn create_glyph_atlas(
|
||||
/// Resolve `Color::SENTINEL` to `default_color`; return other colors unchanged.
|
||||
#[inline]
|
||||
fn resolve_glyph_color(color: Color, default_color: Color) -> Color {
|
||||
if color == Color::SENTINEL { default_color } else { color }
|
||||
if color == Color::SENTINEL {
|
||||
default_color
|
||||
} else {
|
||||
color
|
||||
}
|
||||
}
|
||||
|
||||
fn color_from_cosmic(color: cosmic_text::Color) -> Color {
|
||||
@@ -1879,9 +1954,9 @@ fn color_to_f32(color: Color) -> [f32; 4] {
|
||||
]
|
||||
}
|
||||
|
||||
fn build_vertices(scene: &SceneSnapshot) -> Vec<Vertex> {
|
||||
let width = scene.logical_size.width.max(1.0);
|
||||
let height = scene.logical_size.height.max(1.0);
|
||||
fn build_vertices(scene: &SceneSnapshot, logical_size: UiSize) -> Vec<Vertex> {
|
||||
let width = logical_size.width.max(1.0);
|
||||
let height = logical_size.height.max(1.0);
|
||||
let mut vertices = Vec::new();
|
||||
let mut clip_stack = Vec::new();
|
||||
let mut active_clip = ActiveClip::default();
|
||||
@@ -2143,6 +2218,22 @@ fn rect_to_pixel_rect(rect: Rect) -> PixelRect {
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_rect_to_pixel_rect(rect: Rect, scale: f32) -> PixelRect {
|
||||
PixelRect {
|
||||
left: (rect.origin.x * scale).floor() as i32,
|
||||
top: (rect.origin.y * scale).floor() as i32,
|
||||
right: ((rect.origin.x + rect.size.width) * scale).ceil() as i32,
|
||||
bottom: ((rect.origin.y + rect.size.height) * scale).ceil() as i32,
|
||||
}
|
||||
}
|
||||
|
||||
fn pixel_rects_intersect(first: PixelRect, second: PixelRect) -> bool {
|
||||
first.left < second.right
|
||||
&& first.right > second.left
|
||||
&& first.top < second.bottom
|
||||
&& first.bottom > second.top
|
||||
}
|
||||
|
||||
fn shadow_blur_extent(blur: f32) -> f32 {
|
||||
blur.max(0.0) * 2.0
|
||||
}
|
||||
@@ -2202,6 +2293,33 @@ fn intersect_rects(first: Option<Rect>, second: Option<Rect>) -> Option<Rect> {
|
||||
}
|
||||
}
|
||||
|
||||
fn text_raster_clip(
|
||||
text: &PreparedText,
|
||||
clip: ActiveClip,
|
||||
logical_size: UiSize,
|
||||
) -> Option<Option<Rect>> {
|
||||
let mut absolute_clip = Some(Rect::new(0.0, 0.0, logical_size.width, logical_size.height));
|
||||
if clip.rect_active {
|
||||
absolute_clip = intersect_rects(absolute_clip, Some(clip.resolved_rect()?));
|
||||
}
|
||||
if let Some(bounds) = text.bounds {
|
||||
let text_bounds = Rect::new(text.origin.x, text.origin.y, bounds.width, bounds.height);
|
||||
absolute_clip = intersect_rects(absolute_clip, Some(text_bounds));
|
||||
}
|
||||
if absolute_clip.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(absolute_clip.map(|rect| {
|
||||
Rect::new(
|
||||
rect.origin.x - text.origin.x,
|
||||
rect.origin.y - text.origin.y,
|
||||
rect.size.width,
|
||||
rect.size.height,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
fn empty_clip_rect() -> Rect {
|
||||
Rect::new(1.0, 1.0, -1.0, -1.0)
|
||||
}
|
||||
@@ -2297,7 +2415,7 @@ fn clip_textured_rect(
|
||||
/// return only those glyph indices. This turns O(all_glyphs) iteration into
|
||||
/// O(log(lines) + visible_glyphs) — critical for large text nodes in scroll
|
||||
/// boxes where only a few lines are on screen at once.
|
||||
fn clip_visible_glyphs<'a>(text: &'a PreparedText, clip: Option<Rect>) -> &'a [GlyphInstance] {
|
||||
fn clip_visible_glyphs(text: &PreparedText, clip: Option<Rect>) -> &[GlyphInstance] {
|
||||
let Some(clip) = clip else {
|
||||
return &text.glyphs;
|
||||
};
|
||||
@@ -2324,15 +2442,29 @@ fn clip_visible_glyphs<'a>(text: &'a PreparedText, clip: Option<Rect>) -> &'a [G
|
||||
&text.glyphs[glyph_start..glyph_end.min(text.glyphs.len())]
|
||||
}
|
||||
|
||||
fn text_texture_key(text: &PreparedText) -> TextTextureKey {
|
||||
fn text_texture_key(
|
||||
text: &PreparedText,
|
||||
raster_clip: Option<Rect>,
|
||||
scale_factor: f32,
|
||||
) -> TextTextureKey {
|
||||
TextTextureKey {
|
||||
text: text.text.clone(),
|
||||
bounds: text
|
||||
.bounds
|
||||
.map(|bounds| (bounds.width.to_bits(), bounds.height.to_bits())),
|
||||
bounds: text.bounds.map(|bounds| {
|
||||
(
|
||||
(bounds.width * scale_factor).ceil() as i32,
|
||||
(bounds.height * scale_factor).ceil() as i32,
|
||||
)
|
||||
}),
|
||||
clip: raster_clip
|
||||
.map(|clip| pixel_rect_tuple(scale_rect_to_pixel_rect(clip, scale_factor))),
|
||||
font_size_bits: text.font_size.to_bits(),
|
||||
line_height_bits: text.line_height.to_bits(),
|
||||
color: (text.default_color.r, text.default_color.g, text.default_color.b, text.default_color.a),
|
||||
color: (
|
||||
text.default_color.r,
|
||||
text.default_color.g,
|
||||
text.default_color.b,
|
||||
text.default_color.a,
|
||||
),
|
||||
glyphs: text
|
||||
.glyphs
|
||||
.iter()
|
||||
@@ -2348,16 +2480,17 @@ fn text_texture_key(text: &PreparedText) -> TextTextureKey {
|
||||
}
|
||||
}
|
||||
|
||||
fn pixel_rect_tuple(rect: PixelRect) -> (i32, i32, i32, i32) {
|
||||
(rect.left, rect.top, rect.right, rect.bottom)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
ActiveClip, blend_rgba, build_text_vertices, build_vertices, clip_rect_array,
|
||||
push_clip_state, rounded_clip_arrays, text_texture_key,
|
||||
};
|
||||
use ruin_ui::{
|
||||
ClipRegion, Color, GlyphInstance, Point, PreparedText, Rect, SceneSnapshot,
|
||||
TextSelectionStyle, UiSize,
|
||||
};
|
||||
use ruin_ui::{ClipRegion, Color, Point, PreparedText, Rect, SceneSnapshot, UiSize};
|
||||
|
||||
#[test]
|
||||
fn quad_scenes_expand_to_six_vertices_per_quad() {
|
||||
@@ -2378,7 +2511,7 @@ mod tests {
|
||||
Color::rgb(0xFF, 0xFF, 0xFF),
|
||||
));
|
||||
|
||||
let vertices = build_vertices(&scene);
|
||||
let vertices = build_vertices(&scene, scene.logical_size);
|
||||
assert_eq!(vertices.len(), 12);
|
||||
}
|
||||
|
||||
@@ -2410,7 +2543,29 @@ mod tests {
|
||||
Color::rgb(0xEE, 0xEE, 0xEE),
|
||||
);
|
||||
|
||||
assert_eq!(text_texture_key(&first), text_texture_key(&second));
|
||||
assert_eq!(
|
||||
text_texture_key(&first, None, 1.0),
|
||||
text_texture_key(&second, None, 1.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_texture_key_normalizes_clip_to_device_pixels() {
|
||||
let text = PreparedText::monospace(
|
||||
"scrolling text",
|
||||
Point::new(20.0, 30.0),
|
||||
16.0,
|
||||
8.0,
|
||||
Color::rgb(0xEE, 0xEE, 0xEE),
|
||||
);
|
||||
|
||||
let first = text_texture_key(&text, Some(Rect::new(0.001, 4.001, 80.001, 20.001)), 2.0);
|
||||
let second = text_texture_key(&text, Some(Rect::new(0.002, 4.002, 80.002, 20.002)), 2.0);
|
||||
let different_pixel =
|
||||
text_texture_key(&text, Some(Rect::new(0.6, 4.001, 80.001, 20.001)), 2.0);
|
||||
|
||||
assert_eq!(first, second);
|
||||
assert_ne!(first, different_pixel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user