443 lines
15 KiB
Rust
443 lines
15 KiB
Rust
use gdk_wl::WaylandDisplayManualExt;
|
|
use glib::subclass::prelude::*;
|
|
use glib::translate::*;
|
|
use gtk::prelude::*;
|
|
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, ScanoutDMABUF, Update};
|
|
|
|
mod imp {
|
|
use super::*;
|
|
use glib::subclass;
|
|
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<ScanoutDMABUF>>,
|
|
pub scanout_size: Cell<(u32, u32)>,
|
|
}
|
|
|
|
impl ObjectSubclass for QemuConsoleArea {
|
|
const NAME: &'static str = "QemuConsoleArea";
|
|
type Type = super::QemuConsoleArea;
|
|
type ParentType = gtk::GLArea;
|
|
type Interfaces = ();
|
|
type Instance = subclass::simple::InstanceStruct<Self>;
|
|
type Class = subclass::simple::ClassStruct<Self>;
|
|
|
|
glib::object_subclass!();
|
|
|
|
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)),
|
|
}
|
|
}
|
|
|
|
fn class_init(_klass: &mut Self::Class) {
|
|
// GL loading could be done earlier?
|
|
let egl = egl::egl();
|
|
|
|
gl::load_with(|s| {
|
|
egl.get_proc_address(s)
|
|
.map(|f| f as _)
|
|
.unwrap_or(std::ptr::null())
|
|
});
|
|
}
|
|
}
|
|
|
|
impl ObjectImpl for QemuConsoleArea {
|
|
fn constructed(&self, obj: &Self::Type) {
|
|
self.parent_constructed(obj);
|
|
}
|
|
|
|
fn properties() -> &'static [glib::ParamSpec] {
|
|
use once_cell::sync::Lazy;
|
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
|
vec![glib::ParamSpec::boolean(
|
|
"resize-hack",
|
|
"resize-hack",
|
|
"Resize hack to notify parent",
|
|
false,
|
|
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT,
|
|
)]
|
|
});
|
|
PROPERTIES.as_ref()
|
|
}
|
|
|
|
fn set_property(
|
|
&self,
|
|
_obj: &Self::Type,
|
|
_id: usize,
|
|
_value: &glib::Value,
|
|
_pspec: &glib::ParamSpec,
|
|
) {
|
|
}
|
|
}
|
|
|
|
impl WidgetImpl for QemuConsoleArea {
|
|
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, &e);
|
|
widget.set_error(Some(&e));
|
|
}
|
|
}
|
|
|
|
fn size_allocate(&self, widget: &Self::Type, width: i32, height: i32, baseline: i32) {
|
|
self.parent_size_allocate(widget, width, height, baseline);
|
|
widget.notify("resize-hack");
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
// parent will return to update call
|
|
false
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
if s.format != 0x20020888 {
|
|
todo!();
|
|
}
|
|
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 _);
|
|
gl::PixelStorei(gl::UNPACK_ROW_LENGTH, s.stride as i32 / 4);
|
|
gl::TexImage2D(
|
|
gl::TEXTURE_2D,
|
|
0,
|
|
gl::RGB as _,
|
|
s.width as _,
|
|
s.height as _,
|
|
0,
|
|
gl::BGRA,
|
|
gl::UNSIGNED_BYTE,
|
|
s.data.as_ptr() as _,
|
|
);
|
|
}
|
|
|
|
self.scanout_size.set((s.width, s.height));
|
|
}
|
|
|
|
pub fn update(&self, widget: &super::QemuConsoleArea, u: Update) {
|
|
widget.make_current();
|
|
|
|
if u.format != 0x20020888 {
|
|
todo!();
|
|
}
|
|
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 _);
|
|
gl::PixelStorei(gl::UNPACK_ROW_LENGTH, u.stride as i32 / 4);
|
|
gl::TexSubImage2D(
|
|
gl::TEXTURE_2D,
|
|
0,
|
|
u.x,
|
|
u.y,
|
|
u.w,
|
|
u.h,
|
|
gl::BGRA,
|
|
gl::UNSIGNED_BYTE,
|
|
u.data.as_ptr() as _,
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn set_scanout_dmabuf(&self, widget: &super::QemuConsoleArea, s: ScanoutDMABUF) {
|
|
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, gtk::GLArea;
|
|
}
|
|
|
|
impl QemuConsoleArea {
|
|
pub fn scanout_size(&self) -> (u32, u32) {
|
|
let priv_ = imp::QemuConsoleArea::from_instance(self);
|
|
|
|
priv_.scanout_size.get()
|
|
}
|
|
|
|
pub fn set_scanout(&self, s: Scanout) {
|
|
let priv_ = imp::QemuConsoleArea::from_instance(self);
|
|
|
|
priv_.set_scanout(self, s);
|
|
}
|
|
|
|
pub fn update(&self, u: Update) {
|
|
let priv_ = imp::QemuConsoleArea::from_instance(self);
|
|
|
|
priv_.update(self, u);
|
|
}
|
|
|
|
pub fn set_scanout_dmabuf(&self, s: ScanoutDMABUF) {
|
|
let priv_ = imp::QemuConsoleArea::from_instance(self);
|
|
|
|
priv_.set_scanout_dmabuf(self, s);
|
|
}
|
|
|
|
pub fn save_to_png(&self, filename: &str) {
|
|
let priv_ = imp::QemuConsoleArea::from_instance(self);
|
|
|
|
priv_.save_to_png(self, filename);
|
|
}
|
|
|
|
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;
|
|
}
|
|
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) -> GLuint {
|
|
let shader = gl::CreateShader(type_);
|
|
gl::ShaderSource(shader, 1, &src.as_ptr(), std::ptr::null());
|
|
gl::CompileShader(shader);
|
|
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)
|
|
}
|