use std::collections::HashMap;
use std::num::NonZeroU32;
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use egui_glow::EguiGlow;
use glutin::config::ConfigTemplateBuilder;
use glutin::context::{ContextApi, ContextAttributesBuilder, Version};
use glutin::display::GetGlDisplay;
use glutin::prelude::*;
use glutin::surface::SurfaceAttributesBuilder;
use glutin_winit::DisplayBuilder;
use raw_window_handle::HasWindowHandle;
use winit::dpi::LogicalSize;
use winit::event::WindowEvent;
use winit::event_loop::ActiveEventLoop;
use winit::window::{Window, WindowAttributes};
pub use crate::frontends::native::egui_texture::NativeTexture as LoadedTexture;
pub struct BrowserGl {
window: Arc<Window>,
surface: glutin::surface::Surface<glutin::surface::WindowSurface>,
gl_context: glutin::context::PossiblyCurrentContext,
pub egui_glow: EguiGlow,
#[allow(dead_code)]
glow_context: Arc<egui_glow::glow::Context>,
last_frame: Instant,
textures: HashMap<TextureKey, LoadedTexture>,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub enum TextureKey {
CoverArt(i64),
Screenshot(i64, usize),
Placeholder,
}
impl BrowserGl {
pub fn new(
event_loop: &ActiveEventLoop,
width: u32,
height: u32,
fullscreen: bool,
) -> Result<Self, String> {
let mut window_attrs = WindowAttributes::default()
.with_title("NESER - ROM Browser")
.with_inner_size(LogicalSize::new(width, height))
.with_resizable(true);
if fullscreen {
window_attrs =
window_attrs.with_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
}
let config_template = ConfigTemplateBuilder::new()
.with_alpha_size(8)
.with_depth_size(0)
.with_stencil_size(0);
let display_builder = DisplayBuilder::new().with_window_attributes(Some(window_attrs));
let (window, gl_config) = display_builder
.build(event_loop, config_template, |configs| {
configs
.reduce(|accum, config| {
if config.num_samples() > accum.num_samples() {
config
} else {
accum
}
})
.expect("no compatible GL configurations found")
})
.map_err(|e| format!("failed to build display: {e}"))?;
let window = Arc::new(window.ok_or("failed to create browser window")?);
let raw_window_handle = window
.window_handle()
.map_err(|e| format!("failed to get window handle: {e}"))?
.as_raw();
let gl_display = gl_config.display();
let context_attributes = ContextAttributesBuilder::new()
.with_context_api(ContextApi::OpenGl(Some(Version::new(3, 2))))
.build(Some(raw_window_handle));
let not_current_context = unsafe {
gl_display
.create_context(&gl_config, &context_attributes)
.map_err(|e| format!("failed to create GL context: {e}"))?
};
let size = window.inner_size();
let surface_attrs = SurfaceAttributesBuilder::<glutin::surface::WindowSurface>::new()
.build(
raw_window_handle,
NonZeroU32::new(size.width.max(1)).expect("non-zero width"),
NonZeroU32::new(size.height.max(1)).expect("non-zero height"),
);
let surface = unsafe {
gl_display
.create_window_surface(&gl_config, &surface_attrs)
.map_err(|e| format!("failed to create window surface: {e}"))?
};
let gl_context = not_current_context
.make_current(&surface)
.map_err(|e| format!("failed to make GL context current: {e}"))?;
gl::load_with(|s| {
gl_display
.get_proc_address(std::ffi::CString::new(s).expect("valid CString").as_c_str())
as *const _
});
unsafe {
gl::Disable(gl::DEPTH_TEST);
gl::Disable(gl::CULL_FACE);
gl::Viewport(0, 0, size.width as i32, size.height as i32);
gl::ClearColor(0.08, 0.08, 0.12, 1.0);
}
let glow_context = Arc::new(unsafe {
egui_glow::glow::Context::from_loader_function(|s| {
let s = std::ffi::CString::new(s).expect("valid CString");
gl_display.get_proc_address(s.as_c_str()).cast()
})
});
let egui_glow = EguiGlow::new(event_loop, glow_context.clone(), None, None, true);
egui_glow
.egui_ctx
.set_fonts(crate::frontends::native::egui_theme::native_font_definitions());
egui_glow
.egui_ctx
.set_visuals(crate::frontends::native::egui_theme::native_dark_visuals());
let _ = surface.set_swap_interval(
&gl_context,
glutin::surface::SwapInterval::Wait(NonZeroU32::new(1).unwrap()),
);
Ok(Self {
window,
surface,
gl_context,
egui_glow,
glow_context,
last_frame: Instant::now(),
textures: HashMap::new(),
})
}
pub fn window(&self) -> &Window {
&self.window
}
pub fn notify_resize(&mut self, width: u32, height: u32) {
self.surface.resize(
&self.gl_context,
NonZeroU32::new(width.max(1)).unwrap(),
NonZeroU32::new(height.max(1)).unwrap(),
);
}
pub fn on_window_event(&mut self, event: &WindowEvent) -> egui_winit::EventResponse {
self.egui_glow.on_window_event(&self.window, event)
}
pub fn run_frame<F>(&mut self, build_ui: F)
where
F: FnMut(&mut egui::Ui),
{
let size = self.window.inner_size();
unsafe {
gl::Viewport(0, 0, size.width as i32, size.height as i32);
gl::Clear(gl::COLOR_BUFFER_BIT);
}
self.egui_glow.run(&self.window, build_ui);
self.egui_glow.paint(&self.window);
self.surface
.swap_buffers(&self.gl_context)
.expect("swap_buffers failed");
self.last_frame = Instant::now();
}
pub fn load_texture_from_file(
&mut self,
key: TextureKey,
path: &Path,
) -> Option<LoadedTexture> {
if let Some(existing) = self.textures.get(&key) {
return Some(*existing);
}
let img = image::open(path).ok()?.into_rgba8();
let (w, h) = img.dimensions();
let pixels = img.into_raw();
let loaded = unsafe { self.create_and_upload_texture(w, h, &pixels) };
self.textures.insert(key, loaded);
Some(loaded)
}
pub fn load_texture_from_rgba(
&mut self,
key: TextureKey,
width: u32,
height: u32,
pixels: &[u8],
) -> LoadedTexture {
if let Some(existing) = self.textures.get(&key) {
return *existing;
}
let loaded = unsafe { self.create_and_upload_texture(width, height, pixels) };
self.textures.insert(key, loaded);
loaded
}
pub fn get_texture(&self, key: &TextureKey) -> Option<&LoadedTexture> {
self.textures.get(key)
}
pub fn remove_texture(&mut self, key: &TextureKey) {
if let Some(loaded) = self.textures.remove(key) {
self.egui_glow.painter.free_texture(loaded.egui_id);
}
}
pub fn texture_count(&self) -> usize {
self.textures.len()
}
pub fn texture_keys(&self) -> Vec<TextureKey> {
self.textures.keys().cloned().collect()
}
unsafe fn create_and_upload_texture(
&mut self,
width: u32,
height: u32,
pixels: &[u8],
) -> LoadedTexture {
use egui_glow::glow::HasContext as _;
let gl = &self.glow_context;
let tex = unsafe { gl.create_texture().expect("GL texture creation failed") };
unsafe {
gl.bind_texture(egui_glow::glow::TEXTURE_2D, Some(tex));
gl.tex_parameter_i32(
egui_glow::glow::TEXTURE_2D,
egui_glow::glow::TEXTURE_MIN_FILTER,
egui_glow::glow::LINEAR as i32,
);
gl.tex_parameter_i32(
egui_glow::glow::TEXTURE_2D,
egui_glow::glow::TEXTURE_MAG_FILTER,
egui_glow::glow::LINEAR as i32,
);
gl.tex_parameter_i32(
egui_glow::glow::TEXTURE_2D,
egui_glow::glow::TEXTURE_WRAP_S,
egui_glow::glow::CLAMP_TO_EDGE as i32,
);
gl.tex_parameter_i32(
egui_glow::glow::TEXTURE_2D,
egui_glow::glow::TEXTURE_WRAP_T,
egui_glow::glow::CLAMP_TO_EDGE as i32,
);
gl.pixel_store_i32(egui_glow::glow::UNPACK_ALIGNMENT, 1);
gl.tex_image_2d(
egui_glow::glow::TEXTURE_2D,
0,
egui_glow::glow::RGBA8 as i32,
width as i32,
height as i32,
0,
egui_glow::glow::RGBA,
egui_glow::glow::UNSIGNED_BYTE,
egui_glow::glow::PixelUnpackData::Slice(Some(pixels)),
);
}
let egui_id = self.egui_glow.painter.register_native_texture(tex);
LoadedTexture {
egui_id,
width,
height,
}
}
pub fn drawable_size(&self) -> (u32, u32) {
let size = self.window.inner_size();
(size.width, size.height)
}
pub fn logical_size(&self) -> (f32, f32) {
let size = self.window.inner_size();
let scale = self.window.scale_factor() as f32;
(size.width as f32 / scale, size.height as f32 / scale)
}
pub fn delta_time(&self) -> f32 {
self.last_frame.elapsed().as_secs_f32()
}
}
impl Drop for BrowserGl {
fn drop(&mut self) {
self.egui_glow.destroy();
}
}