use alloc::{borrow::Cow, boxed::Box, format, string::String, vec, vec::Vec};
use eframe::{self, egui};
use image::{ColorType, ImageError, save_buffer};
use pollster::block_on;
use rlvgl_core::event::Key;
use std::{backtrace::Backtrace, eprintln, panic, path::Path};
use tracing_subscriber::EnvFilter;
use winit::{
dpi::{LogicalSize, PhysicalSize},
event::{ElementState, Event, KeyEvent, MouseButton, WindowEvent},
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
keyboard::{KeyCode, PhysicalKey},
window::{Fullscreen, Window},
};
use crate::input::InputEvent;
fn init_wgpu_logger() {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("wgpu=warn"));
let _ = tracing_subscriber::fmt().with_env_filter(filter).try_init();
}
fn show_panic_window(message: String) {
struct PanicApp {
msg: String,
}
impl eframe::App for PanicApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("rlvgl panic");
egui::ScrollArea::vertical().show(ui, |ui| {
ui.add(egui::TextEdit::multiline(&mut self.msg).desired_width(f32::INFINITY));
});
ui.horizontal(|ui| {
if ui.button("Copy").clicked() {
ctx.output_mut(|o| o.copied_text = self.msg.clone());
}
if ui.button("Close").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
});
}
}
let event_loop = EventLoop::new().expect("failed to create event loop");
#[allow(deprecated)]
let hidden_window = event_loop
.create_window(Window::default_attributes().with_visible(false))
.expect("failed to create window");
let monitor_size = hidden_window
.current_monitor()
.map(|m| m.size())
.unwrap_or(PhysicalSize::new(800, 600));
drop(hidden_window);
drop(event_loop);
let max = egui::vec2(monitor_size.width as f32, monitor_size.height as f32);
let initial = egui::vec2(max.x * 0.8, max.y * 0.8);
let viewport = egui::ViewportBuilder::default()
.with_inner_size(initial)
.with_max_inner_size(max)
.with_decorations(true)
.with_resizable(true);
let options = eframe::NativeOptions {
viewport,
..Default::default()
};
let msg_copy = message.clone();
if let Err(e) = eframe::run_native(
"rlvgl panic",
options,
Box::new(|_| Box::new(PanicApp { msg: message })),
) {
eprintln!("{msg_copy}\nfailed to show panic window: {e}");
}
}
struct WgpuState {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
frame: Vec<u8>,
fb_width: u32,
fb_height: u32,
blit_buf: Vec<u8>,
max_texture_dim: u32,
}
impl WgpuState {
fn new(
window: &'static Window,
fb_width: u32,
fb_height: u32,
surface_width: u32,
surface_height: u32,
) -> Self {
init_wgpu_logger();
let instance = wgpu::Instance::default();
let surface = instance
.create_surface(window)
.expect("failed to create surface");
let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
}))
.expect("failed to find adapter");
let (device, queue) = block_on(adapter.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
},
None,
))
.expect("failed to create device");
device.on_uncaptured_error(Box::new(|e| {
eprintln!("wgpu uncaptured error: {e:?}");
debug_assert!(false, "wgpu uncaptured error: {e:?}");
}));
let caps = surface.get_capabilities(&adapter);
let format = caps
.formats
.iter()
.copied()
.find(|f| f.is_srgb())
.unwrap_or(caps.formats[0]);
let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Fifo) {
wgpu::PresentMode::Fifo
} else {
caps.present_modes[0]
};
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width: surface_width,
height: surface_height,
present_mode,
alpha_mode: caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 1,
};
surface.configure(&device, &config);
eprintln!("Surface format: {:?}", config.format);
let max_texture_dim = device.limits().max_texture_dimension_2d;
let frame = vec![0; (fb_width * fb_height * 4) as usize];
Self {
surface,
device,
queue,
config,
frame,
fb_width,
fb_height,
blit_buf: Vec::new(),
max_texture_dim,
}
}
fn frame_mut(&mut self) -> &mut [u8] {
&mut self.frame
}
fn resize(&mut self, width: u32, height: u32) {
self.config.width = width;
self.config.height = height;
self.surface.configure(&self.device, &self.config);
}
fn render(&mut self, dst_width: u32, dst_height: u32, offset_x: u32, offset_y: u32) {
let output = match self.surface.get_current_texture() {
Ok(frame) => frame,
Err(e) => {
eprintln!("surface error: {e:?}");
debug_assert!(false, "surface error: {e:?}");
self.surface.configure(&self.device, &self.config);
self.surface
.get_current_texture()
.expect("failed to acquire surface texture")
}
};
let surface_w = self.config.width;
let surface_h = self.config.height;
let required = (surface_w * surface_h * 4) as usize;
if self.blit_buf.len() != required {
self.blit_buf.resize(required, 0);
for px in self.blit_buf.chunks_exact_mut(4) {
px[3] = 0xff;
}
} else {
for px in self.blit_buf.chunks_exact_mut(4) {
px[0] = 0;
px[1] = 0;
px[2] = 0;
px[3] = 0xff;
}
}
for y in 0..dst_height.min(surface_h) {
let src_y = y * self.fb_height / dst_height;
for x in 0..dst_width.min(surface_w) {
let src_x = x * self.fb_width / dst_width;
let src_idx = ((src_y * self.fb_width + src_x) * 4) as usize;
let dst_x = x + offset_x;
let dst_y = y + offset_y;
if dst_x < surface_w && dst_y < surface_h {
let dst_idx = ((dst_y * surface_w + dst_x) * 4) as usize;
self.blit_buf[dst_idx..dst_idx + 4]
.copy_from_slice(&self.frame[src_idx..src_idx + 4]);
}
}
}
let data: Cow<[u8]> = match self.config.format {
wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb => {
for px in self.blit_buf.chunks_exact_mut(4) {
px.swap(0, 2);
}
Cow::Borrowed(&self.blit_buf)
}
_ => Cow::Borrowed(&self.blit_buf),
};
let data = data.as_ref();
let row_bytes = 4 * surface_w as usize;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
if row_bytes.is_multiple_of(align) {
self.queue.write_texture(
wgpu::ImageCopyTexture {
texture: &output.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
data,
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(row_bytes as u32),
rows_per_image: Some(surface_h),
},
wgpu::Extent3d {
width: surface_w,
height: surface_h,
depth_or_array_layers: 1,
},
);
} else {
let stride = row_bytes.div_ceil(align) * align;
let mut padded = vec![0u8; stride * surface_h as usize];
for y in 0..surface_h as usize {
let src_off = y * row_bytes;
let dst_off = y * stride;
padded[dst_off..dst_off + row_bytes]
.copy_from_slice(&data[src_off..src_off + row_bytes]);
}
self.queue.write_texture(
wgpu::ImageCopyTexture {
texture: &output.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&padded,
wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(stride as u32),
rows_per_image: Some(surface_h),
},
wgpu::Extent3d {
width: surface_w,
height: surface_h,
depth_or_array_layers: 1,
},
);
}
self.queue.submit(std::iter::empty());
output.present();
}
fn max_texture_dimension(&self) -> u32 {
self.max_texture_dim
}
}
pub struct WgpuDisplay {
width: usize,
height: usize,
event_loop: EventLoop<()>,
window: &'static Window,
state: WgpuState,
scale: (f64, f64),
present_scale: (f64, f64),
dest_size: (u32, u32),
surface_offset: (f64, f64),
}
impl WgpuDisplay {
pub fn new(width: usize, height: usize) -> Self {
panic::set_hook(Box::new(|info| {
let backtrace = Backtrace::force_capture();
let message = format!("{info}\n\n{backtrace}");
show_panic_window(message);
std::process::exit(1);
}));
let event_loop = EventLoop::new().expect("failed to create event loop");
#[allow(deprecated)]
let window = event_loop
.create_window(
Window::default_attributes()
.with_title("rlvgl simulator")
.with_inner_size(LogicalSize::new(width as f64, height as f64)),
)
.expect("failed to create window");
let window = Box::leak(Box::new(window));
let phys = window.inner_size();
let state = WgpuState::new(window, width as u32, height as u32, phys.width, phys.height);
let window: &'static Window = &*window;
let scale = (
phys.width as f64 / width as f64,
phys.height as f64 / height as f64,
);
Self {
width,
height,
event_loop,
window,
state,
scale,
present_scale: (1.0, 1.0),
dest_size: (phys.width, phys.height),
surface_offset: (0.0, 0.0),
}
}
pub fn run(
self,
mut frame_callback: impl FnMut(&mut [u8], usize, usize) + 'static,
mut event_callback: impl FnMut(InputEvent) + 'static,
) {
let WgpuDisplay {
width,
height,
event_loop,
window,
mut state,
scale: initial_scale,
present_scale: initial_present_scale,
mut dest_size,
mut surface_offset,
} = self;
event_loop.set_control_flow(ControlFlow::Poll);
window.request_redraw();
let mut pointer_pos = (0.0f64, 0.0f64);
let mut pointer_down = false;
let mut scale = initial_scale;
let mut present_scale = initial_present_scale;
let aspect_ratio = width as f64 / height as f64;
let max_dim = state.max_texture_dimension();
let mut fullscreen = false;
fn key_from_event(event: &KeyEvent) -> Key {
if let Some(text) = &event.text
&& let Some(ch) = text.chars().next()
{
return Key::Character(ch);
}
match event.physical_key {
PhysicalKey::Code(code) => match code {
KeyCode::Escape => Key::Escape,
KeyCode::Enter => Key::Enter,
KeyCode::Space => Key::Space,
KeyCode::ArrowUp => Key::ArrowUp,
KeyCode::ArrowDown => Key::ArrowDown,
KeyCode::ArrowLeft => Key::ArrowLeft,
KeyCode::ArrowRight => Key::ArrowRight,
KeyCode::F1 => Key::Function(1),
KeyCode::F2 => Key::Function(2),
KeyCode::F3 => Key::Function(3),
KeyCode::F4 => Key::Function(4),
KeyCode::F5 => Key::Function(5),
KeyCode::F6 => Key::Function(6),
KeyCode::F7 => Key::Function(7),
KeyCode::F8 => Key::Function(8),
KeyCode::F9 => Key::Function(9),
KeyCode::F10 => Key::Function(10),
KeyCode::F11 => Key::Function(11),
KeyCode::F12 => Key::Function(12),
_ => Key::Other(code as u32),
},
_ => Key::Other(0),
}
}
#[allow(deprecated)]
event_loop
.run(move |event, target: &ActiveEventLoop| match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => target.exit(),
Event::WindowEvent {
event: WindowEvent::RedrawRequested,
..
} => {
frame_callback(state.frame_mut(), width, height);
state.render(
dest_size.0,
dest_size.1,
surface_offset.0.round() as u32,
surface_offset.1.round() as u32,
);
}
Event::WindowEvent {
event: WindowEvent::Resized(size),
..
} => {
let surf_w = size.width.min(max_dim);
let surf_h = size.height.min(max_dim);
state.resize(surf_w, surf_h);
let mut w = surf_w;
let mut h = surf_h;
if (w as f64 / h as f64 - aspect_ratio).abs() > f64::EPSILON {
if w as f64 / h as f64 > aspect_ratio {
w = (h as f64 * aspect_ratio).round() as u32;
} else {
h = (w as f64 / aspect_ratio).round() as u32;
}
}
let logical = (
(pointer_pos.0 - surface_offset.0) / scale.0,
(pointer_pos.1 - surface_offset.1) / scale.1,
);
surface_offset = (
(surf_w as f64 - w as f64) / 2.0,
(surf_h as f64 - h as f64) / 2.0,
);
dest_size = (w, h);
present_scale = (
size.width as f64 / surf_w as f64,
size.height as f64 / surf_h as f64,
);
scale = (w as f64 / width as f64, h as f64 / height as f64);
pointer_pos = (
logical.0 * scale.0 + surface_offset.0,
logical.1 * scale.1 + surface_offset.1,
);
window.request_redraw();
}
Event::WindowEvent {
event: WindowEvent::KeyboardInput { event, .. },
..
} => {
if let PhysicalKey::Code(code) = event.physical_key {
if code == KeyCode::F11 && event.state == ElementState::Pressed {
fullscreen = !fullscreen;
if fullscreen {
window.set_fullscreen(Some(Fullscreen::Borderless(None)));
} else {
window.set_fullscreen(None);
}
}
let key = key_from_event(&event);
match event.state {
ElementState::Pressed => {
event_callback(InputEvent::KeyDown { key });
}
ElementState::Released => {
event_callback(InputEvent::KeyUp { key });
}
}
}
}
Event::WindowEvent {
event: WindowEvent::CursorMoved { position, .. },
..
} => {
let surf_x = position.x / present_scale.0;
let surf_y = position.y / present_scale.1;
pointer_pos = (surf_x, surf_y);
let adj_x = surf_x - surface_offset.0;
let adj_y = surf_y - surface_offset.1;
let x = (adj_x / scale.0).clamp(0.0, width as f64 - 1.0) as i32;
let y = (adj_y / scale.1).clamp(0.0, height as f64 - 1.0) as i32;
if pointer_down {
event_callback(InputEvent::PointerMove { x, y });
}
}
Event::WindowEvent {
event:
WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Left,
..
},
..
} => {
pointer_down = true;
let adj_x = pointer_pos.0 - surface_offset.0;
let adj_y = pointer_pos.1 - surface_offset.1;
let x = (adj_x / scale.0).clamp(0.0, width as f64 - 1.0) as i32;
let y = (adj_y / scale.1).clamp(0.0, height as f64 - 1.0) as i32;
event_callback(InputEvent::PointerDown { x, y });
}
Event::WindowEvent {
event:
WindowEvent::MouseInput {
state: ElementState::Released,
button: MouseButton::Left,
..
},
..
} => {
let adj_x = pointer_pos.0 - surface_offset.0;
let adj_y = pointer_pos.1 - surface_offset.1;
let x = (adj_x / scale.0).clamp(0.0, width as f64 - 1.0) as i32;
let y = (adj_y / scale.1).clamp(0.0, height as f64 - 1.0) as i32;
pointer_down = false;
event_callback(InputEvent::PointerUp { x, y });
}
Event::AboutToWait => {
window.request_redraw();
}
_ => {}
})
.expect("event loop error");
}
pub fn headless(
width: usize,
height: usize,
mut frame_callback: impl FnMut(&mut [u8]),
path: impl AsRef<Path>,
) -> Result<(), ImageError> {
let mut frame = vec![0u8; width * height * 4];
frame_callback(&mut frame);
save_buffer(path, &frame, width as u32, height as u32, ColorType::Rgba8)
}
}