use softbuffer::{Context, Surface};
use velox_dom::VeloxError;
use winit::window::Window;
use crate::viewport::Viewport;
pub fn is_compositor_available() -> bool {
if std::env::var("VELOX_HEADLESS").as_deref() == Ok("1") {
return false;
}
let wayland = std::env::var("WAYLAND_DISPLAY").is_ok();
let x11 = std::env::var("DISPLAY").is_ok();
wayland || x11
}
#[cfg(unix)]
fn is_wayland_socket_alive() -> bool {
let display = match std::env::var("WAYLAND_DISPLAY") {
Ok(d) => d,
Err(_) => return false,
};
let socket_path = if display.starts_with('/') {
std::path::PathBuf::from(display)
} else {
let runtime = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".into());
std::path::PathBuf::from(runtime).join(&display)
};
if !socket_path.exists() {
return false;
}
use std::os::unix::net::UnixStream;
UnixStream::connect(&socket_path).is_ok()
}
#[cfg(not(unix))]
fn is_wayland_socket_alive() -> bool {
false
}
fn force_backend(backend: &str) {
if cfg!(target_os = "linux") {
unsafe { std::env::set_var("WINIT_UNIX_BACKEND", backend) }
}
}
pub fn debug_compositor_choice() -> String {
format!(
"[velox] backend={:?} wayland={:?} display={:?}",
std::env::var("WINIT_UNIX_BACKEND"),
std::env::var("WAYLAND_DISPLAY"),
std::env::var("DISPLAY")
)
}
static DEBUG_COMPOSITOR_LOG: std::sync::Once = std::sync::Once::new();
pub fn prepare_backend() -> bool {
if std::env::var("VELOX_DEBUG_COMPOSITOR").is_ok() {
DEBUG_COMPOSITOR_LOG.call_once(|| {
eprintln!("{}", debug_compositor_choice());
});
}
if std::env::var("VELOX_HEADLESS").as_deref() == Ok("1") {
return true;
}
let has_display = std::env::var("DISPLAY").is_ok();
let has_wayland = std::env::var("WAYLAND_DISPLAY").is_ok();
if has_display && has_wayland {
force_backend("x11");
} else if has_wayland && !has_display {
if !is_wayland_socket_alive() {
return true;
}
}
false
}
fn is_broken_pipe_error(msg: &str) -> bool {
let lower = msg.to_ascii_lowercase();
lower.contains("broken pipe")
|| lower.contains("os error 32")
|| lower.contains("epipe")
|| lower.contains("no compositor")
|| lower.contains("failed to connect to wayland")
|| lower.contains("x11 display")
}
fn compositor_help(err_detail: &str) -> String {
format!(
"{err_detail} — no compositor available (headless environment?). \
Set VELOX_HEADLESS=1 to run without a window, or ensure WAYLAND_DISPLAY/DISPLAY is set \
and a Wayland/X11 compositor is running."
)
}
static EPIPE_WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
const MAX_STAGING_BYTES: usize = 128 * 1024 * 1024;
fn nonzero_dim(v: u32) -> std::num::NonZeroU32 {
std::num::NonZeroU32::new(v).unwrap_or(std::num::NonZeroU32::MIN)
}
fn staging_len(w: u32, h: u32) -> Result<usize, VeloxError> {
let len = (w as usize)
.checked_mul(h as usize)
.and_then(|px| px.checked_mul(4))
.ok_or_else(|| {
VeloxError::Render(
"softbuffer: presenter dimensions overflow the staging buffer".into(),
)
})?;
if len > MAX_STAGING_BYTES {
return Err(VeloxError::Render(format!(
"softbuffer: presenter {w}x{h} needs {len} bytes, over the {MAX_STAGING_BYTES}-byte cap"
)));
}
Ok(len)
}
fn claim_epipe_warning(flag: &std::sync::atomic::AtomicBool) -> bool {
!flag.swap(true, std::sync::atomic::Ordering::SeqCst)
}
fn warn_epipe_once(detail: &str) {
if claim_epipe_warning(&EPIPE_WARNED) {
eprintln!(
"[velox] compositor connection lost (broken pipe) — presenter degraded to no-op, \
rendering continues offscreen. Error: {detail}"
);
eprintln!(
"[velox] hint: set VELOX_HEADLESS=1 to run without a window, or ensure \
WAYLAND_DISPLAY/DISPLAY points to a running compositor."
);
}
}
pub struct SoftbufferPresenter {
_context: Context,
surface: Surface,
viewport: Viewport,
width: u32,
height: u32,
rgba: Vec<u8>,
degraded: bool,
}
impl SoftbufferPresenter {
pub fn new(window: &Window, width: u32, height: u32) -> Result<Self, VeloxError> {
if !is_compositor_available() {
return Err(VeloxError::Render(compositor_help(
"softbuffer: no display server detected (WAYLAND_DISPLAY and DISPLAY are both unset)",
)));
}
let context = unsafe {
Context::new(window).map_err(|e| {
let msg = e.to_string();
if is_broken_pipe_error(&msg) {
VeloxError::Render(compositor_help(&format!(
"softbuffer context failed: {msg}"
)))
} else {
VeloxError::Render(format!("softbuffer context failed: {msg}"))
}
})?
};
let mut surface = unsafe {
Surface::new(&context, window).map_err(|e| {
let msg = e.to_string();
if is_broken_pipe_error(&msg) {
VeloxError::Render(compositor_help(&format!(
"softbuffer surface failed: {msg}"
)))
} else {
VeloxError::Render(format!("softbuffer surface failed: {msg}"))
}
})?
};
let w = width.max(1);
let h = height.max(1);
if let Err(e) = surface.resize(nonzero_dim(w), nonzero_dim(h)) {
let msg = e.to_string();
if is_broken_pipe_error(&msg) {
return Err(VeloxError::Render(compositor_help(&format!(
"softbuffer resize failed: {msg}"
))));
} else {
return Err(VeloxError::Render(format!(
"softbuffer resize failed: {}",
msg
)));
}
}
let viewport = Viewport::new(w, h, 1.0);
let rgba = vec![0u8; staging_len(w, h)?];
Ok(Self {
_context: context,
surface,
viewport,
width: w,
height: h,
rgba,
degraded: false,
})
}
pub fn resize(&mut self, width: u32, height: u32) -> Result<(), VeloxError> {
if self.degraded {
return Ok(());
}
let w = width.max(1);
let h = height.max(1);
if w == self.width && h == self.height {
return Ok(());
}
let len = staging_len(w, h)?;
if let Err(e) = self.surface.resize(nonzero_dim(w), nonzero_dim(h)) {
let msg = e.to_string();
if is_broken_pipe_error(&msg) {
warn_epipe_once(&msg);
self.degraded = true;
return Ok(());
} else {
return Err(VeloxError::Render(format!(
"softbuffer resize failed: {}",
msg
)));
}
}
self.viewport.set_physical(w, h);
self.width = w;
self.height = h;
self.rgba.resize(len, 0);
Ok(())
}
pub fn present(
&mut self,
skia_surface: &mut crate::skia_surface::SkiaSurface,
) -> Result<(), VeloxError> {
if self.degraded {
return Ok(());
}
let width = skia_surface.width.max(1) as u32;
let height = skia_surface.height.max(1) as u32;
self.resize(width, height)?;
if self.degraded {
return Ok(());
}
let info = skia_safe::ImageInfo::new(
(self.width as i32, self.height as i32),
skia_safe::ColorType::N32,
skia_safe::AlphaType::Premul,
None,
);
let row_bytes = (self.width * 4) as usize;
if !skia_surface.read_pixels(&info, &mut self.rgba, row_bytes, (0, 0)) {
return Err(VeloxError::Render("skia: read_pixels failed".into()));
}
let mut buffer = match self.surface.buffer_mut() {
Ok(b) => b,
Err(e) => {
let msg = e.to_string();
if is_broken_pipe_error(&msg) {
warn_epipe_once(&msg);
self.degraded = true;
return Ok(());
} else {
return Err(VeloxError::Render(format!(
"softbuffer buffer_mut failed: {}",
msg
)));
}
}
};
let pixels: &mut [u32] = &mut buffer;
let pixel_count = (self.width as usize) * (self.height as usize);
if pixels.len() < pixel_count {
return Err(VeloxError::Render(
"softbuffer: buffer smaller than expected".into(),
));
}
let dst = bytemuck::cast_slice_mut::<u32, u8>(&mut pixels[..pixel_count]);
dst.copy_from_slice(&self.rgba[..pixel_count * 4]);
if let Err(e) = buffer.present() {
let msg = e.to_string();
if is_broken_pipe_error(&msg) {
warn_epipe_once(&msg);
self.degraded = true;
return Ok(());
} else {
return Err(VeloxError::Render(format!(
"softbuffer present failed: {}",
msg
)));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
struct EnvVarGuard {
key: &'static str,
prev: Option<String>,
}
impl EnvVarGuard {
fn set(key: &'static str, value: &str) -> Self {
let prev = std::env::var(key).ok();
unsafe { std::env::set_var(key, value) };
Self { key, prev }
}
fn remove(key: &'static str) -> Self {
let prev = std::env::var(key).ok();
unsafe { std::env::remove_var(key) };
Self { key, prev }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match self.prev.take() {
Some(v) => unsafe { std::env::set_var(self.key, v) },
None => unsafe { std::env::remove_var(self.key) },
};
}
}
#[test]
fn debug_flag_does_not_change_degraded_path() {
let _g_backend = EnvVarGuard::remove("WINIT_UNIX_BACKEND");
let _g_wayland = EnvVarGuard::set("WAYLAND_DISPLAY", "wayland-test-0");
let _g_display = EnvVarGuard::set("DISPLAY", ":99");
let _g_debug = EnvVarGuard::set("VELOX_DEBUG_COMPOSITOR", "1");
let _g_headless = EnvVarGuard::remove("VELOX_HEADLESS");
assert!(!prepare_backend());
assert_eq!(
std::env::var("WINIT_UNIX_BACKEND").as_deref(),
Ok("x11"),
"force-x11 must be kept when DISPLAY and WAYLAND_DISPLAY are both set"
);
assert_eq!(
debug_compositor_choice(),
"[velox] backend=Ok(\"x11\") wayland=Ok(\"wayland-test-0\") display=Ok(\":99\")"
);
{
let _g = EnvVarGuard::remove("WINIT_UNIX_BACKEND");
let s = debug_compositor_choice();
assert!(s.contains("backend=Err(NotPresent)"), "got: {s}");
}
let _g_headless_on = EnvVarGuard::set("VELOX_HEADLESS", "1");
assert!(!is_compositor_available());
assert!(prepare_backend());
assert!(prepare_backend());
}
#[test]
fn epipe_warning_claimed_once_per_session() {
let flag = std::sync::atomic::AtomicBool::new(false);
assert!(claim_epipe_warning(&flag));
assert!(!claim_epipe_warning(&flag));
assert!(!claim_epipe_warning(&flag));
}
}