diff --git a/qemu-gtk4/Cargo.toml b/qemu-gtk4/Cargo.toml index 906632e..2eae256 100644 --- a/qemu-gtk4/Cargo.toml +++ b/qemu-gtk4/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" [dependencies] qemu-display-listener = { path = "../qemu-display-listener", features = ["glib"] } +keycodemap = { path = "../keycodemap" } zbus = { version = "2.0.0-beta" } log = "0.4" pretty_env_logger = "0.4" @@ -15,6 +16,7 @@ once_cell = "1.5" khronos-egl = { version = "3.0.0", features = ["dynamic"] } libloading = "0.6" gl = "0.14.0" +glib = { git = "https://github.com/gtk-rs/gtk-rs", optional = true } [dependencies.gtk] package = "gtk4" @@ -26,5 +28,7 @@ package = "gdk4-wayland" git = "https://github.com/gtk-rs/gtk4-rs" rev = "c43025157b12dba1112fad55962966769908a269" -[build-dependencies] -gl_generator = "0.5.0" +[dependencies.gdk-x11] +package = "gdk4-x11" +git = "https://github.com/gtk-rs/gtk4-rs" +rev = "c43025157b12dba1112fad55962966769908a269" diff --git a/qemu-gtk4/src/console.rs b/qemu-gtk4/src/console.rs index 7c427b8..9948c17 100644 --- a/qemu-gtk4/src/console.rs +++ b/qemu-gtk4/src/console.rs @@ -5,6 +5,7 @@ use gtk::subclass::widget::WidgetImplExt; use gtk::{glib, CompositeTemplate}; use once_cell::sync::OnceCell; +use keycodemap::*; use qemu_display_listener::{Console, Event, MouseButton}; mod imp { @@ -54,12 +55,16 @@ mod imp { self.area.add_controller(&ec); ec.connect_key_pressed(clone!(@weak obj => move |_, _keyval, keycode, _state| { let c = obj.qemu_console(); - let _ = c.keyboard.press(keycode); + if let Some(qnum) = KEYMAP_XORGEVDEV2QNUM.get(keycode as usize) { + let _ = c.keyboard.press(*qnum as u32); + } glib::signal::Inhibit(true) })); ec.connect_key_released(clone!(@weak obj => move |_, _keyval, keycode, _state| { let c = obj.qemu_console(); - let _ = c.keyboard.release(keycode); + if let Some(qnum) = KEYMAP_XORGEVDEV2QNUM.get(keycode as usize) { + let _ = c.keyboard.release(*qnum as u32); + } })); let ec = gtk::EventControllerMotion::new(); @@ -121,6 +126,9 @@ impl QemuConsole { clone!(@weak self as con => move |t| { let con = imp::QemuConsole::from_instance(&con); match t { + Event::Update { .. } => { + con.area.queue_render(); + } Event::Scanout(s) => { con.label.set_label(&format!("{:?}", s)); con.area.set_scanout(s); @@ -144,16 +152,10 @@ impl QemuConsole { fn motion(&self, x: f64, y: f64) { let priv_ = imp::QemuConsole::from_instance(self); - // FIXME: scaling, centering etc.. - let widget_w = self.get_width(); - let widget_h = self.get_height(); - let _widget_scale = self.get_scale_factor(); - - let c = self.qemu_console(); - // FIXME: ideally, we would use ConsoleProxy cached properties instead - let x = (x / widget_w as f64) * priv_.area.scanout_size().0 as f64; - let y = (y / widget_h as f64) * priv_.area.scanout_size().1 as f64; - let _ = c.mouse.set_abs_position(x as u32, y as u32); + if let Some((x, y)) = priv_.area.transform_input(x, y) { + let c = self.qemu_console(); + let _ = c.mouse.set_abs_position(x, y); + } // FIXME: focus on click doesn't work priv_.area.grab_focus(); diff --git a/qemu-gtk4/src/console_area.rs b/qemu-gtk4/src/console_area.rs index 87b787c..9642a69 100644 --- a/qemu-gtk4/src/console_area.rs +++ b/qemu-gtk4/src/console_area.rs @@ -1,10 +1,14 @@ use gdk_wl::WaylandDisplayManualExt; use glib::subclass::prelude::*; +use glib::translate::*; use gtk::prelude::*; -use gtk::{gdk, glib, graphene}; -use std::cell::{Cell, RefCell}; +use gtk::subclass::widget::WidgetImplExt; +use gtk::{gdk, glib}; +use std::cell::Cell; +use std::ffi::{CStr, CString}; use crate::egl; +use crate::error::*; use gl::{self, types::*}; use qemu_display_listener::Scanout; @@ -14,16 +18,18 @@ mod imp { use gtk::subclass::prelude::*; pub struct QemuConsoleArea { + pub tex_id: Cell, + pub texture_blit_vao: Cell, + pub texture_blit_prog: Cell, + pub texture_blit_flip_prog: Cell, pub scanout: Cell>, pub scanout_size: Cell<(u32, u32)>, - pub tex_id: Cell, - pub texture: RefCell>, } impl ObjectSubclass for QemuConsoleArea { const NAME: &'static str = "QemuConsoleArea"; type Type = super::QemuConsoleArea; - type ParentType = gtk::Widget; + type ParentType = gtk::GLArea; type Interfaces = (); type Instance = subclass::simple::InstanceStruct; type Class = subclass::simple::ClassStruct; @@ -32,10 +38,12 @@ mod imp { fn new() -> Self { Self { + tex_id: Cell::new(0), + texture_blit_vao: Cell::new(0), + texture_blit_prog: Cell::new(0), + texture_blit_flip_prog: Cell::new(0), scanout: Cell::new(None), scanout_size: Cell::new((0, 0)), - tex_id: Cell::new(0), - texture: RefCell::new(None), } } @@ -58,45 +66,214 @@ mod imp { } impl WidgetImpl for QemuConsoleArea { - fn snapshot(&self, widget: &Self::Type, snapshot: >k::Snapshot) { - let (width, height) = (widget.get_width() as f32, widget.get_height() as f32); - let whole = &graphene::Rect::new(0_f32, 0_f32, width, height); - // TODO: make this a CSS style? - //snapshot.append_color(&gdk::RGBA::black(), whole); - if let Some(texture) = &*self.texture.borrow() { - snapshot.append_texture(texture, whole); + fn realize(&self, widget: &Self::Type) { + widget.set_has_depth_buffer(false); + widget.set_has_stencil_buffer(false); + widget.set_auto_render(false); + widget.set_required_version(3, 2); + self.parent_realize(widget); + widget.make_current(); + + if let Err(e) = unsafe { self.realize_gl() } { + let e = glib::Error::new(AppError::GL, &format!("{}", e)); + widget.set_error(Some(&e)); + } + } + } + + impl GLAreaImpl for QemuConsoleArea { + fn render(&self, gl_area: &Self::Type, _context: &gdk::GLContext) -> bool { + unsafe { + gl::ClearColor(0.1, 0.1, 0.1, 1.0); + gl::Clear(gl::COLOR_BUFFER_BIT); + gl::Disable(gl::BLEND); + + let vp = self.viewport(gl_area); + gl::Viewport(vp.x, vp.y, vp.width, vp.height); + self.texture_blit(false); + } + return true; /* FIXME: Inibit */ + } + } + + impl QemuConsoleArea { + pub fn borders(&self, gl_area: &super::QemuConsoleArea) -> (u32, u32) { + let sf = gl_area.get_scale_factor(); + let (w, h) = (gl_area.get_width() * sf, gl_area.get_height() * sf); + let (gw, gh) = gl_area.scanout_size(); + let (sw, sh) = (w as f32 / gw as f32, h as f32 / gh as f32); + + if sw < sh { + let bh = h - (h as f32 * sw / sh) as i32; + (0, bh as u32 / 2) + } else { + let bw = w - (w as f32 * sh / sw) as i32; + (bw as u32 / 2, 0) + } + } + + pub fn viewport(&self, gl_area: &super::QemuConsoleArea) -> gdk::Rectangle { + let sf = gl_area.get_scale_factor(); + let (w, h) = (gl_area.get_width() * sf, gl_area.get_height() * sf); + let (borderw, borderh) = self.borders(gl_area); + let (borderw, borderh) = (borderw as i32, borderh as i32); + gdk::Rectangle { + x: borderw, + y: borderh, + width: w - borderw * 2, + height: h - borderh * 2, + } + } + + unsafe fn realize_gl(&self) -> Result<(), String> { + let texture_blit_vs = CString::new(include_str!("texture-blit.vert")).unwrap(); + let texture_blit_flip_vs = + CString::new(include_str!("texture-blit-flip.vert")).unwrap(); + let texture_blit_fs = CString::new(include_str!("texture-blit.frag")).unwrap(); + + let texture_blit_prg = + compile_prog(texture_blit_vs.as_c_str(), texture_blit_fs.as_c_str())?; + self.texture_blit_prog.set(texture_blit_prg); + let texture_blit_flip_prg = + compile_prog(texture_blit_flip_vs.as_c_str(), texture_blit_fs.as_c_str())?; + self.texture_blit_flip_prog.set(texture_blit_flip_prg); + + let mut vao = 0; + gl::GenVertexArrays(1, &mut vao); + gl::BindVertexArray(vao); + let mut vb = 0; + gl::GenBuffers(1, &mut vb); + gl::BindBuffer(gl::ARRAY_BUFFER, vb); + static POS: [f32; 8] = [-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0]; + gl::BufferData( + gl::ARRAY_BUFFER, + std::mem::size_of::<[f32; 8]>() as _, + POS.as_ptr() as _, + gl::STATIC_DRAW, + ); + let in_pos = gl::GetAttribLocation( + texture_blit_prg, + CString::new("in_position").unwrap().as_c_str().as_ptr(), + ) as u32; + gl::VertexAttribPointer(in_pos, 2, gl::FLOAT, gl::FALSE, 0, std::ptr::null()); + gl::EnableVertexAttribArray(in_pos); + gl::BindBuffer(gl::ARRAY_BUFFER, 0); + gl::BindVertexArray(0); + self.texture_blit_vao.set(vao); + + let tex_unit = gl::GetUniformLocation( + texture_blit_prg, + CString::new("tex_unit").unwrap().as_c_str().as_ptr(), + ); + gl::ProgramUniform1i(texture_blit_prg, tex_unit, 0); + + let mut tex_id = 0; + gl::GenTextures(1, &mut tex_id); + self.tex_id.set(tex_id); + + Ok(()) + } + + unsafe fn texture_blit(&self, flip: bool) { + gl::UseProgram(if flip { + todo!(); + //self.texture_blit_flip_prog.get() + } else { + self.texture_blit_prog.get() + }); + gl::ActiveTexture(gl::TEXTURE0); + gl::BindTexture(gl::TEXTURE_2D, self.tex_id()); + gl::BindVertexArray(self.texture_blit_vao.get()); + gl::DrawArrays(gl::TRIANGLE_STRIP, 0, 4); + } + + pub fn tex_id(&self) -> GLuint { + self.tex_id.get() + } + + pub fn save_to_png(&self, widget: &super::QemuConsoleArea, filename: &str) { + let (gw, gh) = self.scanout_size.get(); + let ctxt = widget.get_context().unwrap(); + let tex = unsafe { gdk::GLTexture::new(&ctxt, self.tex_id(), gw as _, gh as _) }; + tex.save_to_png(filename); + } + + pub fn set_scanout(&self, widget: &super::QemuConsoleArea, s: Scanout) { + widget.make_current(); + let egl = egl::egl(); + + let egl_dpy = if let Ok(dpy) = widget.get_display().downcast::() + { + let wl_dpy = dpy.get_wl_display(); + egl.get_display(wl_dpy.as_ref().c_ptr() as _) + .expect("Failed to get EGL display") + } else if let Ok(dpy) = widget.get_display().downcast::() { + let _dpy = + unsafe { gdk_x11::ffi::gdk_x11_display_get_xdisplay(dpy.to_glib_none().0) }; + eprintln!("X11: unsupported display kind, todo: EGL"); + return; + } else { + eprintln!("Unsupported display kind"); + return; + }; + + let attribs = vec![ + egl::WIDTH as usize, + s.width as usize, + egl::HEIGHT as usize, + s.height as usize, + egl::LINUX_DRM_FOURCC_EXT as usize, + s.fourcc as usize, + egl::DMA_BUF_PLANE0_FD_EXT as usize, + s.fd as usize, + egl::DMA_BUF_PLANE0_PITCH_EXT as usize, + s.stride as usize, + egl::DMA_BUF_PLANE0_OFFSET_EXT as usize, + 0, + egl::DMA_BUF_PLANE0_MODIFIER_LO_EXT as usize, + (s.modifier & 0xffffffff) as usize, + egl::DMA_BUF_PLANE0_MODIFIER_HI_EXT as usize, + (s.modifier >> 32 & 0xffffffff) as usize, + egl::NONE as usize, + ]; + + let img = egl + .create_image( + egl_dpy, + unsafe { egl::Context::from_ptr(egl::NO_CONTEXT) }, + egl::LINUX_DMA_BUF_EXT, + unsafe { egl::ClientBuffer::from_ptr(std::ptr::null_mut()) }, + &attribs, + ) + .expect("Failed to eglCreateImage"); + + unsafe { + gl::BindTexture(gl::TEXTURE_2D, self.tex_id()); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::NEAREST as _); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as _); + } + if let Some(image_target) = egl::image_target_texture_2d_oes() { + image_target(gl::TEXTURE_2D, img.as_ptr() as gl::types::GLeglImageOES); + } else { + eprintln!("Failed to set texture image"); + } + + self.scanout_size.set((s.width, s.height)); + self.scanout.set(Some(s)); + + if let Err(e) = egl.destroy_image(egl_dpy, img) { + eprintln!("Destroy image failed: {}", e); } } } } glib::wrapper! { - pub struct QemuConsoleArea(ObjectSubclass) @extends gtk::Widget; + pub struct QemuConsoleArea(ObjectSubclass) + @extends gtk::Widget, gtk::GLArea; } impl QemuConsoleArea { - pub fn tex_id(&self) -> GLuint { - let priv_ = imp::QemuConsoleArea::from_instance(self); - let mut tex_id = priv_.tex_id.get(); - if tex_id == 0 { - unsafe { gl::GenTextures(1, &mut tex_id) } - priv_.tex_id.set(tex_id); - } - tex_id - } - - fn update_texture(&self, s: &Scanout) { - let priv_ = imp::QemuConsoleArea::from_instance(self); - let ctxt = gdk::GLContext::get_current().unwrap(); - let tex = - unsafe { gdk::GLTexture::new(&ctxt, self.tex_id(), s.width as i32, s.height as i32) }; - - //tex.save_to_png("/tmp/tex.png"); - //tex.clone().downcast::().unwrap().release(); - tex.release(); - *priv_.texture.borrow_mut() = Some(tex.upcast()); - } - pub fn scanout_size(&self) -> (u32, u32) { let priv_ = imp::QemuConsoleArea::from_instance(self); @@ -105,63 +282,68 @@ impl QemuConsoleArea { pub fn set_scanout(&self, s: Scanout) { let priv_ = imp::QemuConsoleArea::from_instance(self); - let egl = egl::egl(); - let egl_dpy = if let Ok(dpy) = self.get_display().downcast::() { - let wl_dpy = dpy.get_wl_display(); - egl.get_display(wl_dpy.as_ref().c_ptr() as _) - .expect("Failed to get EGL display") - } else { - eprintln!("Unsupported display kind"); - return; - }; + priv_.set_scanout(self, s); + } - let attribs = vec![ - egl::WIDTH as usize, - s.width as usize, - egl::HEIGHT as usize, - s.height as usize, - egl::LINUX_DRM_FOURCC_EXT as usize, - s.fourcc as usize, - egl::DMA_BUF_PLANE0_FD_EXT as usize, - s.fd as usize, - egl::DMA_BUF_PLANE0_PITCH_EXT as usize, - s.stride as usize, - egl::DMA_BUF_PLANE0_OFFSET_EXT as usize, - 0, - egl::DMA_BUF_PLANE0_MODIFIER_LO_EXT as usize, - (s.modifier & 0xffffffff) as usize, - egl::DMA_BUF_PLANE0_MODIFIER_HI_EXT as usize, - (s.modifier >> 32 & 0xffffffff) as usize, - egl::NONE as usize, - ]; + pub fn save_to_png(&self, filename: &str) { + let priv_ = imp::QemuConsoleArea::from_instance(self); - let img = egl - .create_image( - egl_dpy, - unsafe { egl::Context::from_ptr(egl::NO_CONTEXT) }, - egl::LINUX_DMA_BUF_EXT, - unsafe { egl::ClientBuffer::from_ptr(std::ptr::null_mut()) }, - &attribs, - ) - .expect("Failed to eglCreateImage"); + priv_.save_to_png(self, filename); + } - let tex_id = self.tex_id(); - unsafe { gl::BindTexture(gl::TEXTURE_2D, tex_id) } - if let Some(image_target) = egl::image_target_texture_2d_oes() { - image_target(gl::TEXTURE_2D, img.as_ptr() as gl::types::GLeglImageOES); - } else { - eprintln!("Failed to set texture image"); + pub fn transform_input(&self, x: f64, y: f64) -> Option<(u32, u32)> { + let priv_ = imp::QemuConsoleArea::from_instance(self); + + let vp = priv_.viewport(self); + let x = x as i32 * self.get_scale_factor(); + let y = y as i32 * self.get_scale_factor(); + if !vp.contains_point(x, y) { + return None; } - - self.update_texture(&s); - self.queue_draw(); - - if let Err(e) = egl.destroy_image(egl_dpy, img) { - eprintln!("Destroy image failed: {}", e); - } - - priv_.scanout_size.set((s.width, s.height)); - priv_.scanout.set(Some(s)); + let (sw, sh) = priv_.scanout_size.get(); + let x = (x - vp.x) as f64 * (sw as f64 / vp.width as f64); + let y = (y - vp.y) as f64 * (sh as f64 / vp.height as f64); + Some((x as u32, y as u32)) } } + +unsafe fn compile_shader(type_: GLenum, src: &CStr) -> Result { + let shader = gl::CreateShader(type_); + gl::ShaderSource(shader, 1, &src.as_ptr(), std::ptr::null()); + gl::CompileShader(shader); + Ok(shader) +} + +fn cstring_new_len(len: usize) -> CString { + let buffer: Vec = Vec::with_capacity(len + 1); + unsafe { CString::from_vec_unchecked(buffer) } +} + +unsafe fn compile_prog(vs: &CStr, fs: &CStr) -> Result { + let vs = compile_shader(gl::VERTEX_SHADER, vs)?; + let fs = compile_shader(gl::FRAGMENT_SHADER, fs)?; + let prog = gl::CreateProgram(); + + gl::AttachShader(prog, vs); + gl::AttachShader(prog, fs); + gl::LinkProgram(prog); + + let mut status: i32 = 0; + gl::GetProgramiv(prog, gl::LINK_STATUS, &mut status); + if status == 0 { + let mut len: GLint = 0; + gl::GetProgramiv(prog, gl::INFO_LOG_LENGTH, &mut len); + let error = cstring_new_len(len as usize); + gl::GetProgramInfoLog( + prog, + len, + std::ptr::null_mut(), + error.as_ptr() as *mut gl::types::GLchar, + ); + return Err(error.to_string_lossy().into_owned()); + } + gl::DeleteShader(vs); + gl::DeleteShader(fs); + Ok(prog) +} diff --git a/qemu-gtk4/src/error.rs b/qemu-gtk4/src/error.rs new file mode 100644 index 0000000..1febdc6 --- /dev/null +++ b/qemu-gtk4/src/error.rs @@ -0,0 +1,28 @@ +use gtk::glib; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AppError { + GL = 1, + Failed = 2, +} + +impl glib::error::ErrorDomain for AppError { + fn domain() -> glib::Quark { + glib::Quark::from_string("qemu-gtk4") + } + + fn code(self) -> i32 { + self as _ + } + + fn from(code: i32) -> Option + where + Self: Sized, + { + use self::AppError::*; + match code { + x if x == GL as i32 => Some(GL), + _ => Some(Failed), + } + } +} diff --git a/qemu-gtk4/src/main.rs b/qemu-gtk4/src/main.rs index cb9372b..bde65b3 100644 --- a/qemu-gtk4/src/main.rs +++ b/qemu-gtk4/src/main.rs @@ -5,6 +5,7 @@ mod config; mod console; mod console_area; mod egl; +mod error; mod window; use application::QemuApplication; diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 18d510d..50ad9cf 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -55,7 +55,9 @@ fn codegen() -> Result<(), DynError> { ]; for km in &keymaps { let varname = format!("keymap_{}2qnum", km); - let out = cmd!("{keymap_gen} code-map --lang rust --varname {varname} {keymaps_csv} {km} qnum").read()?; + let out = + cmd!("{keymap_gen} code-map --lang rust --varname {varname} {keymaps_csv} {km} qnum") + .read()?; write_file(keycodemap_src.join(format!("{}.rs", varname)), out)?; } Ok(())