qemu-gtk4: work in progress

This commit is contained in:
Marc-André Lureau 2021-02-12 15:54:07 +04:00
parent b0a1f355a6
commit 437b739c0e
6 changed files with 324 additions and 105 deletions

View File

@ -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"

View File

@ -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();

View File

@ -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<GLuint>,
pub texture_blit_vao: Cell<GLuint>,
pub texture_blit_prog: Cell<GLuint>,
pub texture_blit_flip_prog: Cell<GLuint>,
pub scanout: Cell<Option<Scanout>>,
pub scanout_size: Cell<(u32, u32)>,
pub tex_id: Cell<GLuint>,
pub texture: RefCell<Option<gdk::Texture>>,
}
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<Self>;
type Class = subclass::simple::ClassStruct<Self>;
@ -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: &gtk::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::<gdk_wl::WaylandDisplay>()
{
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::<gdk_x11::X11Display>() {
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<imp::QemuConsoleArea>) @extends gtk::Widget;
pub struct QemuConsoleArea(ObjectSubclass<imp::QemuConsoleArea>)
@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::<gdk::GLTexture>().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::<gdk_wl::WaylandDisplay>() {
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<GLuint, String> {
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<u8> = Vec::with_capacity(len + 1);
unsafe { CString::from_vec_unchecked(buffer) }
}
unsafe fn compile_prog(vs: &CStr, fs: &CStr) -> Result<GLuint, String> {
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)
}

28
qemu-gtk4/src/error.rs Normal file
View File

@ -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<Self>
where
Self: Sized,
{
use self::AppError::*;
match code {
x if x == GL as i32 => Some(GL),
_ => Some(Failed),
}
}
}

View File

@ -5,6 +5,7 @@ mod config;
mod console;
mod console_area;
mod egl;
mod error;
mod window;
use application::QemuApplication;

View File

@ -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(())