mirror of
https://github.com/marcoallegretti/WEFT_OS.git
synced 2026-03-27 01:13:09 +00:00
Add full DRM/KMS backend with libseat session, GBM allocation, EGL display initialisation, and a GpuManager-driven rendering loop. - drm_device.rs: type aliases and per-device/per-output state structs (WeftDrmDevice, WeftOutputSurface, WeftDrmData) - drm.rs: replace skeleton with complete backend libseat session, udev device enumeration, libinput event source, connector scanning via smithay-drm-extras DrmScanner, DrmOutputManager initialisation per CRTC, VBlank-driven render_output, sd_notify(READY=1) - state.rs: add drm: Option<WeftDrmData> field; route dmabuf import through GPU manager when the DRM path is active - Cargo.toml: add renderer_multi, use_system_lib Smithay features; add smithay-drm-extras and sd-notify Linux dependencies render_output submits a clear-colour-only frame to establish the VBlank pipeline. Surface compositing is wired up in a subsequent commit.
645 lines
No EOL
19 KiB
Rust
645 lines
No EOL
19 KiB
Rust
// Non-Linux: DRM/KMS backend is unavailable; callers must use --winit.
|
|
#[cfg(not(target_os = "linux"))]
|
|
pub fn run() -> anyhow::Result<()> {
|
|
anyhow::bail!("DRM/KMS backend requires Linux; pass --winit for development on other platforms")
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
use std::{collections::HashMap, path::Path, sync::Arc, time::Duration};
|
|
|
|
#[cfg(target_os = "linux")]
|
|
use anyhow::Context;
|
|
|
|
#[cfg(target_os = "linux")]
|
|
use smithay::{
|
|
backend::{
|
|
allocator::{
|
|
format::FormatSet,
|
|
gbm::{GbmAllocator, GbmBufferFlags, GbmDevice},
|
|
Fourcc, Modifier,
|
|
},
|
|
drm::{
|
|
compositor::FrameFlags,
|
|
exporter::gbm::GbmFramebufferExporter,
|
|
output::{DrmOutputManager, DrmOutputRenderElements},
|
|
DrmDevice, DrmDeviceFd, DrmEvent, DrmNode, NodeType,
|
|
},
|
|
egl::{EGLDevice, EGLDisplay},
|
|
input::InputEvent,
|
|
libinput::{LibinputInputBackend, LibinputSessionInterface},
|
|
renderer::{
|
|
element::surface::WaylandSurfaceRenderElement,
|
|
gles::GlesRenderer,
|
|
multigpu::{gbm::GbmGlesBackend, GpuManager, MultiRenderer},
|
|
},
|
|
session::{libseat::LibSeatSession, Event as SessionEvent, Session},
|
|
udev::{all_gpus, primary_gpu, UdevBackend, UdevEvent},
|
|
},
|
|
output::{Mode as WlMode, Output, PhysicalProperties, Subpixel},
|
|
reexports::{
|
|
calloop::{EventLoop, Interest, Mode, PostAction, generic::Generic},
|
|
drm::control::{connector, crtc, ModeTypeFlags},
|
|
input::{DeviceCapability, Libinput},
|
|
rustix::fs::OFlags,
|
|
wayland_server::Display,
|
|
},
|
|
utils::{DeviceFd, Transform},
|
|
wayland::socket::ListeningSocketSource,
|
|
};
|
|
|
|
#[cfg(target_os = "linux")]
|
|
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
|
|
|
|
#[cfg(target_os = "linux")]
|
|
use crate::{
|
|
input,
|
|
state::{WeftClientState, WeftCompositorState},
|
|
};
|
|
|
|
#[cfg(target_os = "linux")]
|
|
use super::drm_device::{WeftDrmData, WeftDrmDevice, WeftOutputSurface};
|
|
|
|
#[cfg(target_os = "linux")]
|
|
const SUPPORTED_FORMATS: &[Fourcc] = &[
|
|
Fourcc::Abgr2101010,
|
|
Fourcc::Argb2101010,
|
|
Fourcc::Abgr8888,
|
|
Fourcc::Argb8888,
|
|
];
|
|
|
|
#[cfg(target_os = "linux")]
|
|
const SUPPORTED_FORMATS_8BIT_ONLY: &[Fourcc] = &[Fourcc::Abgr8888, Fourcc::Argb8888];
|
|
|
|
#[cfg(target_os = "linux")]
|
|
type WeftMultiRenderer<'a> = MultiRenderer<
|
|
'a,
|
|
'a,
|
|
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
|
GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
|
|
>;
|
|
|
|
#[cfg(target_os = "linux")]
|
|
pub fn run() -> anyhow::Result<()> {
|
|
let mut event_loop: EventLoop<'static, WeftCompositorState> = EventLoop::try_new()?;
|
|
let loop_handle = event_loop.handle();
|
|
|
|
let mut display = Display::<WeftCompositorState>::new()?;
|
|
let display_handle = display.handle();
|
|
|
|
let (session, session_notifier) =
|
|
LibSeatSession::new().context("failed to create libseat session")?;
|
|
|
|
let seat_name = session.seat().to_owned();
|
|
let loop_signal = event_loop.get_signal();
|
|
|
|
let primary_gpu_node = if let Ok(var) = std::env::var("WEFT_DRM_DEVICE") {
|
|
DrmNode::from_path(var).context("invalid WEFT_DRM_DEVICE path")?
|
|
} else {
|
|
primary_gpu(&seat_name)
|
|
.ok()
|
|
.flatten()
|
|
.and_then(|p| DrmNode::from_path(p).ok()?.node_with_type(NodeType::Render)?.ok())
|
|
.or_else(|| {
|
|
all_gpus(&seat_name)
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.find_map(|p| DrmNode::from_path(p).ok())
|
|
})
|
|
.context("no GPU found")?
|
|
};
|
|
tracing::info!(?primary_gpu_node, "primary GPU");
|
|
|
|
let gpu_manager: super::drm_device::WeftGpuManager =
|
|
GpuManager::new(Default::default()).context("failed to create GPU manager")?;
|
|
|
|
let listening_socket =
|
|
ListeningSocketSource::new_auto().context("failed to create Wayland socket")?;
|
|
let socket_name = listening_socket.socket_name().to_os_string();
|
|
std::env::set_var("WAYLAND_DISPLAY", &socket_name);
|
|
tracing::info!(?socket_name, "Wayland socket open");
|
|
|
|
loop_handle
|
|
.insert_source(listening_socket, |stream, _, state| {
|
|
state
|
|
.display_handle
|
|
.insert_client(stream, Arc::new(WeftClientState::default()))
|
|
.unwrap();
|
|
})
|
|
.map_err(|e| anyhow::anyhow!("socket source: {e}"))?;
|
|
|
|
loop_handle
|
|
.insert_source(
|
|
Generic::new(display, Interest::READ, Mode::Level),
|
|
|_, display, state| {
|
|
// Safety: Display is owned by this Generic source and outlives the event loop.
|
|
unsafe {
|
|
display.get_mut().dispatch_clients(state).unwrap();
|
|
}
|
|
Ok(PostAction::Continue)
|
|
},
|
|
)
|
|
.map_err(|e| anyhow::anyhow!("display source: {e}"))?;
|
|
|
|
let udev_backend =
|
|
UdevBackend::new(&seat_name).context("failed to create udev backend")?;
|
|
|
|
let mut libinput_ctx =
|
|
Libinput::new_with_udev::<LibinputSessionInterface<LibSeatSession>>(
|
|
session.clone().into(),
|
|
);
|
|
libinput_ctx
|
|
.udev_assign_seat(&seat_name)
|
|
.map_err(|_| anyhow::anyhow!("libinput seat assignment failed"))?;
|
|
let libinput_backend = LibinputInputBackend::new(libinput_ctx);
|
|
|
|
loop_handle
|
|
.insert_source(
|
|
libinput_backend,
|
|
move |mut event, _, state: &mut WeftCompositorState| {
|
|
if let InputEvent::DeviceAdded { device } = &mut event {
|
|
if device.has_capability(DeviceCapability::Keyboard) {
|
|
if let Some(led) = state.seat.get_keyboard().map(|k| k.led_state()) {
|
|
device.led_update(led.into());
|
|
}
|
|
if let Some(drm) = state.drm.as_mut() {
|
|
drm.keyboards.push(device.clone());
|
|
}
|
|
}
|
|
}
|
|
input::process_input_event(state, event);
|
|
},
|
|
)
|
|
.map_err(|e| anyhow::anyhow!("libinput source: {e}"))?;
|
|
|
|
loop_handle
|
|
.insert_source(
|
|
session_notifier,
|
|
move |event, &mut (), state: &mut WeftCompositorState| match event {
|
|
SessionEvent::PauseSession => {
|
|
if let Some(drm) = state.drm.as_mut() {
|
|
for dev in drm.devices.values_mut() {
|
|
dev.drm_output_manager.pause();
|
|
}
|
|
}
|
|
}
|
|
SessionEvent::ActivateSession => {
|
|
tracing::info!("session activated");
|
|
}
|
|
},
|
|
)
|
|
.map_err(|e| anyhow::anyhow!("session notifier: {e}"))?;
|
|
|
|
loop_handle
|
|
.insert_source(
|
|
udev_backend,
|
|
move |event, _, state: &mut WeftCompositorState| match event {
|
|
UdevEvent::Added { device_id: _, path } => {
|
|
let node = match DrmNode::from_path(&path) {
|
|
Ok(n) => n,
|
|
Err(e) => {
|
|
tracing::warn!(?e, "failed to build DRM node");
|
|
return;
|
|
}
|
|
};
|
|
if let Err(e) = device_added(state, node, &path) {
|
|
tracing::warn!(?e, "failed to add DRM device");
|
|
}
|
|
}
|
|
UdevEvent::Changed { device_id } => {
|
|
let node = state
|
|
.drm
|
|
.as_ref()
|
|
.and_then(|d| d.devices.keys().find(|n| n.dev_id() == device_id).copied());
|
|
if let Some(node) = node {
|
|
device_changed(state, node);
|
|
}
|
|
}
|
|
UdevEvent::Removed { device_id } => {
|
|
let node = state
|
|
.drm
|
|
.as_ref()
|
|
.and_then(|d| d.devices.keys().find(|n| n.dev_id() == device_id).copied());
|
|
if let Some(node) = node {
|
|
device_removed(state, node);
|
|
}
|
|
}
|
|
},
|
|
)
|
|
.map_err(|e| anyhow::anyhow!("udev source: {e}"))?;
|
|
|
|
let mut state = WeftCompositorState::new(
|
|
display_handle.clone(),
|
|
loop_signal,
|
|
loop_handle,
|
|
seat_name,
|
|
);
|
|
|
|
state.drm = Some(WeftDrmData {
|
|
session,
|
|
primary_gpu: primary_gpu_node,
|
|
gpu_manager,
|
|
devices: HashMap::new(),
|
|
keyboards: Vec::new(),
|
|
display_handle,
|
|
});
|
|
|
|
let existing: Vec<(DrmNode, std::path::PathBuf)> = state
|
|
.drm
|
|
.as_ref()
|
|
.map(|d| {
|
|
all_gpus(d.session.seat())
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.filter_map(|p| DrmNode::from_path(&p).ok().map(|n| (n, p)))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
for (node, path) in existing {
|
|
if let Err(e) = device_added(&mut state, node, &path) {
|
|
tracing::warn!(?e, ?node, "startup device_added failed");
|
|
}
|
|
}
|
|
|
|
let _ = sd_notify::notify(false, &[sd_notify::NotifyState::Ready]);
|
|
|
|
event_loop.run(None, &mut state, |_| {})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn device_added(
|
|
state: &mut WeftCompositorState,
|
|
node: DrmNode,
|
|
path: &Path,
|
|
) -> anyhow::Result<()> {
|
|
let drm_data = state.drm.as_mut().context("DRM data not initialised")?;
|
|
|
|
let fd = drm_data
|
|
.session
|
|
.open(
|
|
path,
|
|
OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
|
|
)
|
|
.context("failed to open DRM device")?;
|
|
|
|
let fd = DrmDeviceFd::new(DeviceFd::from(fd));
|
|
let (drm, notifier) =
|
|
DrmDevice::new(fd.clone(), true).context("DrmDevice::new failed")?;
|
|
let gbm = GbmDevice::new(fd).context("GbmDevice::new failed")?;
|
|
|
|
let render_node = (|| -> anyhow::Result<DrmNode> {
|
|
// Safety: EGLDisplay requires the GBM device to outlive it; gbm lives in WeftDrmDevice.
|
|
let egl_display =
|
|
unsafe { EGLDisplay::new(gbm.clone()).context("EGLDisplay::new failed")? };
|
|
let egl_device =
|
|
EGLDevice::device_for_display(&egl_display).context("no EGL device")?;
|
|
if egl_device.is_software() {
|
|
anyhow::bail!("software renderer");
|
|
}
|
|
let rn = egl_device
|
|
.try_get_render_node()
|
|
.ok()
|
|
.flatten()
|
|
.unwrap_or(node);
|
|
drm_data
|
|
.gpu_manager
|
|
.as_mut()
|
|
.add_node(rn, gbm.clone())
|
|
.map_err(|e| anyhow::anyhow!("add_node: {e:?}"))?;
|
|
Ok(rn)
|
|
})()
|
|
.map_err(|e| {
|
|
tracing::warn!(?e, "EGL init failed; output may render black");
|
|
e
|
|
})
|
|
.ok();
|
|
|
|
let effective_gpu = render_node.unwrap_or(drm_data.primary_gpu);
|
|
|
|
let allocator = GbmAllocator::new(
|
|
gbm.clone(),
|
|
GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
|
|
);
|
|
|
|
let exporter = GbmFramebufferExporter::new(gbm.clone(), render_node.into());
|
|
|
|
let color_formats = if std::env::var("WEFT_DISABLE_10BIT").is_ok() {
|
|
SUPPORTED_FORMATS_8BIT_ONLY
|
|
} else {
|
|
SUPPORTED_FORMATS
|
|
};
|
|
|
|
let render_formats: FormatSet = drm_data
|
|
.gpu_manager
|
|
.single_renderer(&effective_gpu)
|
|
.map(|r| {
|
|
r.as_ref()
|
|
.egl_context()
|
|
.dmabuf_render_formats()
|
|
.iter()
|
|
.filter(|f| render_node.is_some() || f.modifier == Modifier::Linear)
|
|
.copied()
|
|
.collect::<FormatSet>()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let drm_output_manager = DrmOutputManager::new(
|
|
drm,
|
|
allocator,
|
|
exporter,
|
|
Some(gbm),
|
|
color_formats.iter().copied(),
|
|
render_formats,
|
|
);
|
|
|
|
let registration_token = state
|
|
.loop_handle
|
|
.insert_source(
|
|
notifier,
|
|
move |event, _metadata, data: &mut WeftCompositorState| match event {
|
|
DrmEvent::VBlank(crtc) => render_output(data, node, crtc),
|
|
DrmEvent::Error(e) => tracing::error!(?e, "DRM error"),
|
|
},
|
|
)
|
|
.map_err(|e| anyhow::anyhow!("DRM notifier: {e}"))?;
|
|
|
|
state.drm.as_mut().unwrap().devices.insert(
|
|
node,
|
|
WeftDrmDevice {
|
|
drm_output_manager,
|
|
drm_scanner: DrmScanner::new(),
|
|
surfaces: HashMap::new(),
|
|
render_node,
|
|
registration_token,
|
|
},
|
|
);
|
|
|
|
device_changed(state, node);
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn device_changed(state: &mut WeftCompositorState, node: DrmNode) {
|
|
let drm_data = match state.drm.as_mut() {
|
|
Some(d) => d,
|
|
None => return,
|
|
};
|
|
let device = match drm_data.devices.get_mut(&node) {
|
|
Some(d) => d,
|
|
None => return,
|
|
};
|
|
|
|
let events: Vec<DrmScanEvent> = device
|
|
.drm_scanner
|
|
.scan_connectors(device.drm_output_manager.device());
|
|
|
|
for event in events {
|
|
match event {
|
|
DrmScanEvent::Connected {
|
|
connector,
|
|
crtc: Some(crtc),
|
|
} => connector_connected(state, node, connector, crtc),
|
|
DrmScanEvent::Disconnected {
|
|
connector,
|
|
crtc: Some(crtc),
|
|
} => connector_disconnected(state, node, connector, crtc),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn connector_connected(
|
|
state: &mut WeftCompositorState,
|
|
node: DrmNode,
|
|
connector: connector::Info,
|
|
crtc: crtc::Handle,
|
|
) {
|
|
let name = format!("{:?}-{}", connector.interface(), connector.interface_id());
|
|
|
|
let mode = match connector
|
|
.modes()
|
|
.iter()
|
|
.find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
|
|
.copied()
|
|
.or_else(|| connector.modes().first().copied())
|
|
{
|
|
Some(m) => m,
|
|
None => {
|
|
tracing::warn!(?name, "connector has no modes");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let wl_mode = WlMode {
|
|
size: (mode.size().0 as i32, mode.size().1 as i32).into(),
|
|
refresh: mode.vrefresh() as i32 * 1000,
|
|
};
|
|
|
|
let output = Output::new(
|
|
name.clone(),
|
|
PhysicalProperties {
|
|
size: (0, 0).into(),
|
|
subpixel: Subpixel::Unknown,
|
|
make: "Unknown".to_string(),
|
|
model: name.clone(),
|
|
},
|
|
);
|
|
|
|
let drm_data = match state.drm.as_mut() {
|
|
Some(d) => d,
|
|
None => return,
|
|
};
|
|
|
|
let global = output.create_global::<WeftCompositorState>(&drm_data.display_handle);
|
|
output.change_current_state(
|
|
Some(wl_mode),
|
|
Some(Transform::Normal),
|
|
None,
|
|
Some((0, 0).into()),
|
|
);
|
|
output.set_preferred(wl_mode);
|
|
|
|
let render_node = drm_data
|
|
.devices
|
|
.get(&node)
|
|
.and_then(|d| d.render_node)
|
|
.unwrap_or(drm_data.primary_gpu);
|
|
|
|
let planes = drm_data
|
|
.devices
|
|
.get_mut(&node)
|
|
.and_then(|d| d.drm_output_manager.device().planes(&crtc).ok());
|
|
|
|
let WeftDrmData {
|
|
ref mut gpu_manager,
|
|
ref mut devices,
|
|
..
|
|
} = *drm_data;
|
|
|
|
let device = match devices.get_mut(&node) {
|
|
Some(d) => d,
|
|
None => return,
|
|
};
|
|
|
|
let mut renderer = match gpu_manager.single_renderer(&render_node) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
tracing::warn!(?e, "no renderer for output init");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let drm_output = match device.drm_output_manager.initialize_output(
|
|
crtc,
|
|
mode,
|
|
&[connector.handle()],
|
|
&output,
|
|
planes,
|
|
&mut renderer,
|
|
&DrmOutputRenderElements::default(),
|
|
) {
|
|
Ok(o) => o,
|
|
Err(e) => {
|
|
tracing::warn!(?e, ?name, "initialize_output failed");
|
|
return;
|
|
}
|
|
};
|
|
|
|
device.surfaces.insert(
|
|
crtc,
|
|
WeftOutputSurface {
|
|
output: output.clone(),
|
|
drm_output,
|
|
device_id: node,
|
|
global,
|
|
},
|
|
);
|
|
|
|
state.space.map_output(&output, (0, 0));
|
|
tracing::info!(?name, "output connected");
|
|
render_output(state, node, crtc);
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn connector_disconnected(
|
|
state: &mut WeftCompositorState,
|
|
node: DrmNode,
|
|
_connector: connector::Info,
|
|
crtc: crtc::Handle,
|
|
) {
|
|
let drm_data = match state.drm.as_mut() {
|
|
Some(d) => d,
|
|
None => return,
|
|
};
|
|
if let Some(device) = drm_data.devices.get_mut(&node) {
|
|
if let Some(surface) = device.surfaces.remove(&crtc) {
|
|
state.space.unmap_output(&surface.output);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn device_removed(state: &mut WeftCompositorState, node: DrmNode) {
|
|
let drm_data = match state.drm.as_mut() {
|
|
Some(d) => d,
|
|
None => return,
|
|
};
|
|
if let Some(device) = drm_data.devices.remove(&node) {
|
|
state.loop_handle.remove(device.registration_token);
|
|
for surface in device.surfaces.into_values() {
|
|
state.space.unmap_output(&surface.output);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn render_output(state: &mut WeftCompositorState, node: DrmNode, crtc: crtc::Handle) {
|
|
let output = {
|
|
let drm_data = match state.drm.as_ref() {
|
|
Some(d) => d,
|
|
None => return,
|
|
};
|
|
match drm_data
|
|
.devices
|
|
.get(&node)
|
|
.and_then(|d| d.surfaces.get(&crtc))
|
|
{
|
|
Some(s) => s.output.clone(),
|
|
None => return,
|
|
}
|
|
};
|
|
|
|
let render_node = {
|
|
let d = state.drm.as_ref().unwrap();
|
|
d.devices
|
|
.get(&node)
|
|
.and_then(|d| d.render_node)
|
|
.unwrap_or(d.primary_gpu)
|
|
};
|
|
|
|
let WeftCompositorState {
|
|
ref mut drm,
|
|
ref space,
|
|
..
|
|
} = *state;
|
|
|
|
let drm_data = match drm.as_mut() {
|
|
Some(d) => d,
|
|
None => return,
|
|
};
|
|
|
|
let WeftDrmData {
|
|
ref mut gpu_manager,
|
|
ref mut devices,
|
|
..
|
|
} = *drm_data;
|
|
|
|
let device = match devices.get_mut(&node) {
|
|
Some(d) => d,
|
|
None => return,
|
|
};
|
|
let surface = match device.surfaces.get_mut(&crtc) {
|
|
Some(s) => s,
|
|
None => return,
|
|
};
|
|
|
|
let mut renderer = match gpu_manager.single_renderer(&render_node) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
tracing::warn!(?e, "renderer unavailable");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Wave 2: clear-colour-only frame to establish the VBlank pipeline.
|
|
// Surface compositing is added in Wave 3.
|
|
let elements: &[WaylandSurfaceRenderElement<WeftMultiRenderer<'_>>] = &[];
|
|
match surface.drm_output.render_frame(
|
|
&mut renderer,
|
|
elements,
|
|
[0.08_f32, 0.08, 0.08, 1.0],
|
|
FrameFlags::DEFAULT,
|
|
) {
|
|
Ok(result) if !result.is_empty => {
|
|
if let Err(e) = surface.drm_output.queue_frame(None) {
|
|
tracing::warn!(?e, "queue_frame failed");
|
|
}
|
|
}
|
|
Ok(_) => {}
|
|
Err(e) => tracing::warn!(?e, "render_frame failed"),
|
|
}
|
|
|
|
space.elements().for_each(|window| {
|
|
window.send_frame(
|
|
&output,
|
|
Duration::ZERO,
|
|
Some(Duration::ZERO),
|
|
|_, _| Some(output.clone()),
|
|
);
|
|
});
|
|
|
|
let _ = state.display_handle.flush_clients();
|
|
} |