Lots of claude-driven performance work.
This commit is contained in:
@@ -7,7 +7,7 @@ use std::io::ErrorKind;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::num::NonZeroU32;
|
||||
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd};
|
||||
use std::os::fd::{AsFd, AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||
use std::ptr::NonNull;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -17,7 +17,9 @@ use raw_window_handle::{
|
||||
RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, WindowHandle,
|
||||
};
|
||||
use ruin_runtime::channel::mpsc;
|
||||
use ruin_runtime::{WorkerHandle, queue_future, queue_task, spawn_worker};
|
||||
use ruin_runtime::{
|
||||
TimeoutHandle, WorkerHandle, clear_timeout, queue_future, set_timeout, spawn_worker,
|
||||
};
|
||||
use ruin_ui::{
|
||||
CursorIcon, KeyboardEvent, KeyboardEventKind, KeyboardKey, KeyboardModifiers, PlatformEndpoint,
|
||||
PlatformEvent, PlatformRequest, PlatformRuntime, Point, PointerButton, PointerEvent,
|
||||
@@ -38,6 +40,7 @@ use wayland_client::{
|
||||
use wayland_protocols::wp::cursor_shape::v1::client::{
|
||||
wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
|
||||
};
|
||||
use wayland_protocols::wp::viewporter::client::{wp_viewport, wp_viewporter};
|
||||
use wayland_protocols::wp::primary_selection::zv1::client::{
|
||||
zwp_primary_selection_device_manager_v1, zwp_primary_selection_device_v1,
|
||||
zwp_primary_selection_offer_v1, zwp_primary_selection_source_v1,
|
||||
@@ -114,6 +117,16 @@ struct WindowWorkerState {
|
||||
viewport_request_in_flight: Option<UiSize>,
|
||||
event_tx: mpsc::UnboundedSender<PlatformEvent>,
|
||||
internal_tx: mpsc::UnboundedSender<InternalBackendEvent>,
|
||||
/// Pending keyboard-repeat timeout scheduled via [`ruin_runtime::set_timeout`].
|
||||
keyboard_repeat_timer: Option<TimeoutHandle>,
|
||||
/// When the current pending viewport was first requested; used to measure end-to-end
|
||||
/// resize latency in the "resize complete" log event.
|
||||
pending_viewport_since: Option<Instant>,
|
||||
/// Swapchain size that should be applied just before the next `renderer.render()`.
|
||||
/// Set on every `frame.resized` event; cleared once applied. Deferring the resize
|
||||
/// to render-time means rapid configures (A→B→C) cause only one swapchain recreation
|
||||
/// instead of one per configure.
|
||||
pending_swapchain_size: Option<(u32, u32)>,
|
||||
}
|
||||
|
||||
enum WindowWorkerCommand {
|
||||
@@ -144,6 +157,9 @@ struct State {
|
||||
clipboard_device: Option<wl_data_device::WlDataDevice>,
|
||||
cursor_shape_manager: Option<wp_cursor_shape_manager_v1::WpCursorShapeManagerV1>,
|
||||
cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
|
||||
/// Surface viewport for compositor-side scaling during resize. When set, the compositor
|
||||
/// scales the last-committed buffer to the destination size without a GPU re-render.
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
primary_selection_manager:
|
||||
Option<zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1>,
|
||||
primary_selection_device: Option<zwp_primary_selection_device_v1::ZwpPrimarySelectionDeviceV1>,
|
||||
@@ -300,6 +316,7 @@ impl WaylandWindow {
|
||||
},
|
||||
);
|
||||
let cursor_shape_manager = globals.bind(&qh, 1..=2, ()).ok();
|
||||
let viewporter: Option<wp_viewporter::WpViewporter> = globals.bind(&qh, 1..=1, ()).ok();
|
||||
let primary_selection_manager = globals.bind(&qh, 1..=1, ()).ok();
|
||||
let primary_selection_device = primary_selection_manager.as_ref().map(
|
||||
|manager: &zwp_primary_selection_device_manager_v1::ZwpPrimarySelectionDeviceManagerV1| {
|
||||
@@ -307,6 +324,7 @@ impl WaylandWindow {
|
||||
},
|
||||
);
|
||||
let surface = compositor.create_surface(&qh, ());
|
||||
let viewport = viewporter.as_ref().map(|vp| vp.get_viewport(&surface, &qh, ()));
|
||||
let xdg_surface = wm_base.get_xdg_surface(&surface, &qh, ());
|
||||
let toplevel = xdg_surface.get_toplevel(&qh, ());
|
||||
toplevel.set_title(spec.title.clone());
|
||||
@@ -351,6 +369,7 @@ impl WaylandWindow {
|
||||
clipboard_device,
|
||||
cursor_shape_manager,
|
||||
cursor_shape_device: None,
|
||||
viewport,
|
||||
primary_selection_manager,
|
||||
primary_selection_device,
|
||||
qh,
|
||||
@@ -666,6 +685,29 @@ impl WaylandWindow {
|
||||
self.state.frame_callback = None;
|
||||
}
|
||||
|
||||
/// Set the compositor-side destination size for the surface viewport.
|
||||
/// The compositor will scale the last-committed buffer to this size without a GPU re-render.
|
||||
/// Call `clear_viewport_destination` before the next GPU render so the buffer size is used.
|
||||
/// Returns `true` if the viewport was set (compositor supports wp_viewporter), `false` if
|
||||
/// the caller must fall back to a GPU render for the preview.
|
||||
fn set_viewport_destination(&mut self, width: u32, height: u32) -> bool {
|
||||
if let Some(vp) = self.state.viewport.as_ref() {
|
||||
vp.set_destination(width as i32, height as i32);
|
||||
self.state._surface.commit();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the compositor-side destination override. Must be called before the next GPU
|
||||
/// render so the compositor uses the buffer's natural dimensions.
|
||||
fn clear_viewport_destination(&mut self) {
|
||||
if let Some(vp) = self.state.viewport.as_ref() {
|
||||
vp.set_destination(-1, -1);
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_connection(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
self.state._connection.flush()?;
|
||||
Ok(())
|
||||
@@ -910,18 +952,17 @@ fn handle_replace_scene(
|
||||
height = scene.logical_size.height,
|
||||
"received scene replacement"
|
||||
);
|
||||
let mut command_tx = None;
|
||||
{
|
||||
let command_tx = {
|
||||
let mut state_ref = state.borrow_mut();
|
||||
let Some(record) = state_ref.windows.get_mut(&window_id) else {
|
||||
return;
|
||||
};
|
||||
record.latest_scene = Some(scene.clone());
|
||||
if let Some(worker) = record.worker.as_ref() {
|
||||
command_tx = Some(worker.command_tx.clone());
|
||||
}
|
||||
}
|
||||
record.worker.as_ref().map(|w| w.command_tx.clone())
|
||||
};
|
||||
if let Some(command_tx) = command_tx {
|
||||
// send() uses MSG_RING to wake the command loop on the worker thread; the command
|
||||
// loop then writes to the wakeup pipe to notify the pump.
|
||||
let _ = command_tx.send(WindowWorkerCommand::ReplaceScene(scene));
|
||||
}
|
||||
}
|
||||
@@ -1020,6 +1061,18 @@ fn shutdown_wayland_backend(state: &Rc<RefCell<WaylandBackendState>>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes one byte to the pump wakeup pipe (non-blocking, ignores errors).
|
||||
fn write_wakeup(fd: RawFd) {
|
||||
let b: u8 = 1;
|
||||
let _ = unsafe { libc::write(fd, &b as *const u8 as *const libc::c_void, 1) };
|
||||
}
|
||||
|
||||
/// Drains all pending bytes from the pump wakeup pipe (non-blocking).
|
||||
fn drain_wakeup(fd: RawFd) {
|
||||
let mut buf = [0u8; 64];
|
||||
let _ = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, 64) };
|
||||
}
|
||||
|
||||
fn spawn_window_worker(
|
||||
window_id: WindowId,
|
||||
spec: WindowSpec,
|
||||
@@ -1028,6 +1081,18 @@ fn spawn_window_worker(
|
||||
internal_tx: mpsc::UnboundedSender<InternalBackendEvent>,
|
||||
) -> WindowWorkerHandle {
|
||||
let (command_tx, mut command_rx) = mpsc::unbounded_channel::<WindowWorkerCommand>();
|
||||
|
||||
// Create a wakeup pipe (non-blocking, close-on-exec) used to drive the async pump.
|
||||
// Three sources write to the write end:
|
||||
// 1. The Wayland-fd watcher task — when the compositor buffers new events.
|
||||
// 2. The command loop — after processing each window command.
|
||||
// 3. Keyboard-repeat timer callbacks scheduled via set_timeout.
|
||||
let mut pipe_fds = [0i32; 2];
|
||||
let r = unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_NONBLOCK | libc::O_CLOEXEC) };
|
||||
assert_eq!(r, 0, "pipe2 failed");
|
||||
let pipe_read_fd = pipe_fds[0];
|
||||
let pipe_write_fd = pipe_fds[1];
|
||||
|
||||
let worker = spawn_worker(
|
||||
move || {
|
||||
let window = match WaylandWindow::open(spec.clone()) {
|
||||
@@ -1038,6 +1103,8 @@ fn spawn_window_worker(
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Extract the Wayland socket fd before moving `window` into state.
|
||||
let wayland_fd = window.poll_fd();
|
||||
let renderer = match WgpuSceneRenderer::new(
|
||||
window.surface_target(),
|
||||
spec.requested_inner_size
|
||||
@@ -1068,8 +1135,26 @@ fn spawn_window_worker(
|
||||
viewport_request_in_flight: None,
|
||||
event_tx,
|
||||
internal_tx,
|
||||
keyboard_repeat_timer: None,
|
||||
pending_viewport_since: None,
|
||||
}));
|
||||
|
||||
// Task 1: Wayland-fd watcher. Waits for the compositor to buffer events (frame
|
||||
// callbacks, input, configure, etc.) then pokes the pump via the wakeup pipe.
|
||||
// When the socket errors or hangs up the watcher exits silently; the pump will
|
||||
// detect the closure on its next iteration.
|
||||
queue_future(async move {
|
||||
loop {
|
||||
if ruin_runtime::fd::wait_readable(wayland_fd).await.is_err() {
|
||||
break;
|
||||
}
|
||||
write_wakeup(pipe_write_fd);
|
||||
}
|
||||
});
|
||||
|
||||
// Task 2: command loop. Receives window commands from the platform thread via
|
||||
// the mpsc channel (which uses MSG_RING for cross-thread delivery). After each
|
||||
// command, pokes the pump so it can act on the updated state immediately.
|
||||
queue_future({
|
||||
let state = Rc::clone(&state);
|
||||
async move {
|
||||
@@ -1175,16 +1260,313 @@ fn spawn_window_worker(
|
||||
}
|
||||
WindowWorkerCommand::Shutdown => {
|
||||
state.borrow_mut().shutdown_requested = true;
|
||||
// Poke the pump so it sees shutdown_requested.
|
||||
write_wakeup(pipe_write_fd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Poke the pump so it acts on the updated state without waiting for the
|
||||
// next Wayland event.
|
||||
write_wakeup(pipe_write_fd);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
queue_task({
|
||||
// Task 3: async pump. Sleeps on the wakeup pipe read end; woken by Task 1
|
||||
// (Wayland events), Task 2 (commands), or keyboard-repeat timers. On each wake,
|
||||
// dispatches Wayland events non-blockingly and renders if ready.
|
||||
queue_future({
|
||||
let state = Rc::clone(&state);
|
||||
move || pump_window_worker(state)
|
||||
async move {
|
||||
loop {
|
||||
// Consume all pending wakeup bytes before doing work.
|
||||
drain_wakeup(pipe_read_fd);
|
||||
|
||||
let mut keep_running = true;
|
||||
{
|
||||
let mut state_ref = state.borrow_mut();
|
||||
|
||||
// Non-blocking: read the Wayland socket and dispatch buffered events.
|
||||
match state_ref.window.dispatch_ready() {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
emit_window_closed(&mut state_ref, false);
|
||||
keep_running = false;
|
||||
}
|
||||
}
|
||||
|
||||
if keep_running {
|
||||
for event in state_ref.window.drain_pointer_events() {
|
||||
let _ = state_ref.event_tx.send(PlatformEvent::Pointer {
|
||||
window_id: state_ref.window_id,
|
||||
event,
|
||||
});
|
||||
}
|
||||
for event in state_ref.window.drain_keyboard_events() {
|
||||
tracing::trace!(
|
||||
target: "ruin_ui_platform_wayland::event_bridge",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
keycode = event.keycode,
|
||||
?event.kind,
|
||||
?event.key,
|
||||
text = event.text.as_deref().unwrap_or(""),
|
||||
"forwarding keyboard event to UI runtime"
|
||||
);
|
||||
let _ = state_ref.event_tx.send(PlatformEvent::Keyboard {
|
||||
window_id: state_ref.window_id,
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
let user_closed = !state_ref.shutdown_requested;
|
||||
if state_ref.shutdown_requested || !state_ref.window.is_running() {
|
||||
emit_window_closed(&mut state_ref, user_closed);
|
||||
keep_running = false;
|
||||
}
|
||||
}
|
||||
|
||||
if keep_running {
|
||||
if let Some(frame) = state_ref.window.prepare_frame() {
|
||||
let current_viewport =
|
||||
UiSize::new(frame.width as f32, frame.height as f32);
|
||||
if frame.resized {
|
||||
debug!(
|
||||
target: "ruin_ui_platform_wayland::resize",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
width = current_viewport.width,
|
||||
height = current_viewport.height,
|
||||
"worker observed resized frame"
|
||||
);
|
||||
state_ref.pending_viewport = Some(current_viewport);
|
||||
state_ref.pending_viewport_since.get_or_insert_with(Instant::now);
|
||||
// Emit the Configured event BEFORE resizing the GPU
|
||||
// swapchain so the app thread starts layout immediately.
|
||||
// renderer.resize() then runs concurrently with layout
|
||||
// (different CPU threads), cutting the critical path from
|
||||
// resize_gpu + layout to max(resize_gpu, layout).
|
||||
if state_ref
|
||||
.latest_scene
|
||||
.as_ref()
|
||||
.is_none_or(|s| s.logical_size != current_viewport)
|
||||
|| state_ref.viewport_request_in_flight.is_some()
|
||||
{
|
||||
maybe_request_pending_viewport(&mut state_ref);
|
||||
} else {
|
||||
state_ref.pending_viewport = None;
|
||||
}
|
||||
let t_resize = std::time::Instant::now();
|
||||
state_ref.renderer.resize(frame.width, frame.height);
|
||||
let resize_gpu_us = t_resize.elapsed().as_micros();
|
||||
debug!(
|
||||
target: "ruin_ui_platform_wayland::perf",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
width = frame.width,
|
||||
height = frame.height,
|
||||
resize_gpu_us,
|
||||
"renderer swapchain resized"
|
||||
);
|
||||
state_ref.window.request_redraw();
|
||||
}
|
||||
if !state_ref.opened_emitted {
|
||||
state_ref.opened_emitted = true;
|
||||
let _ = state_ref.event_tx.send(PlatformEvent::Opened {
|
||||
window_id: state_ref.window_id,
|
||||
});
|
||||
}
|
||||
let scene = state_ref.latest_scene.clone();
|
||||
if let Some(scene) = scene.as_ref() {
|
||||
if scene.logical_size != current_viewport {
|
||||
// Resize case: render the preview immediately.
|
||||
// Drop any pending frame callback — vsync pacing
|
||||
// doesn't matter for a placeholder preview, and
|
||||
// waiting for it (potentially 16–150ms) is the
|
||||
// primary source of visible resize lag.
|
||||
state_ref.window.clear_frame_callback();
|
||||
debug!(
|
||||
target: "ruin_ui_platform_wayland::resize",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
scene_version = scene.version,
|
||||
scene_width = scene.logical_size.width,
|
||||
scene_height = scene.logical_size.height,
|
||||
viewport_width = current_viewport.width,
|
||||
viewport_height = current_viewport.height,
|
||||
"scene size does not match current viewport"
|
||||
);
|
||||
state_ref.pending_viewport = Some(current_viewport);
|
||||
state_ref.pending_viewport_since.get_or_insert_with(Instant::now);
|
||||
// Use wp_viewport to scale the last-committed buffer
|
||||
// to the new size without any GPU work. This lets
|
||||
// the compositor composite immediately after
|
||||
// ack_configure, before a new GPU frame is ready.
|
||||
// Falls back to a GPU stretch render when the
|
||||
// compositor does not advertise wp_viewporter.
|
||||
let viewport_set = state_ref.window
|
||||
.set_viewport_destination(
|
||||
current_viewport.width as u32,
|
||||
current_viewport.height as u32,
|
||||
);
|
||||
let preview_ok = if viewport_set {
|
||||
true
|
||||
} else {
|
||||
let mut preview_scene = scene.clone();
|
||||
preview_scene.logical_size = current_viewport;
|
||||
state_ref.renderer.render(&preview_scene).is_ok()
|
||||
};
|
||||
if preview_ok {
|
||||
match state_ref.window.flush_connection() {
|
||||
Ok(()) => {
|
||||
let preview_lag_us = state_ref
|
||||
.pending_viewport_since
|
||||
.map(|t| t.elapsed().as_micros());
|
||||
debug!(
|
||||
target: "ruin_ui_platform_wayland::resize",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
scene_version = scene.version,
|
||||
width = current_viewport.width,
|
||||
height = current_viewport.height,
|
||||
preview_lag_us,
|
||||
compositor_scaled = viewport_set,
|
||||
"presented preview; waiting for correct scene"
|
||||
);
|
||||
let _ = state_ref.event_tx.send(
|
||||
PlatformEvent::FramePresented {
|
||||
window_id: state_ref.window_id,
|
||||
scene_version: scene.version,
|
||||
item_count: scene.item_count(),
|
||||
},
|
||||
);
|
||||
finish_presented_viewport_request(
|
||||
&mut state_ref,
|
||||
scene.logical_size,
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
debug!(
|
||||
target: "ruin_ui_platform_wayland::scene",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
error = %error,
|
||||
"failed to flush preview"
|
||||
);
|
||||
state_ref.window.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if !state_ref.window.presentation_ready() {
|
||||
// Correct scene is ready but the compositor
|
||||
// hasn't signalled vsync yet. Wait for it.
|
||||
state_ref.window.request_redraw();
|
||||
} else {
|
||||
// Correct scene: clear the viewport destination so
|
||||
// the compositor uses the buffer's natural size.
|
||||
// This must happen before wgpu commits the new buffer.
|
||||
state_ref.window.clear_viewport_destination();
|
||||
state_ref.window.arm_frame_callback();
|
||||
let t_render = std::time::Instant::now();
|
||||
match state_ref.renderer.render(scene) {
|
||||
Ok(()) => {
|
||||
let render_gpu_us = t_render.elapsed().as_micros();
|
||||
debug!(
|
||||
target: "ruin_ui_platform_wayland::perf",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
scene_version = scene.version,
|
||||
render_gpu_us,
|
||||
"renderer.render() complete"
|
||||
);
|
||||
match state_ref.window.flush_connection() {
|
||||
Ok(()) => {
|
||||
let resize_lag_us = state_ref
|
||||
.pending_viewport_since
|
||||
.take()
|
||||
.map(|t| t.elapsed().as_micros());
|
||||
debug!(
|
||||
target: "ruin_ui_platform_wayland::resize",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
scene_version = scene.version,
|
||||
width = scene.logical_size.width,
|
||||
height = scene.logical_size.height,
|
||||
resize_lag_us,
|
||||
"resize complete: presented correct scene"
|
||||
);
|
||||
finish_presented_viewport_request(
|
||||
&mut state_ref,
|
||||
scene.logical_size,
|
||||
);
|
||||
let _ = state_ref.event_tx.send(
|
||||
PlatformEvent::FramePresented {
|
||||
window_id: state_ref.window_id,
|
||||
scene_version: scene.version,
|
||||
item_count: scene.item_count(),
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
debug!(
|
||||
target: "ruin_ui_platform_wayland::scene",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
error = %error,
|
||||
"failed to flush presented scene"
|
||||
);
|
||||
state_ref.window.clear_frame_callback();
|
||||
state_ref.window.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(RenderError::Lost | RenderError::Outdated) => {
|
||||
state_ref.window.clear_frame_callback();
|
||||
state_ref
|
||||
.renderer
|
||||
.resize(frame.width, frame.height);
|
||||
state_ref.window.request_redraw();
|
||||
}
|
||||
Err(
|
||||
RenderError::Timeout
|
||||
| RenderError::Occluded
|
||||
| RenderError::Validation,
|
||||
) => {
|
||||
state_ref.window.clear_frame_callback();
|
||||
state_ref.window.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if state_ref.viewport_request_in_flight.is_none()
|
||||
&& state_ref.pending_viewport.is_some()
|
||||
{
|
||||
maybe_request_pending_viewport(&mut state_ref);
|
||||
}
|
||||
|
||||
// Re-arm the keyboard-repeat timer so the pump wakes at the right
|
||||
// time even when no Wayland events or commands are in flight.
|
||||
if let Some(timer) = state_ref.keyboard_repeat_timer.take() {
|
||||
clear_timeout(&timer);
|
||||
}
|
||||
if let Some(repeat) =
|
||||
state_ref.window.state.keyboard_repeat.as_ref()
|
||||
{
|
||||
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),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !keep_running {
|
||||
unsafe {
|
||||
libc::close(pipe_read_fd);
|
||||
libc::close(pipe_write_fd);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Yield until the Wayland watcher, command loop, or a keyboard-repeat
|
||||
// timer writes to the wakeup pipe.
|
||||
ruin_runtime::fd::wait_readable(pipe_read_fd).await.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|| {},
|
||||
@@ -1195,192 +1577,6 @@ fn spawn_window_worker(
|
||||
}
|
||||
}
|
||||
|
||||
fn pump_window_worker(state: Rc<RefCell<WindowWorkerState>>) {
|
||||
let mut reschedule = true;
|
||||
{
|
||||
let mut state_ref = state.borrow_mut();
|
||||
let wait_result = state_ref.window.wait_for_events(Duration::from_millis(16));
|
||||
if state_ref.shutdown_requested || wait_result.is_err() {
|
||||
emit_window_closed(&mut state_ref, false);
|
||||
reschedule = false;
|
||||
} else {
|
||||
for event in state_ref.window.drain_pointer_events() {
|
||||
let _ = state_ref.event_tx.send(PlatformEvent::Pointer {
|
||||
window_id: state_ref.window_id,
|
||||
event,
|
||||
});
|
||||
}
|
||||
for event in state_ref.window.drain_keyboard_events() {
|
||||
tracing::trace!(
|
||||
target: "ruin_ui_platform_wayland::event_bridge",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
keycode = event.keycode,
|
||||
?event.kind,
|
||||
?event.key,
|
||||
text = event.text.as_deref().unwrap_or(""),
|
||||
"forwarding keyboard event to UI runtime"
|
||||
);
|
||||
let _ = state_ref.event_tx.send(PlatformEvent::Keyboard {
|
||||
window_id: state_ref.window_id,
|
||||
event,
|
||||
});
|
||||
}
|
||||
|
||||
if !state_ref.window.is_running() {
|
||||
emit_window_closed(&mut state_ref, true);
|
||||
reschedule = false;
|
||||
} else if let Some(frame) = state_ref.window.prepare_frame() {
|
||||
let current_viewport = UiSize::new(frame.width as f32, frame.height as f32);
|
||||
if frame.resized {
|
||||
debug!(
|
||||
target: "ruin_ui_platform_wayland::resize",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
width = current_viewport.width,
|
||||
height = current_viewport.height,
|
||||
"worker observed resized frame"
|
||||
);
|
||||
state_ref.renderer.resize(frame.width, frame.height);
|
||||
state_ref.window.request_redraw();
|
||||
state_ref.pending_viewport = Some(current_viewport);
|
||||
if state_ref
|
||||
.latest_scene
|
||||
.as_ref()
|
||||
.is_none_or(|scene| scene.logical_size != current_viewport)
|
||||
|| state_ref.viewport_request_in_flight.is_some()
|
||||
{
|
||||
maybe_request_pending_viewport(&mut state_ref);
|
||||
} else {
|
||||
state_ref.pending_viewport = None;
|
||||
}
|
||||
}
|
||||
if !state_ref.opened_emitted {
|
||||
state_ref.opened_emitted = true;
|
||||
let _ = state_ref.event_tx.send(PlatformEvent::Opened {
|
||||
window_id: state_ref.window_id,
|
||||
});
|
||||
}
|
||||
let scene = state_ref.latest_scene.clone();
|
||||
if let Some(scene) = scene.as_ref() {
|
||||
if !state_ref.window.presentation_ready() {
|
||||
state_ref.window.request_redraw();
|
||||
// Wait for the compositor frame callback before attempting another present.
|
||||
} else if scene.logical_size != current_viewport {
|
||||
debug!(
|
||||
target: "ruin_ui_platform_wayland::resize",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
scene_version = scene.version,
|
||||
scene_width = scene.logical_size.width,
|
||||
scene_height = scene.logical_size.height,
|
||||
viewport_width = current_viewport.width,
|
||||
viewport_height = current_viewport.height,
|
||||
"scene size does not match current viewport"
|
||||
);
|
||||
state_ref.pending_viewport = Some(current_viewport);
|
||||
let mut preview_scene = scene.clone();
|
||||
preview_scene.logical_size = current_viewport;
|
||||
state_ref.window.arm_frame_callback();
|
||||
if state_ref.renderer.render(&preview_scene).is_ok() {
|
||||
match state_ref.window.flush_connection() {
|
||||
Ok(()) => {
|
||||
trace!(
|
||||
target: "ruin_ui_platform_wayland::scene",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
scene_version = scene.version,
|
||||
width = current_viewport.width,
|
||||
height = current_viewport.height,
|
||||
"presented scaled preview scene for current viewport"
|
||||
);
|
||||
let _ = state_ref.event_tx.send(PlatformEvent::FramePresented {
|
||||
window_id: state_ref.window_id,
|
||||
scene_version: scene.version,
|
||||
item_count: scene.item_count(),
|
||||
});
|
||||
finish_presented_viewport_request(
|
||||
&mut state_ref,
|
||||
scene.logical_size,
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
debug!(
|
||||
target: "ruin_ui_platform_wayland::scene",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
error = %error,
|
||||
"failed to flush presented preview scene"
|
||||
);
|
||||
state_ref.window.clear_frame_callback();
|
||||
state_ref.window.request_redraw();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state_ref.window.clear_frame_callback();
|
||||
}
|
||||
} else {
|
||||
state_ref.window.arm_frame_callback();
|
||||
match state_ref.renderer.render(scene) {
|
||||
Ok(()) => {
|
||||
match state_ref.window.flush_connection() {
|
||||
Ok(()) => {
|
||||
trace!(
|
||||
target: "ruin_ui_platform_wayland::scene",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
scene_version = scene.version,
|
||||
width = scene.logical_size.width,
|
||||
height = scene.logical_size.height,
|
||||
"presented matching scene"
|
||||
);
|
||||
finish_presented_viewport_request(
|
||||
&mut state_ref,
|
||||
scene.logical_size,
|
||||
);
|
||||
let _ =
|
||||
state_ref.event_tx.send(PlatformEvent::FramePresented {
|
||||
window_id: state_ref.window_id,
|
||||
scene_version: scene.version,
|
||||
item_count: scene.item_count(),
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
debug!(
|
||||
target: "ruin_ui_platform_wayland::scene",
|
||||
window_id = state_ref.window_id.raw(),
|
||||
error = %error,
|
||||
"failed to flush presented scene"
|
||||
);
|
||||
state_ref.window.clear_frame_callback();
|
||||
state_ref.window.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(RenderError::Lost | RenderError::Outdated) => {
|
||||
state_ref.window.clear_frame_callback();
|
||||
state_ref.renderer.resize(frame.width, frame.height);
|
||||
state_ref.window.request_redraw();
|
||||
}
|
||||
Err(
|
||||
RenderError::Timeout
|
||||
| RenderError::Occluded
|
||||
| RenderError::Validation,
|
||||
) => {
|
||||
state_ref.window.clear_frame_callback();
|
||||
state_ref.window.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if state_ref.viewport_request_in_flight.is_none()
|
||||
&& state_ref.pending_viewport.is_some()
|
||||
{
|
||||
maybe_request_pending_viewport(&mut state_ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if reschedule {
|
||||
queue_task(move || pump_window_worker(state));
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_window_closed(state: &mut WindowWorkerState, emit_close_requested: bool) {
|
||||
if emit_close_requested && !state.close_requested_emitted {
|
||||
state.close_requested_emitted = true;
|
||||
@@ -1459,6 +1655,8 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
|
||||
}
|
||||
}
|
||||
|
||||
delegate_noop!(State: ignore wp_viewporter::WpViewporter);
|
||||
delegate_noop!(State: ignore wp_viewport::WpViewport);
|
||||
delegate_noop!(State: ignore wl_compositor::WlCompositor);
|
||||
delegate_noop!(State: ignore wl_surface::WlSurface);
|
||||
delegate_noop!(State: ignore wl_data_device_manager::WlDataDeviceManager);
|
||||
|
||||
Reference in New Issue
Block a user