use crate::platform::{
alsa::{self, AudioThread},
blit,
evdev::Input,
InputSnapshot, Platform, Rotate,
};
use anyhow::{anyhow, Context, Result};
use drm::{
buffer::{Buffer as DrmBuffer, DrmFourcc},
control::{
connector, crtc, dumbbuffer::DumbBuffer, framebuffer, Device as ControlDevice, Mode,
ModeTypeFlags, PageFlipFlags,
},
Device as DrmDevice,
};
use nix::poll::{poll, PollFd, PollFlags, PollTimeout};
use pixel8_runtime::{audio::AudioHandle, fb::Framebuffer};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
const KDSETMODE: u64 = 0x4B3A;
nix::ioctl_write_int_bad!(kd_setmode, KDSETMODE);
struct Card(OwnedFd);
impl AsFd for Card {
fn as_fd(&self) -> BorrowedFd<'_> {
self.0.as_fd()
}
}
impl DrmDevice for Card {}
impl ControlDevice for Card {}
pub struct KmsPlatform {
card: Card,
crtc: crtc::Handle,
dumb: [DumbBuffer; 2],
fb: [framebuffer::Handle; 2],
front: usize,
width: usize,
height: usize,
rotate: Rotate,
input: Input,
scratch: Vec<u32>,
vt: Option<std::fs::File>,
_audio: Option<AudioThread>,
}
impl KmsPlatform {
pub fn new(audio: AudioHandle) -> Result<KmsPlatform> {
let card = open_card()?;
let (con, mode, crtc) = pick_output(&card)?;
let (w, h) = mode.size();
let (width, height) = (w as usize, h as usize);
let (db0, fb0) = make_buffer(&card, w, h)?;
let (db1, fb1) = make_buffer(&card, w, h)?;
let con_handle = con.handle();
let rotate = Rotate::from_env_or(detect_rotation(&card, con_handle));
card.set_crtc(crtc, Some(fb0), (0, 0), &[con_handle], Some(mode))
.context(
"setting the display mode (could not become DRM master \
— close any running compositor or run from a bare TTY)",
)?;
let vt = set_vt_graphics();
Ok(KmsPlatform {
card,
crtc,
dumb: [db0, db1],
fb: [fb0, fb1],
front: 0,
width,
height,
rotate,
input: Input::new(),
scratch: vec![0u32; width * height],
vt,
_audio: alsa::spawn(audio),
})
}
}
impl Platform for KmsPlatform {
fn poll(&mut self) -> InputSnapshot {
self.input.poll()
}
fn present(&mut self, fb: &Framebuffer) -> Result<()> {
let back = 1 - self.front;
blit_to_dumb(
&self.card,
&mut self.dumb[back],
self.width,
self.height,
fb,
self.rotate,
&mut self.scratch,
)?;
self.card
.page_flip(self.crtc, self.fb[back], PageFlipFlags::EVENT, None)?;
wait_for_page_flip(&self.card)?;
self.front = back;
Ok(())
}
}
impl Drop for KmsPlatform {
fn drop(&mut self) {
if let Some(f) = &self.vt {
let _ = unsafe { kd_setmode(f.as_raw_fd(), 0) }; }
}
}
fn open_card() -> Result<Card> {
if let Ok(path) = std::env::var("PIXEL8_DRM_CARD") {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.with_context(|| format!("opening {path}"))?;
return Ok(Card(OwnedFd::from(file)));
}
let mut names: Vec<std::ffi::OsString> = std::fs::read_dir("/dev/dri")
.context("reading /dev/dri")?
.filter_map(|e| e.ok())
.map(|e| e.file_name())
.filter(|n| n.to_str().is_some_and(|s| s.starts_with("card")))
.collect();
names.sort();
for name in &names {
let path = std::path::Path::new("/dev/dri").join(name);
let Ok(file) = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
else {
continue;
};
let card = Card(OwnedFd::from(file));
if card_has_display(&card) {
return Ok(card);
}
}
Err(anyhow!(
"no DRM device with a connected display found in /dev/dri. \
The desktop player must run from a bare virtual terminal \
(e.g. Ctrl+Alt+F3), not inside an X11/Wayland session. \
Set PIXEL8_DRM_CARD to override."
))
}
fn card_has_display(card: &Card) -> bool {
let Ok(res) = card.resource_handles() else {
return false;
};
res.connectors().iter().any(|&h| {
card.get_connector(h, false)
.is_ok_and(|c| c.state() == connector::State::Connected)
})
}
fn pick_output(card: &Card) -> Result<(connector::Info, Mode, crtc::Handle)> {
let res = card.resource_handles()?;
let con = res
.connectors()
.iter()
.filter_map(|&h| card.get_connector(h, true).ok())
.find(|c| c.state() == connector::State::Connected)
.ok_or_else(|| anyhow!("no connected display"))?;
let mode = con
.modes()
.iter()
.find(|m| m.mode_type().contains(ModeTypeFlags::PREFERRED))
.or_else(|| con.modes().first())
.copied()
.ok_or_else(|| anyhow!("connector has no modes"))?;
let enc_handle = con
.current_encoder()
.or_else(|| con.encoders().first().copied())
.ok_or_else(|| anyhow!("connector has no encoder"))?;
let enc = card.get_encoder(enc_handle)?;
let crtc = res
.filter_crtcs(enc.possible_crtcs())
.first()
.copied()
.ok_or_else(|| anyhow!("no usable CRTC"))?;
Ok((con, mode, crtc))
}
fn make_buffer(card: &Card, w: u16, h: u16) -> Result<(DumbBuffer, framebuffer::Handle)> {
let db = card.create_dumb_buffer((w.into(), h.into()), DrmFourcc::Xrgb8888, 32)?;
let fb = card.add_framebuffer(&db, 24, 32)?;
Ok((db, fb))
}
fn blit_to_dumb(
card: &Card,
dumb: &mut DumbBuffer,
width: usize,
height: usize,
fb: &Framebuffer,
rotate: Rotate,
scratch: &mut [u32],
) -> Result<()> {
let pitch = dumb.pitch() as usize; let expected_pitch = width * 4; let mut map = card.map_dumb_buffer(dumb)?;
if pitch == expected_pitch {
let dst: &mut [u32] = bytemuck::cast_slice_mut(map.as_mut());
blit::present_into(fb, dst, width, height, rotate);
} else {
blit::present_into(fb, scratch, width, height, rotate);
let raw: &mut [u8] = map.as_mut();
for y in 0..height {
let src_start = y * width;
let dst_byte = y * pitch;
let src_bytes = bytemuck::cast_slice::<u32, u8>(&scratch[src_start..src_start + width]);
raw[dst_byte..dst_byte + expected_pitch].copy_from_slice(src_bytes);
}
}
Ok(())
}
fn wait_for_page_flip(card: &Card) -> Result<()> {
let mut pfds = [PollFd::new(card.as_fd(), PollFlags::POLLIN)];
poll(&mut pfds, PollTimeout::NONE)?;
for _ev in card.receive_events()? {}
Ok(())
}
fn detect_rotation(card: &Card, con: connector::Handle) -> Rotate {
use drm::control::property::Value;
let Ok(props) = card.get_properties(con) else {
return Rotate::None;
};
for (&id, &raw) in props.iter() {
let Ok(info) = card.get_property(id) else {
continue;
};
if info.name().to_str() != Ok("panel orientation") {
continue;
}
if let Value::Enum(Some(ev)) = info.value_type().convert_value(raw) {
return match ev.name().to_str() {
Ok("Upside Down") => Rotate::Cw180,
Ok("Left Side Up") => Rotate::Cw90,
Ok("Right Side Up") => Rotate::Cw270,
_ => Rotate::None,
};
}
}
Rotate::None
}
fn set_vt_graphics() -> Option<std::fs::File> {
let f = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty0")
.ok()?;
let _ = unsafe { kd_setmode(f.as_raw_fd(), 1) }; Some(f)
}