use std::time::Duration;
use std::collections::HashMap;
use easydrm::{EasyDRM, MonitorContextCreationRequest, gl};
use rand::Rng;
use tokio::time::Instant;
use skia::gpu;
use skia_safe::{self as skia, FontMgr, Typeface, gpu::gl::FramebufferInfo};
struct MonitorContext {
frame_count: u64,
color_offset: f32,
monitor_name: String,
fps_frames: u64,
fps_last: Instant,
fps_current: f64,
surfaces: HashMap<u32, skia::Surface>,
current_fbo: u32,
width: usize,
height: usize,
gl: easydrm::gl::Gles2,
font: Typeface,
}
impl MonitorContext {
fn new(req: &MonitorContextCreationRequest<'_>) -> Self {
unsafe {
let version = std::ffi::CStr::from_ptr(req.gl.GetString(gl::VERSION) as *const i8);
println!("Monitor initialized with OpenGL version: {:?}", version);
}
let initial_fbo = current_framebuffer(req.gl);
let surfaces = HashMap::new();
let mut rng = rand::rng();
let fontmgr = FontMgr::new();
let font = fontmgr.match_family("Inter").new_typeface(0).unwrap();
Self {
font: font,
frame_count: 0,
color_offset: rng.random_range(0.0..1.0),
monitor_name: format!("Monitor-{}", rng.random_range(0..10_000)),
gl: req.gl.clone(),
fps_frames: 0,
fps_last: Instant::now(),
fps_current: 0.0,
surfaces,
current_fbo: initial_fbo,
width: req.width,
height: req.height,
}
}
fn update_and_get_color(&mut self) -> (f32, f32, f32) {
self.frame_count += 1;
self.fps_frames += 1;
let hue = ((self.frame_count as f32 * 0.01 + self.color_offset) % 1.0).abs();
hsv_to_rgb(hue, 0.8, 1.0)
}
fn maybe_update_fps(&mut self, now: Instant) {
let elapsed = now.duration_since(self.fps_last);
if elapsed >= Duration::from_secs(1) {
self.fps_current = self.fps_frames as f64 / elapsed.as_secs_f64();
self.fps_frames = 0;
self.fps_last = now;
}
}
fn ensure_surface_for_current_fbo(
&mut self,
gr: &mut gpu::DirectContext,
width: usize,
height: usize,
) {
if self.width != width || self.height != height {
self.width = width;
self.height = height;
self.surfaces.clear();
}
let fbo = current_framebuffer(&self.gl);
self.current_fbo = fbo;
self.surfaces.entry(fbo).or_insert_with(|| {
create_skia_surface_for_fbo(gr, fbo, self.width, self.height)
});
}
fn draw_fps_overlay(&mut self, gr: &mut gpu::DirectContext) {
let surface = self
.surfaces
.get_mut(&self.current_fbo)
.expect("Skia surface for current framebuffer must exist");
let canvas = surface.canvas();
let mut bg = skia::Paint::default();
bg.set_anti_alias(true);
bg.set_color(skia::Color::from_argb(160, 0, 0, 0));
let rect = skia::Rect::from_xywh(12.0, 12.0, 400.0, 48.0);
canvas.draw_round_rect(rect, 10.0, 10.0, &bg);
let mut paint = skia::Paint::default();
paint.set_anti_alias(true);
paint.set_color(skia::Color::WHITE);
let font = skia::Font::from_typeface(&self.font, 26.);
let text = format!("{:.1} FPS | {}", self.fps_current, self.monitor_name);
canvas.draw_str(text, (22.0, 46.0), &font, &paint);
gr.flush(None);
}
fn status(&self) -> String {
format!(
"{} | fps={:.1} | total_frames={}",
self.monitor_name, self.fps_current, self.frame_count
)
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Initializing EasyDRM...");
let mut easydrm = EasyDRM::init(MonitorContext::new)?;
easydrm.make_current()?;
let interface = gpu::gl::Interface::new_load_with(|s| easydrm.get_proc_address(s))
.expect("Failed to create shared Skia GL interface");
let mut gr = gpu::direct_contexts::make_gl(interface, None)
.expect("Failed to create shared Skia DirectContext");
println!("EasyDRM initialized successfully!");
println!("Found {} monitor(s)", easydrm.monitor_count());
for (i, monitor) in easydrm.monitors().enumerate() {
let mode = monitor.active_mode();
let ctx = monitor.context();
println!(
"Monitor {}: {}x{} @ {}Hz - {}",
i,
mode.size().0,
mode.size().1,
mode.vrefresh(),
ctx.monitor_name
);
}
println!("\nStarting render loop. Press Ctrl+C to exit.");
tokio::spawn(async {
let mut ticker = tokio::time::interval(Duration::from_millis(2));
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut last = Instant::now();
let mut worst = Duration::ZERO;
loop {
ticker.tick().await;
let now = Instant::now();
let dt = now.duration_since(last);
last = now;
if dt > worst {
worst = dt;
}
if dt > Duration::from_millis(10) {
eprintln!("[SENTINEL] late tick: {:?} (worst={:?})", dt, worst);
}
}
});
let mut global_frame_count: u64 = 0;
let mut last_print = Instant::now();
loop {
let now = Instant::now();
for monitor in easydrm.monitors_mut() {
if monitor.can_render() && monitor.make_current().is_ok() {
let mode = monitor.active_mode();
let (w, h) = (mode.size().0 as usize, mode.size().1 as usize);
let gl = monitor.gl().clone();
{
let ctx = monitor.context_mut();
ctx.ensure_surface_for_current_fbo(&mut gr, w, h);
let (r, g, b) = ctx.update_and_get_color();
ctx.maybe_update_fps(now);
unsafe {
gl.ClearColor(r, g, b, 1.0);
gl.Clear(gl::COLOR_BUFFER_BIT);
}
ctx.draw_fps_overlay(&mut gr);
}
}
}
if let Err(e) = easydrm.swap_buffers() {
eprintln!("Commit Error: {e:#?}")
}
easydrm.poll_events_async().await?;
global_frame_count += 1;
let now2 = Instant::now();
if now2.duration_since(last_print) >= Duration::from_secs(1) {
last_print = now2;
println!(
"=== status | global_frame={} | monitors={} ===",
global_frame_count,
easydrm.monitor_count()
);
for (i, monitor) in easydrm.monitors().enumerate() {
println!(" Monitor {}: {}", i, monitor.context().status());
}
println!();
}
}
}
fn create_skia_surface_for_fbo(
gr: &mut gpu::DirectContext,
fbo: u32,
width: usize,
height: usize,
) -> skia::Surface {
let fb_info = FramebufferInfo {
fboid: fbo,
format: gpu::gl::Format::RGBA8.into(),
protected: gpu::Protected::No,
};
let backend_rt = gpu::backend_render_targets::make_gl(
(width as i32, height as i32),
0,
8,
fb_info,
);
gpu::surfaces::wrap_backend_render_target(
gr,
&backend_rt,
gpu::SurfaceOrigin::TopLeft,
skia::ColorType::RGBA8888,
None,
None,
)
.expect("Failed to create Skia Surface from backend render target")
}
fn current_framebuffer(gl: &gl::Gles2) -> u32 {
let mut fbo: i32 = 0;
unsafe {
gl.GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fbo);
}
fbo as u32
}
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (f32, f32, f32) {
let c = v * s;
let h_prime = h * 6.0;
let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
let m = v - c;
let (r, g, b) = match h_prime as i32 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
5 => (c, 0.0, x),
_ => (c, x, 0.0),
};
(r + m, g + m, b + m)
}