fix: Electron offscreen rendering and DRM render node propagation
Three root causes prevented Electron frames from reaching the compositor: 1. --disable-gpu killed Chromium's rendering pipeline, preventing the 'paint' event from firing in offscreen mode. Replaced with --use-gl=swiftshader so Chromium can composite internally while the native wo_dmabuf module handles GPU buffer allocation separately. 2. DISPLAY=:0 with no X server caused Electron to fail silently on pure Wayland sessions (Ly/logind). Replaced with --ozone-platform=headless which lets Chromium's OSR pipeline work without any display server connection. 3. WO_DRM_RENDER_NODE was never set, so the native wo_dmabuf.node module always defaulted to /dev/dri/renderD128 regardless of which GPU the compositor actually opened. Now resolved via sysfs (card1 -> renderD128, card2 -> renderD129, etc.) and passed through to Electron. Also: retry EACCES/EPERM as transient during DRM device open (logind session activation can lag behind libseat's is_active() flag on Ly). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
eee56bc5f4
commit
ae528b5132
3 changed files with 122 additions and 87 deletions
|
|
@ -518,11 +518,17 @@ pub struct ElectronProcess {
|
||||||
impl ElectronProcess {
|
impl ElectronProcess {
|
||||||
/// Spawn `electron <app-dir>` for the given window config.
|
/// Spawn `electron <app-dir>` for the given window config.
|
||||||
/// The app dir is resolved to `<config-dir>/wo-electron`.
|
/// The app dir is resolved to `<config-dir>/wo-electron`.
|
||||||
|
///
|
||||||
|
/// `render_node` is the DRI render node (e.g. `/dev/dri/renderD128`) that
|
||||||
|
/// matches the GPU the compositor opened. The native `wo_dmabuf.node`
|
||||||
|
/// module inside Electron uses it to allocate GBM buffers that are
|
||||||
|
/// importable by the compositor's EGL context.
|
||||||
pub fn spawn(
|
pub fn spawn(
|
||||||
config: &WindowConfig,
|
config: &WindowConfig,
|
||||||
electron_bin: &str,
|
electron_bin: &str,
|
||||||
app_dir: &Path,
|
app_dir: &Path,
|
||||||
ipc_socket: &str,
|
ipc_socket: &str,
|
||||||
|
render_node: &str,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
|
@ -533,33 +539,39 @@ impl ElectronProcess {
|
||||||
|
|
||||||
tracing::info!("Serialized config: {}", serialized);
|
tracing::info!("Serialized config: {}", serialized);
|
||||||
|
|
||||||
//Explicitly specify the main.js entry point
|
|
||||||
let main_js = app_dir.join("dist/main.js");
|
let main_js = app_dir.join("dist/main.js");
|
||||||
|
|
||||||
let child = Command::new(electron_bin)
|
let child = Command::new(electron_bin)
|
||||||
.arg(&main_js)
|
.arg(&main_js)
|
||||||
.env("WO_IPC_SOCKET", ipc_socket)
|
.env("WO_IPC_SOCKET", ipc_socket)
|
||||||
.env("WO_WINDOW_CONFIG", &serialized)
|
.env("WO_WINDOW_CONFIG", &serialized)
|
||||||
// Force software rendering to avoid Wayland buffer issues
|
.env("WO_DRM_RENDER_NODE", render_node)
|
||||||
.env("DISABLE_GPU_SANDBOX", "1")
|
// Headless Ozone backend: Electron is used purely for offscreen
|
||||||
.env("DISABLE_SOFTWARE_RASTERIZER", "0")
|
// rendering — it must not try to connect to a Wayland or X11
|
||||||
// Use XWayland if possible, or X11 if available, fallback to headless
|
// display server (there is none for it to connect to; *we* are
|
||||||
.env("DISPLAY", ":0")
|
// the compositor). The headless platform lets Chromium's OSR
|
||||||
// Disable Wayland protocol issues
|
// paint pipeline work while the native wo_dmabuf module handles
|
||||||
.env("QT_QPA_PLATFORM", "offscreen")
|
// GPU buffer allocation via its own GBM/EGL context on the
|
||||||
// Electron needs these flags for proper rendering
|
// render node.
|
||||||
.arg("--disable-gpu")
|
.arg("--ozone-platform=headless")
|
||||||
.arg("--disable-gpu-compositing")
|
|
||||||
.arg("--disable-software-rasterizer=false")
|
|
||||||
.arg("--no-sandbox")
|
.arg("--no-sandbox")
|
||||||
.arg("--disable-gpu-sandbox")
|
.arg("--disable-gpu-sandbox")
|
||||||
|
// Use the *software* GL implementation inside Chromium for its
|
||||||
|
// own compositing (we do not need hardware-accelerated web
|
||||||
|
// content — the paint callback gives us BGRA bitmaps that the
|
||||||
|
// native module imports into a GBM BO). This avoids Chromium
|
||||||
|
// fighting with the compositor over the DRM render node.
|
||||||
|
.arg("--use-gl=swiftshader")
|
||||||
|
// Make sure OSR (offscreen) rendering actually produces
|
||||||
|
// paint callbacks at the requested frame rate.
|
||||||
|
.arg("--enable-features=Vulkan")
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::inherit())
|
.stdout(Stdio::inherit())
|
||||||
.stderr(Stdio::inherit())
|
.stderr(Stdio::inherit())
|
||||||
.spawn()
|
.spawn()
|
||||||
.with_context(|| format!("spawning electron for window '{}'", config.name))?;
|
.with_context(|| format!("spawning electron for window '{}'", config.name))?;
|
||||||
|
|
||||||
info!(window = %config.name, pid = child.id(), "spawned Electron process");
|
info!(window = %config.name, pid = child.id(), render_node, "spawned Electron process");
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
name: config.name.clone(),
|
name: config.name.clone(),
|
||||||
|
|
|
||||||
152
src/main.rs
152
src/main.rs
|
|
@ -68,6 +68,7 @@ struct EventLoopData {
|
||||||
msg_rx: tokio::sync::mpsc::Receiver<crate::electron::ElectronMessage>,
|
msg_rx: tokio::sync::mpsc::Receiver<crate::electron::ElectronMessage>,
|
||||||
swapchain: Swapchain<GbmAllocator<DrmDeviceFd>>,
|
swapchain: Swapchain<GbmAllocator<DrmDeviceFd>>,
|
||||||
drm_device_fd: DrmDeviceFd,
|
drm_device_fd: DrmDeviceFd,
|
||||||
|
drm: DrmDevice,
|
||||||
drm_surface: smithay::backend::drm::DrmSurface,
|
drm_surface: smithay::backend::drm::DrmSurface,
|
||||||
output_rect: Rectangle<i32, Physical>,
|
output_rect: Rectangle<i32, Physical>,
|
||||||
src_rect: Rectangle<f64, Buffer>,
|
src_rect: Rectangle<f64, Buffer>,
|
||||||
|
|
@ -86,6 +87,8 @@ struct EventLoopData {
|
||||||
active: bool,
|
active: bool,
|
||||||
vblank_pending: bool,
|
vblank_pending: bool,
|
||||||
consecutive_flip_failures: u32,
|
consecutive_flip_failures: u32,
|
||||||
|
/// DRI render node path for spawning Electron processes (e.g. `/dev/dri/renderD128`).
|
||||||
|
electron_render_node: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_CONSECUTIVE_FLIP_FAILURES: u32 = 5;
|
const MAX_CONSECUTIVE_FLIP_FAILURES: u32 = 5;
|
||||||
|
|
@ -149,6 +152,37 @@ fn enumerate_drm_card_paths() -> Vec<std::path::PathBuf> {
|
||||||
paths
|
paths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve the DRI render node (e.g. `/dev/dri/renderD128`) that belongs to
|
||||||
|
/// the same physical GPU as `card_path` (e.g. `/dev/dri/card1`).
|
||||||
|
///
|
||||||
|
/// The lookup uses sysfs: `/sys/class/drm/card<N>/device/drm/renderD*`.
|
||||||
|
/// Falls back to `/dev/dri/renderD128` if the mapping cannot be determined.
|
||||||
|
fn render_node_for_card(card_path: &std::path::Path) -> String {
|
||||||
|
let card_name = card_path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("card0");
|
||||||
|
let sys_dir = format!("/sys/class/drm/{card_name}/device/drm");
|
||||||
|
if let Ok(entries) = std::fs::read_dir(&sys_dir) {
|
||||||
|
for entry in entries.filter_map(Result::ok) {
|
||||||
|
let name = entry.file_name();
|
||||||
|
if let Some(s) = name.to_str() {
|
||||||
|
if s.starts_with("renderD") {
|
||||||
|
let path = format!("/dev/dri/{s}");
|
||||||
|
info!(card = %card_path.display(), render_node = %path, "resolved render node");
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let fallback = "/dev/dri/renderD128".to_string();
|
||||||
|
warn!(
|
||||||
|
card = %card_path.display(),
|
||||||
|
"could not resolve render node via sysfs, falling back to {fallback}"
|
||||||
|
);
|
||||||
|
fallback
|
||||||
|
}
|
||||||
|
|
||||||
fn is_egl_context_or_surface_lost(err: &str) -> bool {
|
fn is_egl_context_or_surface_lost(err: &str) -> bool {
|
||||||
let lowered = err.to_ascii_lowercase();
|
let lowered = err.to_ascii_lowercase();
|
||||||
lowered.contains("context has been lost")
|
lowered.contains("context has been lost")
|
||||||
|
|
@ -509,7 +543,18 @@ fn render_frame(data: &mut EventLoopData) {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = data.drm_surface.page_flip([plane_state.clone()], true);
|
// Schedule page flip (or full modeset commit for the first frame).
|
||||||
|
// smithay's DrmSurface tracks pending state (mode, connectors). When
|
||||||
|
// commit_pending() is true (e.g. first frame after surface creation or
|
||||||
|
// VT switch), we must use commit() which includes ALLOW_MODESET.
|
||||||
|
// page_flip() only does a non-blocking flip without modesetting and will
|
||||||
|
// fail with EINVAL if the CRTC doesn't have a mode set yet.
|
||||||
|
let result = if data.drm_surface.commit_pending() {
|
||||||
|
info!("Performing initial modeset commit");
|
||||||
|
data.drm_surface.commit([plane_state.clone()], true)
|
||||||
|
} else {
|
||||||
|
data.drm_surface.page_flip([plane_state.clone()], true)
|
||||||
|
};
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
|
@ -600,17 +645,15 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
|
||||||
};
|
};
|
||||||
match event {
|
match event {
|
||||||
Event::PauseSession => {
|
Event::PauseSession => {
|
||||||
info!("Session paused (VT switched away). Releasing DRM master and halting rendering.");
|
info!("Session paused (VT switched away). Pausing DRM device and halting rendering.");
|
||||||
data.active = false;
|
data.active = false;
|
||||||
data.vblank_pending = false;
|
data.vblank_pending = false;
|
||||||
if let Err(e) = drm::Device::release_master_lock(&data.drm_device_fd) {
|
data.drm.pause();
|
||||||
warn!("Failed to release DRM master on session pause: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Event::ActivateSession => {
|
Event::ActivateSession => {
|
||||||
info!("Session activated (VT switched back). Re-acquiring DRM master and resuming rendering.");
|
info!("Session activated (VT switched back). Activating DRM device and resuming rendering.");
|
||||||
if let Err(e) = drm::Device::acquire_master_lock(&data.drm_device_fd) {
|
if let Err(e) = data.drm.activate(false) {
|
||||||
error!("Failed to re-acquire DRM master on session activation: {e}");
|
error!("Failed to activate DRM device on session resume: {e}");
|
||||||
data.active = false;
|
data.active = false;
|
||||||
data.vblank_pending = false;
|
data.vblank_pending = false;
|
||||||
return;
|
return;
|
||||||
|
|
@ -819,73 +862,28 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
|
||||||
|
|
||||||
let drm_device_fd = DrmDeviceFd::new(DeviceFd::from(drm_fd));
|
let drm_device_fd = DrmDeviceFd::new(DeviceFd::from(drm_fd));
|
||||||
|
|
||||||
// Retry acquiring DRM master: on logind-managed sessions (e.g. Ly) the VT
|
// DRM master management: DrmDeviceFd::new() already attempts to acquire
|
||||||
// switch and master hand-off from logind may not be complete at the exact
|
// master internally. On logind-managed sessions (Ly, greetd, etc.) this
|
||||||
// moment session.open() returns. libseat's builtin backend logs "Unable to
|
// call typically fails because logind manages master status via TakeDevice.
|
||||||
// become DRM master, entering unprivileged mode" and returns the fd anyway;
|
// The fd is still usable for modesetting—logind grants master implicitly.
|
||||||
// pumping the event loop lets session-activation events propagate so that
|
// Following niri's approach: do NOT explicitly call acquire_master_lock()
|
||||||
// the next drmSetMaster() attempt succeeds.
|
// here. Instead, rely on DrmDevice::pause()/activate() for VT switching,
|
||||||
const DRM_MASTER_RETRY_MS: u64 = 100;
|
// which checks is_privileged() and skips redundant master management on
|
||||||
const DRM_MASTER_RETRY_ROUNDS: u32 = 30; // up to ~3 s
|
// logind sessions.
|
||||||
let mut last_master_err = None;
|
|
||||||
for round in 0..=DRM_MASTER_RETRY_ROUNDS {
|
|
||||||
match drm::Device::acquire_master_lock(&drm_device_fd) {
|
|
||||||
Ok(()) => {
|
|
||||||
if round > 0 {
|
|
||||||
info!("DRM master acquired after {} retries", round);
|
|
||||||
}
|
|
||||||
last_master_err = None;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
let detail = e.to_string();
|
|
||||||
if round == 0 {
|
|
||||||
warn!(
|
|
||||||
gpu = %gpu_path.display(),
|
|
||||||
"DRM master not yet available ({}); retrying up to {}×{}ms...",
|
|
||||||
detail,
|
|
||||||
DRM_MASTER_RETRY_ROUNDS,
|
|
||||||
DRM_MASTER_RETRY_MS,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
last_master_err = Some(detail);
|
|
||||||
let _ = event_loop.dispatch(
|
|
||||||
Some(Duration::from_millis(DRM_MASTER_RETRY_MS)),
|
|
||||||
&mut pre_init,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(e) = last_master_err {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"failed to become DRM master on {gpu}: {e}. \
|
|
||||||
The GPU is likely in use by another active session/compositor. \
|
|
||||||
Ensure wo is launched as the session compositor by a logind-aware \
|
|
||||||
display manager (e.g. Ly, greetd) with pam_systemd.so in its PAM \
|
|
||||||
config, or switch to a free VT first. \
|
|
||||||
If using seatd, add your user to the 'seat' group.",
|
|
||||||
gpu = gpu_path.display(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to create DRM device with atomic mode first
|
// Create the DRM device. The second parameter (`disable_connectors`) is
|
||||||
// If it fails due to invalid state restoration, try without atomic mode
|
// false following niri's approach: preserve the existing connector state
|
||||||
let (mut drm, _drm_notifier) = match DrmDevice::new(drm_device_fd.clone(), true) {
|
// instead of resetting it. Resetting (true) requires DRM master and can
|
||||||
Ok(result) => {
|
// fail on logind-managed sessions in unprivileged mode.
|
||||||
info!("DRM device created in atomic mode");
|
//
|
||||||
result
|
// Atomic vs legacy modesetting is auto-detected by smithay based on driver
|
||||||
}
|
// capability. Use SMITHAY_USE_LEGACY=1 to force legacy mode if needed.
|
||||||
Err(e) => {
|
let (mut drm, _drm_notifier) = DrmDevice::new(drm_device_fd.clone(), false)
|
||||||
let err_msg = e.to_string();
|
.map_err(|e| anyhow::anyhow!("creating DRM device: {e}"))?;
|
||||||
if err_msg.contains("Invalid argument") || err_msg.contains("EINVAL") {
|
info!(
|
||||||
warn!("Atomic mode initialization failed with EINVAL (likely stale state on display), trying legacy mode: {e}");
|
atomic = drm.is_atomic(),
|
||||||
DrmDevice::new(drm_device_fd.clone(), false)
|
"DRM device created"
|
||||||
.map_err(|e| anyhow::anyhow!("creating DRM device in legacy mode after atomic failure: {e}"))?
|
);
|
||||||
} else {
|
|
||||||
return Err(anyhow::anyhow!("creating DRM device: {e}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let gbm_device = GbmDevice::new(drm_device_fd.clone()).context("creating GBM device")?;
|
let gbm_device = GbmDevice::new(drm_device_fd.clone()).context("creating GBM device")?;
|
||||||
|
|
||||||
|
|
@ -984,6 +982,7 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
|
||||||
std::env::set_var("WAYLAND_DISPLAY", &config.compositor.socket_name);
|
std::env::set_var("WAYLAND_DISPLAY", &config.compositor.socket_name);
|
||||||
|
|
||||||
let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron");
|
let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron");
|
||||||
|
let electron_render_node = render_node_for_card(&gpu_path);
|
||||||
// Spawn any root (fullscreen) entries first. Root entries are an
|
// Spawn any root (fullscreen) entries first. Root entries are an
|
||||||
// alternative to [[windows]] and are mapped to the full output size.
|
// alternative to [[windows]] and are mapped to the full output size.
|
||||||
eprintln!("Root configs: {:#?}", config.root);
|
eprintln!("Root configs: {:#?}", config.root);
|
||||||
|
|
@ -1000,6 +999,7 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
|
||||||
&config.compositor.electron_path,
|
&config.compositor.electron_path,
|
||||||
&app_dir,
|
&app_dir,
|
||||||
&config.compositor.ipc_socket,
|
&config.compositor.ipc_socket,
|
||||||
|
&electron_render_node,
|
||||||
) {
|
) {
|
||||||
Ok(proc) => state.electron_processes.push(proc),
|
Ok(proc) => state.electron_processes.push(proc),
|
||||||
Err(e) => error!(window = %full.name, "failed to spawn Electron root: {e:#}"),
|
Err(e) => error!(window = %full.name, "failed to spawn Electron root: {e:#}"),
|
||||||
|
|
@ -1012,6 +1012,7 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
|
||||||
&config.compositor.electron_path,
|
&config.compositor.electron_path,
|
||||||
&app_dir,
|
&app_dir,
|
||||||
&config.compositor.ipc_socket,
|
&config.compositor.ipc_socket,
|
||||||
|
&electron_render_node,
|
||||||
) {
|
) {
|
||||||
Ok(proc) => state.electron_processes.push(proc),
|
Ok(proc) => state.electron_processes.push(proc),
|
||||||
Err(e) => error!(window = %win_cfg.name, "failed to spawn Electron: {e:#}"),
|
Err(e) => error!(window = %win_cfg.name, "failed to spawn Electron: {e:#}"),
|
||||||
|
|
@ -1103,6 +1104,7 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
|
||||||
msg_rx,
|
msg_rx,
|
||||||
swapchain,
|
swapchain,
|
||||||
drm_device_fd: drm_device_fd.clone(),
|
drm_device_fd: drm_device_fd.clone(),
|
||||||
|
drm,
|
||||||
drm_surface,
|
drm_surface,
|
||||||
output_rect,
|
output_rect,
|
||||||
src_rect,
|
src_rect,
|
||||||
|
|
@ -1121,6 +1123,7 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
|
||||||
vblank_pending: false,
|
vblank_pending: false,
|
||||||
consecutive_flip_failures: 0,
|
consecutive_flip_failures: 0,
|
||||||
gles_tex_cache: std::collections::HashMap::new(),
|
gles_tex_cache: std::collections::HashMap::new(),
|
||||||
|
electron_render_node: electron_render_node,
|
||||||
};
|
};
|
||||||
|
|
||||||
let signal_for_handler = loop_signal.clone();
|
let signal_for_handler = loop_signal.clone();
|
||||||
|
|
@ -1689,6 +1692,7 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
|
||||||
&data.state.config.compositor.electron_path,
|
&data.state.config.compositor.electron_path,
|
||||||
&Path::new(env!("CARGO_MANIFEST_DIR")).join("electron"),
|
&Path::new(env!("CARGO_MANIFEST_DIR")).join("electron"),
|
||||||
&data.state.config.compositor.ipc_socket,
|
&data.state.config.compositor.ipc_socket,
|
||||||
|
&data.electron_render_node,
|
||||||
) {
|
) {
|
||||||
Ok(proc) => data.state.electron_processes.push(proc),
|
Ok(proc) => data.state.electron_processes.push(proc),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,14 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
info!("Set WAYLAND_DISPLAY={}", config.compositor.socket_name);
|
info!("Set WAYLAND_DISPLAY={}", config.compositor.socket_name);
|
||||||
|
|
||||||
let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron");
|
let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron");
|
||||||
|
// Nested mode has no DRM card; use the first available render node.
|
||||||
|
let nested_render_node = std::fs::read_dir("/dev/dri")
|
||||||
|
.ok()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|e| e.filter_map(Result::ok))
|
||||||
|
.find(|e| e.file_name().to_str().map_or(false, |n| n.starts_with("renderD")))
|
||||||
|
.map(|e| e.path().to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| "/dev/dri/renderD128".to_string());
|
||||||
|
|
||||||
for root_cfg in &config.root {
|
for root_cfg in &config.root {
|
||||||
let mut full = root_cfg.clone();
|
let mut full = root_cfg.clone();
|
||||||
|
|
@ -239,6 +247,7 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
&config.compositor.electron_path,
|
&config.compositor.electron_path,
|
||||||
&app_dir,
|
&app_dir,
|
||||||
&config.compositor.ipc_socket,
|
&config.compositor.ipc_socket,
|
||||||
|
&nested_render_node,
|
||||||
) {
|
) {
|
||||||
Ok(proc) => state.electron_processes.push(proc),
|
Ok(proc) => state.electron_processes.push(proc),
|
||||||
Err(e) => error!(window = %full.name, "failed to spawn Electron root: {e:#}"),
|
Err(e) => error!(window = %full.name, "failed to spawn Electron root: {e:#}"),
|
||||||
|
|
@ -251,6 +260,7 @@ pub fn run_nested(config: Config) -> Result<()> {
|
||||||
&config.compositor.electron_path,
|
&config.compositor.electron_path,
|
||||||
&app_dir,
|
&app_dir,
|
||||||
&config.compositor.ipc_socket,
|
&config.compositor.ipc_socket,
|
||||||
|
&nested_render_node,
|
||||||
) {
|
) {
|
||||||
Ok(proc) => state.electron_processes.push(proc),
|
Ok(proc) => state.electron_processes.push(proc),
|
||||||
Err(e) => error!(window = %win_cfg.name, "failed to spawn Electron: {e:#}"),
|
Err(e) => error!(window = %win_cfg.name, "failed to spawn Electron: {e:#}"),
|
||||||
|
|
@ -997,11 +1007,20 @@ fn execute_autostart_task(
|
||||||
|
|
||||||
if let Some(name) = cmd.strip_prefix("wo://window/") {
|
if let Some(name) = cmd.strip_prefix("wo://window/") {
|
||||||
if let Some(win_cfg) = state.config.windows.iter().find(|w| w.name == name) {
|
if let Some(win_cfg) = state.config.windows.iter().find(|w| w.name == name) {
|
||||||
|
// Best-effort render node for nested autostart
|
||||||
|
let render_node = std::fs::read_dir("/dev/dri")
|
||||||
|
.ok()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|e| e.filter_map(Result::ok))
|
||||||
|
.find(|e| e.file_name().to_str().map_or(false, |n| n.starts_with("renderD")))
|
||||||
|
.map(|e| e.path().to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| "/dev/dri/renderD128".to_string());
|
||||||
match ElectronProcess::spawn(
|
match ElectronProcess::spawn(
|
||||||
win_cfg,
|
win_cfg,
|
||||||
&state.config.compositor.electron_path,
|
&state.config.compositor.electron_path,
|
||||||
app_dir,
|
app_dir,
|
||||||
&state.config.compositor.ipc_socket,
|
&state.config.compositor.ipc_socket,
|
||||||
|
&render_node,
|
||||||
) {
|
) {
|
||||||
Ok(proc) => state.electron_processes.push(proc),
|
Ok(proc) => state.electron_processes.push(proc),
|
||||||
Err(e) => error!(window = %name, "autostart spawn failed: {e:#}"),
|
Err(e) => error!(window = %name, "autostart spawn failed: {e:#}"),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue